d-points: dejar sólo Stripe y SumUp con compra de PD funcional

Elimina las pestañas de PayPal/dLocal/Crypto/Skrill y por país; deja
Información + Stripe + SumUp (las dos únicas pasarelas que se usarán).

- Stripe: totalmente funcional reutilizando createCheckoutSession + webhook
  + página de éxito. Nueva ruta /api/dpoints/checkout crea la sesión con
  importe elegido por el usuario (1 unidad = 100 PD).
- SumUp: integración real (lib/sumup.ts) mediante checkout hospedado,
  habilitada al definir SUMUP_API_KEY/SUMUP_MERCHANT_CODE; si no, avisa.
- Nuevo servicio de entrega `dpoints` (lib/paid-services.ts) que acredita
  PD en home_api_points; entrega idempotente vía reclamo atómico.
- Página /d-points-success entrega ambos proveedores (session_id/ref).
- Iconos SVG generados para cada pasarela (stripe/sumup, marca + wordmark).
- Moneda configurable con DP_CURRENCY (por defecto EUR).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 17:14:29 +00:00
parent 44fcedbe62
commit 89f6e5689f
13 changed files with 453 additions and 693 deletions
@@ -0,0 +1,54 @@
import { setRequestLocale } from 'next-intl/server'
import { Link } from '@/i18n/navigation'
import { fulfillCheckoutSession } from '@/lib/fulfill'
import { isSessionPaid } from '@/lib/stripe'
import { fulfillSumUpCheckout } from '@/lib/sumup'
import { PageShell } from '@/components/PageShell'
export const dynamic = 'force-dynamic'
export default async function DPointsSuccessPage({
params,
searchParams,
}: {
params: Promise<{ locale: string }>
searchParams: Promise<{ provider?: string; session_id?: string; ref?: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const { provider, session_id, ref } = await searchParams
// Entrega compartida con el webhook/return (idempotente vía reclamo atómico).
let status: 'ok' | 'already' | 'error' = 'error'
if (provider === 'sumup' && ref) {
const r = await fulfillSumUpCheckout(ref)
if (r.ok) status = 'ok'
else if (r.paid) status = 'already'
} else if (session_id) {
const r = await fulfillCheckoutSession(session_id)
if (r.ok) status = 'ok'
else if (await isSessionPaid(session_id)) status = 'already'
}
return (
<PageShell title="Adquirir PD">
<div className="box-content">
<div className="body-box-content centered">
{status === 'ok' ? (
<span className="ok-form-response">¡Pago completado! Tus PD ya fueron acreditados a tu cuenta.</span>
) : status === 'already' ? (
<span className="ok-form-response">Este pago ya había sido procesado. Tus PD están acreditados.</span>
) : (
<span className="red-form-response">No pudimos confirmar tu pago. Si se realizó el cargo, contacta con soporte.</span>
)}
<br />
<br />
<p>
<Link href="/account"> Regresar a mi cuenta</Link>
</p>
</div>
</div>
</PageShell>
)
}
+2 -17
View File
@@ -1,5 +1,4 @@
import type { Metadata } from 'next'
import { headers } from 'next/headers'
import { setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
@@ -20,25 +19,11 @@ export default async function DPointsPage({ params }: { params: Promise<{ locale
if (!session.username) redirect({ href: '/select-account', locale })
const realm = await getRealmName()
const h = await headers()
const ip = (h.get('x-forwarded-for') || '').split(',')[0].trim() || '0.0.0.0'
const host = h.get('x-forwarded-host') || h.get('host') || ''
const proto = h.get('x-forwarded-proto') || 'https'
const origin = host ? `${proto}://${host}` : ''
// Pasarelas automáticas: se habilitan al configurar sus credenciales por entorno.
const paypalBusiness = process.env.PAYPAL_BUSINESS_EMAIL || ''
const currency = (process.env.DP_CURRENCY || 'eur').toLowerCase()
return (
<PageShell title="Adquirir PD">
<DPointsTabs
realm={realm}
username={session.username!}
ip={ip}
locale={locale}
origin={origin}
paypalBusiness={paypalBusiness}
/>
<DPointsTabs realm={realm} currency={currency} />
</PageShell>
)
}
@@ -0,0 +1,78 @@
import { randomUUID } from 'crypto'
import { getSession } from '@/lib/session'
import { createCheckoutSession } from '@/lib/stripe'
import { createSumUpCheckout, sumupConfigured } from '@/lib/sumup'
import { PD_PER_UNIT, DP_MIN_AMOUNT, DP_MAX_AMOUNT } from '@/lib/dpoints'
/**
* Inicia la compra de PD por Stripe o SumUp. El importe lo elige el usuario
* (1 unidad = 100 PD). Devuelve la URL de checkout a la que redirigir.
*/
export async function POST(request: Request) {
const session = await getSession()
if (!session.accountId || !session.username) {
return Response.json({ success: false, error: 'notAuthenticated' }, { status: 401 })
}
let body: Record<string, unknown> = {}
try {
body = await request.json()
} catch {
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
}
const provider = String(body.provider ?? '')
if (provider !== 'stripe' && provider !== 'sumup') {
return Response.json({ success: false, error: 'invalidProvider' }, { status: 400 })
}
const amount = Number(body.amount)
if (!Number.isInteger(amount) || amount < DP_MIN_AMOUNT || amount > DP_MAX_AMOUNT) {
return Response.json({ success: false, error: 'invalidAmount' })
}
const points = amount * PD_PER_UNIT
const site = process.env.SITE_URL || ''
const ip = (request.headers.get('x-forwarded-for') || '').split(',')[0].trim() || '0.0.0.0'
const currency = process.env.DP_CURRENCY || 'eur'
if (provider === 'stripe') {
const r = await createCheckoutSession({
accountId: session.accountId,
username: session.username,
email: session.bnetEmail || '',
acoreIp: ip,
amount,
currency,
characterName: session.username,
productName: `${points} PD`,
successUrl: `${site}/d-points-success?provider=stripe`,
cancelUrl: `${site}/d-points`,
metadata: {
service: 'dpoints',
accountId: String(session.accountId),
points: String(points),
amount: String(amount),
},
})
if (!r.success || !r.url) return Response.json({ success: false, error: 'stripeError' })
return Response.json({ success: true, url: r.url })
}
// provider === 'sumup'
if (!sumupConfigured()) return Response.json({ success: false, error: 'notConfigured' })
const reference = randomUUID()
const r = await createSumUpCheckout({
accountId: session.accountId,
username: session.username,
email: session.bnetEmail || '',
acoreIp: ip,
amount,
points,
reference,
description: `${points} PD`,
returnUrl: `${site}/d-points-success?provider=sumup&ref=${reference}`,
})
if (!r.success || !r.url) return Response.json({ success: false, error: r.error || 'sumupError' })
return Response.json({ success: true, url: r.url })
}
+8
View File
@@ -419,3 +419,11 @@ textarea:focus {
.class-warlock { color: #9482c9; }
.class-druid { color: #ff7d0a; }
.class-unknown { color: #f0b100; }
/* Logos en línea de las pasarelas de pago (lista de métodos en /d-points) */
.pay-inline-logo {
height: 18px;
width: auto;
vertical-align: middle;
margin: 0 2px;
}
+150 -676
View File
@@ -1,23 +1,12 @@
'use client'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { Link } from '@/i18n/navigation'
const IMG = '/nw-themes/nw-ryu/nw-images'
const LOGOS = `${IMG}/nw-logos`
const RANKS = `${IMG}/nw-ranks`
const FLAGS = `${IMG}/nw-country-flags`
const LOGOS = '/nw-themes/nw-ryu/nw-images/nw-logos'
const RANKS = '/nw-themes/nw-ryu/nw-images/nw-ranks'
type TabId = 'Info' | 'PayPal' | 'dLocal' | 'Crypto' | 'Skrill' | 'Argentina' | 'Chile' | 'Colombia' | 'Perú'
// Las banderas son .png sólo para México y Panamá; el resto son .webp.
function flagSrc(code: string) {
const ext = code === 'mx' || code === 'pa' ? 'png' : 'webp'
return `${FLAGS}/${code}.${ext}`
}
function Flag({ code, name }: { code: string; name: string }) {
return <img src={flagSrc(code)} alt={name} title={`WoW ${name}`} />
}
type TabId = 'Info' | 'Stripe' | 'SumUp'
const RANK_ROWS = [
['1_Newbie', 'Nivel 1', 'Desde 0 PD'],
@@ -36,23 +25,10 @@ const RANK_ROWS = [
['14_GrandMaster', 'Nivel 14', 'Más de 10000 PD'],
]
export function DPointsTabs({
realm,
username,
ip,
locale,
origin,
paypalBusiness,
}: {
realm: string
username: string
ip: string
locale: string
origin: string
paypalBusiness: string
}) {
export function DPointsTabs({ realm, currency }: { realm: string; currency: string }) {
const [active, setActive] = useState<TabId>('Info')
const [showRanks, setShowRanks] = useState(false)
const sym = currency === 'usd' ? '$' : '€'
return (
<>
@@ -68,30 +44,11 @@ export function DPointsTabs({
<Link className="back-to-account-responsive" href="/account">Regresar</Link>
</div>
<div className="dnt-sidebar dnt-methods">
<button className={`dnt-button-2 tablink${active === 'PayPal' ? ' dnt-selected' : ''}`} onClick={() => setActive('PayPal')}>
<img src={`${LOGOS}/paypal-p-logo.webp`} alt="PayPal" title="PayPal" /> PayPal
<button className={`dnt-button-2 tablink${active === 'Stripe' ? ' dnt-selected' : ''}`} onClick={() => setActive('Stripe')}>
<img src={`${LOGOS}/stripe-p-logo.svg`} alt="Stripe" title="Stripe" /> Stripe
</button>
<button className={`dnt-button-2 tablink${active === 'dLocal' ? ' dnt-selected' : ''}`} onClick={() => setActive('dLocal')}>
<img src={`${LOGOS}/dlocalgo-logo.webp`} alt="dLocal" title="dLocal" /> dLocal go
</button>
<button className={`dnt-button-2 tablink${active === 'Crypto' ? ' dnt-selected' : ''}`} onClick={() => setActive('Crypto')}>
<i className="fab fa-bitcoin" style={{ color: '#f7931a' }}></i>{' '}
<span className="new-msg"><sup>¡Nuevo!</sup></span> Crypto
</button>
<button className={`dnt-button-2 tablink${active === 'Skrill' ? ' dnt-selected' : ''}`} onClick={() => setActive('Skrill')}>
<img src={`${LOGOS}/skrill-logo-small.webp`} alt="Skrill" title="Skrill" /> Skrill
</button>
<button className={`dnt-button-2 tablink${active === 'Argentina' ? ' dnt-selected' : ''}`} onClick={() => setActive('Argentina')}>
<Flag code="ar" name="Argentina" /> Argentina
</button>
<button className={`dnt-button-2 tablink${active === 'Chile' ? ' dnt-selected' : ''}`} onClick={() => setActive('Chile')}>
<Flag code="cl" name="Chile" /> Chile
</button>
<button className={`dnt-button-2 tablink${active === 'Colombia' ? ' dnt-selected' : ''}`} onClick={() => setActive('Colombia')}>
<Flag code="co" name="Colombia" /> Colombia
</button>
<button className={`dnt-button-2 tablink${active === 'Perú' ? ' dnt-selected' : ''}`} onClick={() => setActive('Perú')}>
<Flag code="pe" name="Perú" /> Perú
<button className={`dnt-button-2 tablink${active === 'SumUp' ? ' dnt-selected' : ''}`} onClick={() => setActive('SumUp')}>
<img src={`${LOGOS}/sumup-p-logo.svg`} alt="SumUp" title="SumUp" /> SumUp
</button>
</div>
</div>
@@ -132,682 +89,199 @@ export function DPointsTabs({
<br />
<span>¿Cuáles son los métodos de compra de PD?</span>
<p>Los métodos de compra disponibles actualmente son:</p>
<p>- PayPal (Para todos los países)</p>
<p>- dLocal Go para Argentina <Flag code="ar" name="Argentina" />, Bolivia <Flag code="bo" name="Bolivia" />, Chile <Flag code="cl" name="Chile" />, Colombia <Flag code="co" name="Colombia" />, Costa Rica <Flag code="cr" name="Costa Rica" />, Ecuador <Flag code="ec" name="Ecuador" />, Guatemala <Flag code="gt" name="Guatemala" />, México <Flag code="mx" name="México" />, Panamá <Flag code="pa" name="Panamá" />, Perú <Flag code="pe" name="Perú" />, Paraguay <Flag code="py" name="Paraguay" />, Uruguay <Flag code="uy" name="Uruguay" />.</p>
<p>- Criptomonedas (Para todos los países)</p>
<p>- Skrill (Para todos los países)</p>
<p>- Métodos alternativos sólo para Argentina (<Flag code="ar" name="Argentina" /> ARG)</p>
<p>- Métodos alternativos sólo para Chile ( <Flag code="cl" name="Chile" /> CHI)</p>
<p>- Métodos alternativos sólo para Colombia (<Flag code="co" name="Colombia" /> COL)</p>
<p>- Métodos alternativos sólo para Perú (<Flag code="pe" name="Perú" /> PER)</p>
<br />
<p>Puedes hacer una sugerencia sobre nuevos métodos en nuestro foro de sugerencias <Link href="/forum">aquí</Link>.</p>
<p>- <img className="pay-inline-logo" src={`${LOGOS}/stripe-p-logo.svg`} alt="Stripe" /> Stripe (tarjeta de crédito/débito, para todos los países).</p>
<p>- <img className="pay-inline-logo" src={`${LOGOS}/sumup-p-logo.svg`} alt="SumUp" /> SumUp (tarjeta de crédito/débito).</p>
<br />
<hr />
<br />
<span>¿Cuántos PD se reciben?</span>
<p>Cada método indicará cuántos PD se reciben.</p>
<p>Por cada {sym}1 recibes 100 PD. La cantidad final se indica antes de confirmar el pago.</p>
<br />
<hr />
<br />
<span>¿Tienes dudas o problemas con una compra?</span>
<p>No realices una compra hasta estar completamente seguro de que has leído esta información y sabes cómo es el proceso que vayas a escoger.</p>
<p>Si a pesar de leer este instructivo o el de alguna de nuestras herramientas, aún tienes dudas, el equipo de {realm} podrá asesorarte en cualquier momento. Puedes colocar un ticket en el servidor, o contactarnos mediante nuestra plataforma de Discord.</p>
<p>Si a pesar de leer este instructivo aún tienes dudas, el equipo de {realm} podrá asesorarte en cualquier momento. Puedes colocar un ticket en el servidor, o contactarnos mediante nuestra plataforma de Discord.</p>
</div>
</div>
)}
{active === 'PayPal' && (
<PayPalTab realm={realm} username={username} ip={ip} origin={origin} locale={locale} paypalBusiness={paypalBusiness} />
)}
{active === 'dLocal' && <DLocalTab username={username} realm={realm} />}
{active === 'Crypto' && <CryptoTab username={username} realm={realm} />}
{active === 'Skrill' && <SkrillTab />}
{active === 'Argentina' && <ArgentinaTab />}
{active === 'Chile' && <ChileTab />}
{active === 'Colombia' && <ColombiaTab />}
{active === 'Perú' && <PeruTab />}
{active === 'Stripe' && <StripeTab realm={realm} sym={sym} />}
{active === 'SumUp' && <SumUpTab realm={realm} sym={sym} />}
</div>
</>
)
}
/* ---------- Nota común para pasarelas automáticas sin backend configurado ---------- */
function gatewayUnavailable(realm: string) {
alert(`Este método de pago aún no está disponible en ${realm}. Por favor, inténtalo de nuevo más tarde o utiliza otro método.`)
/* --------------------------- Lógica de pago común --------------------------- */
function checkoutError(error?: string): string {
switch (error) {
case 'notConfigured':
return 'Este método de pago aún no está disponible. Por favor, utiliza otro método o inténtalo más tarde.'
case 'invalidAmount':
return 'La cantidad debe ser un número entero entre 1 y 200.'
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
default:
return 'No se pudo iniciar el pago. Inténtalo de nuevo.'
}
}
/* ---------------------------------- PayPal ---------------------------------- */
function PayPalTab({
realm,
username,
ip,
origin,
locale,
paypalBusiness,
}: {
realm: string
username: string
ip: string
origin: string
locale: string
paypalBusiness: string
}) {
function usePayment(provider: 'stripe' | 'sumup') {
const [amount, setAmount] = useState('')
const [accepted, setAccepted] = useState(false)
const configured = Boolean(paypalBusiness)
const [busy, setBusy] = useState(false)
const [err, setErr] = useState<string | null>(null)
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
if (!configured) {
e.preventDefault()
gatewayUnavailable(realm)
async function pay(e: React.FormEvent<HTMLFormElement>, sym: string) {
e.preventDefault()
if (!accepted) {
alert('Debes aceptar los Términos y Condiciones y la Política de Reembolso')
return
}
const form = e.currentTarget
const amount = (form.elements.namedItem('amount') as HTMLInputElement)?.value
if (!window.confirm(`Por favor revisa que el valor en USD que has ingresado es correcto.\nEl valor ingresado es el costo final en USD que pagarás al ser redirigido.\n\nEl monto a pagar es ${amount} USD.`)) {
e.preventDefault()
const n = Number(amount)
if (!Number.isInteger(n) || n < 1 || n > 200) {
setErr(checkoutError('invalidAmount'))
return
}
if (!window.confirm(`Vas a adquirir ${n * 100} PD por ${sym}${n}.\n¿Deseas continuar al pago?`)) return
setBusy(true)
setErr(null)
try {
const res = await fetch('/api/dpoints/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ provider, amount: n }),
})
const d: { success?: boolean; url?: string; error?: string } = await res.json()
if (d.success && d.url) {
window.location.href = d.url
return
}
setErr(checkoutError(d.error))
setBusy(false)
} catch {
setErr(checkoutError())
setBusy(false)
}
}
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué es PayPal?</span><img className="pp-icon" src={`${LOGOS}/paypal-logo.webp`} alt="PayPal" />
<p>Un servicio que te permite pagar, enviar dinero y aceptar pagos sin tener que introducir tus datos financieros continuamente. Paga con PayPal de forma más rápida y segura. 250 millones de personas de más de 200 países y mercados usan PayPal. Y en 25 divisas.<br />Más información sobre PayPal <a href="https://www.paypal.com/es/webapps/mpp/paypal-popup" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<br />
<hr />
<br />
<span>¿Cómo adquirir PD a través de PayPal?</span>
<p>Al ser un sistema auto-gestionado, las adquisiciones mediante este sistema son automáticas y en cuestión de minutos tras realizar tu compra, recibirás los PD en tu cuenta en la página web.</p>
<br />
<hr />
<br />
<p>- Introduce en el campo de texto que aparece más abajo (el recuadro negro), la cantidad que deseas adquirir en USD.</p>
<p>- Al presionar el botón ADQUIRIR PD, serás redirigido a la plataforma de PayPal donde tendrás que ingresar con tu cuenta de PayPal y confirmar la compra en su plataforma.</p>
<p>- Una vez que PayPal valide tu pago, tendrás los PD en tu cuenta.</p>
<br />
<p>Por favor ten en cuenta que a la hora de realizar tu compra, la cantidad a adquirir sólo acepta números enteros y el mínimo posible es $1 USD.</p>
<p>Ejemplos de valor incorrecto: <span className="red-info2">$5,90 | $22,30 | $53,10</span></p>
<p>Ejemplos de valor correcto: <span className="green-info2">$6 | $22 | $53</span></p>
<br /><br /><br />
<p className="yellow-info centered">$1 USD = 100 PD</p>
<p className="centered second-brown">Valor máximo por transacción: $200</p>
<br />
<fieldset>
<div className="separate2">
<p className="centered red-info2">¡Importante!</p>
<p className="centered">No añadas ceros decimales, sólo valores enteros.</p>
<p className="centered">Ejemplo: Si deseas 200 PD, escribe sólo el número 2, sin ceros.</p>
</div>
</fieldset>
<br />
<p className="centered">Cantidad en USD a adquirir</p>
<form className="centered" name="_xclick" id="pp-purch-form" action="https://www.paypal.com/cgi-bin/webscr" method="post" onSubmit={onSubmit}>
<input type="hidden" name="cmd" value="_xclick" />
<input type="number" name="amount" id="pp-input-value" placeholder="Cantidad en USD" min="1" max="200" step="1" required /> USD
<input type="hidden" name="charset" value="utf-8" />
<input type="hidden" name="custom" value={`username=${username}&ip=${ip}`} />
<input type="hidden" name="currency_code" value="USD" />
<input type="hidden" name="item_name" value="Points" />
<input type="hidden" name="item_number" value="2023" />
<input type="hidden" name="business" value={paypalBusiness} />
<input type="hidden" name="return" value={`${origin}/${locale}/account`} />
<input type="hidden" name="cancel_return" value={`${origin}/${locale}/account`} />
<input type="hidden" name="notify_url" value={`${origin}/api/paypal/ipn`} />
<br /><br />
<input type="checkbox" id="accept-terms-refund-pp" className="terms-check" name="accept-terms-refund-pp" checked={accepted} onChange={(e) => setAccepted(e.target.checked)} required />
<label htmlFor="accept-terms-refund-pp"> Acepto los <Link href="/terms-and-conditions" target="_blank">Términos y Condiciones</Link> y la <Link href="/refund-policy" target="_blank">Política de Reembolso</Link></label>
<br /><br />
<input type="submit" id="pp-dnt-button" className="dnt-button" value="ADQUIRIR PD" disabled={!accepted} />
</form>
</div>
</div>
)
return { amount, setAmount, accepted, setAccepted, busy, err, pay }
}
/* ---------------------------------- dLocal ---------------------------------- */
function DLocalTab({ username, realm }: { username: string; realm: string }) {
const [accepted, setAccepted] = useState(false)
/* -------------------------------- Formulario -------------------------------- */
function AmountForm({
provider,
sym,
buttonLabel,
}: {
provider: 'stripe' | 'sumup'
sym: string
buttonLabel: string
}) {
const st = usePayment(provider)
const points = Number(st.amount) > 0 ? Number(st.amount) * 100 : 0
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué es dLocal Go?</span><img className="pp-icon" src={`${LOGOS}/dlocalgo-logo.webp`} alt="dLocal Go" />
<p>dLocal Go es una plataforma de pagos que permite una conexión en directo con todas las tarjetas de crédito y débito, pagos en efectivo y transferencias bancarias en diferentes países de América Latina.<br />Más información sobre dLocal Go <a href="https://dlocalgo.com/" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<br />
<hr />
<br />
<span>¿Cómo adquirir PD a través de dlocal Go?</span>
<p>Al ser un sistema auto-gestionado, las adquisiciones mediante este sistema son automáticas y tras realizar tu compra, recibirás los PD en tu cuenta en la página web.</p>
<div className="trade-information">
<p>Tiempo aproximado de procesamiento de pagos según método:</p>
<p>- Tarjeta de crédito/debito: De minutos a 1 día hábil</p>
<p>- Efectivo: 1 a 2 días hábiles</p>
<p>- Transferencia bancaria: 1 día hábil</p>
<>
<p className="yellow-info centered">{sym}1 = 100 PD</p>
<p className="centered second-brown">Valor mínimo: {sym}1 · Valor máximo por transacción: {sym}200</p>
<br />
<fieldset>
<div className="separate2">
<p className="centered red-info2">¡Importante!</p>
<p className="centered">Introduce sólo valores enteros (sin decimales).</p>
<p className="centered">Ejemplo: para 200 PD escribe 2.</p>
</div>
</fieldset>
<br />
<p className="centered">Cantidad en {sym === '$' ? 'USD' : 'EUR'} a adquirir</p>
<form className="centered" onSubmit={(e) => st.pay(e, sym)}>
<input
type="number"
placeholder={`Cantidad en ${sym === '$' ? 'USD' : 'EUR'}`}
min="1"
max="200"
step="1"
value={st.amount}
onChange={(e) => st.setAmount(e.target.value)}
required
/>{' '}
{sym === '$' ? 'USD' : 'EUR'}
{points > 0 && <p className="centered green-info2">Recibirás {points} PD</p>}
<br /><br />
<p><i className="fas fa-exclamation-triangle"></i> <span>Importante:</span></p>
<p>Los tiempos indicados en la tabla son orientativos. En algunas ocasiones, la demora puede ser mayor.</p>
<p>Ante cualquier duda con la demora, por favor contáctanos en nuestra plataforma de Discord o a través de los medios de contacto disponibles en <Link href="/help">Ayuda</Link>.</p>
<input
type="checkbox"
id={`accept-${provider}`}
className="terms-check"
checked={st.accepted}
onChange={(e) => st.setAccepted(e.target.checked)}
required
/>
<label htmlFor={`accept-${provider}`}> Acepto los <Link href="/terms-and-conditions" target="_blank">Términos y Condiciones</Link> y la <Link href="/refund-policy" target="_blank">Política de Reembolso</Link></label>
<br /><br />
{st.err && <p className="red-form-response">{st.err}</p>}
<button type="submit" className="dnt-button" disabled={!st.accepted || st.busy}>
{st.busy ? 'Redirigiendo…' : buttonLabel}
</button>
</form>
</>
)
}
/* ---------------------------------- Stripe ---------------------------------- */
function StripeTab({ realm, sym }: { realm: string; sym: string }) {
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué es Stripe?</span><img className="pp-icon" src={`${LOGOS}/stripe-logo.svg`} alt="Stripe" />
<p>Stripe es una plataforma de pagos en línea segura que procesa tarjetas de crédito y débito de todo el mundo. Tus datos de pago se gestionan directamente en Stripe; {realm} nunca los almacena.<br />Más información sobre Stripe <a href="https://stripe.com/es" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<br />
<hr />
<br />
<p>- Introduce en el campo de texto que aparece más abajo (el recuadro negro), la cantidad que deseas adquirir en USD.</p>
<p>- Al presionar el botón ADQUIRIR PD, serás redirigido a la plataforma de dLocal Go donde tendrás que realizar el pago y completar la compra en su plataforma.</p>
<p>- Una vez que dLocal Go valide tu pago, tendrás los PD en tu cuenta.</p>
<span>¿Cómo adquirir PD a través de Stripe?</span>
<p>Al ser un sistema auto-gestionado, las adquisiciones son automáticas y en cuestión de minutos tras completar el pago recibirás los PD en tu cuenta.</p>
<br />
<p>Por favor ten en cuenta que a la hora de realizar tu compra, la cantidad a adquirir sólo acepta números enteros y el mínimo posible es $2 USD.</p>
<p>Ejemplos de valor incorrecto: <span className="red-info2">$5,90 | $22,30 | $53,10</span></p>
<p>Ejemplos de valor correcto: <span className="green-info2">$6 | $22 | $53</span></p>
<hr />
<br />
<p>- Introduce en el campo de más abajo la cantidad que deseas adquirir.</p>
<p>- Al presionar <span>ADQUIRIR PD</span>, serás redirigido a la pasarela segura de Stripe para completar el pago con tu tarjeta.</p>
<p>- Una vez que Stripe valide tu pago, tendrás los PD en tu cuenta de forma automática.</p>
<br />
<p>Recuerda que la cantidad sólo acepta números enteros y el mínimo posible es {sym}1.</p>
<p>Ejemplos de valor incorrecto: <span className="red-info2">{sym}5,90 | {sym}22,30 | {sym}53,10</span></p>
<p>Ejemplos de valor correcto: <span className="green-info2">{sym}6 | {sym}22 | {sym}53</span></p>
<br /><br /><br />
<p className="yellow-info centered">$1 USD = 100 PD</p>
<p className="centered second-brown">Valor mínimo por transacción: $2</p>
<br />
<p className="centered">Cantidad en USD a adquirir</p>
<form className="centered" id="dlocal-form" onSubmit={(e) => { e.preventDefault(); if (!accepted) { alert('Debes aceptar los Términos y Condiciones y la Política de Reembolso'); return } gatewayUnavailable(realm) }}>
<input type="number" placeholder="Cantidad en USD" min="2" step="1" max="200" id="cantidad" name="cantidad" required /> USD
<input type="hidden" id="username" name="username" value={username} />
<br /><br />
<input type="checkbox" id="accept-terms-refund-dl" className="terms-check" name="accept-terms-refund-dl" checked={accepted} onChange={(e) => setAccepted(e.target.checked)} required />
<label htmlFor="accept-terms-refund-dl"> Acepto los <Link href="/terms-and-conditions" target="_blank">Términos y Condiciones</Link> y la <Link href="/refund-policy" target="_blank">Política de Reembolso</Link></label>
<br /><br />
<button className="dnt-button" id="dl-dnt-button" type="submit" disabled={!accepted}>Adquirir PD</button>
</form>
<AmountForm provider="stripe" sym={sym} buttonLabel="ADQUIRIR PD" />
</div>
</div>
)
}
/* ---------------------------------- Crypto ---------------------------------- */
function CryptoTab({ username, realm }: { username: string; realm: string }) {
const [accepted, setAccepted] = useState(false)
/* ---------------------------------- SumUp ----------------------------------- */
function SumUpTab({ realm, sym }: { realm: string; sym: string }) {
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué son las criptomonedas?</span><img className="pp-icon" src={`${LOGOS}/nowpayments-logo.png`} alt="NOWPayments" title="NOWPayments" />
<p>Las criptomonedas son monedas digitales descentralizadas que permiten realizar pagos de forma rápida, segura y sin intermediarios. Aceptamos más de 150 criptomonedas a través de NOWPayments.</p>
<p>Más información sobre NOWPayments <a href="https://nowpayments.io/" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<span>¿Qué es SumUp?</span><img className="pp-icon" src={`${LOGOS}/sumup-logo.svg`} alt="SumUp" />
<p>SumUp es una plataforma de pagos europea que permite pagar de forma rápida y segura con tarjeta de crédito o débito. Tus datos de pago se gestionan directamente en SumUp; {realm} nunca los almacena.<br />Más información sobre SumUp <a href="https://sumup.com/es-es/" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<br />
<hr />
<br />
<span>¿Cómo adquirir PD con criptomonedas?</span>
<p>Al ser un sistema auto-gestionado, las adquisiciones mediante este sistema son automáticas y tras realizar tu compra, recibirás los PD en tu cuenta en la página web.</p>
<div className="trade-information">
<p>Tiempo aproximado de procesamiento según criptomoneda:</p>
<p>- BNB, USDT (BSC), USDC (BSC): 1-5 minutos</p>
<p>- ETH (Base), USDC (Base): 1-5 minutos</p>
<p>- Bitcoin (BTC): 10-60 minutos</p>
</div>
<br /><br />
<p><i className="fas fa-exclamation-triangle"></i> <span>Importante:</span></p>
<p>Los tiempos indicados son orientativos y dependen de la congestión de la red blockchain.</p>
<p>Ante cualquier duda, por favor contáctanos en nuestra plataforma de Discord o a través de los medios de contacto disponibles en <Link href="/help">Ayuda</Link>.</p>
<span>¿Cómo adquirir PD a través de SumUp?</span>
<p>Al ser un sistema auto-gestionado, las adquisiciones son automáticas y tras completar el pago recibirás los PD en tu cuenta.</p>
<br />
<hr />
<br />
<p>- Introduce en el campo de texto la cantidad que deseas adquirir en USD.</p>
<p>- Al presionar el botón ADQUIRIR PD, serás redirigido a la plataforma de NOWPayments donde podrás elegir la criptomoneda con la que deseas pagar.</p>
<p>- Una vez que NOWPayments confirme tu pago en la blockchain, tendrás los PD en tu cuenta.</p>
<p>- Introduce en el campo de más abajo la cantidad que deseas adquirir.</p>
<p>- Al presionar <span>ADQUIRIR PD</span>, serás redirigido a la pasarela segura de SumUp para completar el pago con tu tarjeta.</p>
<p>- Una vez que SumUp valide tu pago, tendrás los PD en tu cuenta de forma automática.</p>
<br />
<p>Por favor ten en cuenta que la cantidad a adquirir sólo acepta números enteros y el mínimo posible es $2 USD.</p>
<p>Ejemplos de valor incorrecto: <span className="red-info2">$5,90 | $22,30 | $53,10</span></p>
<p>Ejemplos de valor correcto: <span className="green-info2">$6 | $22 | $53</span></p>
<p>Recuerda que la cantidad sólo acepta números enteros y el mínimo posible es {sym}1.</p>
<p>Ejemplos de valor incorrecto: <span className="red-info2">{sym}5,90 | {sym}22,30 | {sym}53,10</span></p>
<p>Ejemplos de valor correcto: <span className="green-info2">{sym}6 | {sym}22 | {sym}53</span></p>
<br /><br /><br />
<p className="yellow-info centered">$1 USD = 100 PD</p>
<p className="centered second-brown">Valor mínimo por transacción: $2 | Valor máximo: $200</p>
<br />
<fieldset>
<div className="separate2">
<p className="centered red-info2">¡Importante!</p>
<p className="centered">Envía el monto <span>EXACTO</span> que indica NOWPayments.</p>
<p className="centered">Si envías menos, el pago quedará pendiente y deberás contactar a soporte.</p>
</div>
</fieldset>
<br />
<p className="centered">Cantidad en USD a adquirir</p>
<form className="centered" id="nowpayments-form" onSubmit={(e) => { e.preventDefault(); if (!accepted) { alert('Debes aceptar los Términos y Condiciones y la Política de Reembolso'); return } gatewayUnavailable(realm) }}>
<input type="number" placeholder="Cantidad en USD" min="2" max="200" step="1" id="cantidad-np" name="cantidad" required /> USD
<input type="hidden" id="username-np" name="username" value={username} />
<br /><br />
<input type="checkbox" id="accept-terms-refund-np" className="terms-check" name="accept-terms-refund-np" checked={accepted} onChange={(e) => setAccepted(e.target.checked)} required />
<label htmlFor="accept-terms-refund-np"> Acepto los <Link href="/terms-and-conditions" target="_blank">Términos y Condiciones</Link> y la <Link href="/refund-policy" target="_blank">Política de Reembolso</Link></label>
<br /><br />
<button className="dnt-button" id="np-dnt-button" type="submit" disabled={!accepted}>Adquirir PD</button>
</form>
</div>
</div>
)
}
/* ---------------------------------- Skrill ---------------------------------- */
function SkrillTab() {
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué es Skrill?</span><img className="pp-icon" src={`${LOGOS}/skrill-logo.webp`} alt="Skrill" />
<p>Skrill es una plataforma de pagos en línea rápida y segura. Envía dinero, paga y recibe pagos sin ingresar tus datos financieros constantemente. Presente en 200 países y acepta 40 monedas.</p>
<p>Más información sobre Skrill <a href="https://www.skrill.com/es/" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<br />
<hr />
<br />
<span>¿Cuál es el valor de los PD?</span>
<p>El valor será calculado según país en base al cambio indicado en el momento del envío.</p>
<br />
<p className="yellow-info centered">$1 USD = 100 PD</p>
<br />
<hr />
<br />
<span>¿Cómo adquirir PD por Skrill?</span>
<p>Paso 1) Inicia sesión en Skrill. Accede a tu cuenta desde la aplicación o la página web oficial de Skrill.</p>
<br />
<p>Paso 2) Envía el dinero</p>
<p>1. Selecciona la opción &quot;Enviar&quot; o &quot;Send Money&quot;.</p>
<p>2. Elige &quot;Skrill a Skrill&quot;.</p>
<p>3. Introduce el correo del destinatario: <a href="mailto:skrill@ultimowow.com">skrill@ultimowow.com</a></p>
<p>4. Indica el importe que deseas enviar.</p>
<p>5. Revisa los datos y confirma la operación.</p>
<br />
<p>Paso 3) Guarda el comprobante</p>
<p>Una vez completado el pago, realiza una captura de pantalla donde se vea claramente:</p>
<p>- El correo del destinatario.</p>
<p>- El importe enviado.</p>
<p>- El estado de la transacción (Completada, Exitosa o similar).</p>
<br />
<p>Paso 4) Envía la información</p>
<p>Envía un correo a <a href="mailto:skrill@ultimowow.com">skrill@ultimowow.com</a> indicando:</p>
<p className="second-brown">- El nombre de cualquier personaje de la cuenta donde deseas recibir los PD.</p>
<p className="second-brown">- La captura de pantalla de la transacción. No envíes el comprobante en formato PDF.</p>
<br />
<p>Paso 5) Recepción de PD</p>
<p>Una vez verificado el pago, los PD serán acreditados en tu cuenta lo antes posible. Cuando la entrega haya sido completada, serás informado.</p>
<br />
<hr />
<br />
<span>Contacto</span>
<p>Correo: <a href="mailto:skrill@ultimowow.com">skrill@ultimowow.com</a></p>
<br />
<p><i className="fas fa-exclamation-triangle"></i> Este método no es automático sino manual siendo realizado por la persona encargada.</p>
<p>Recomendamos no realizar ninguna acción sin comprobar primero si la persona encargada está disponible para entregar los PD.</p>
</div>
</div>
)
}
/* -------------------------------- Argentina --------------------------------- */
function ArgentinaTab() {
const [usd, setUsd] = useState(1)
const [rate, setRate] = useState<number | null>(null)
useEffect(() => {
let alive = true
fetch('https://dolarapi.com/v1/dolares/oficial')
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status))))
.then((j) => { if (alive) setRate(typeof j?.venta === 'number' ? j.venta : null) })
.catch(() => {})
return () => { alive = false }
}, [])
const fmtARS = (v: number) => v.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
const validUsd = Number.isInteger(usd) && usd >= 1
const arsText = rate && validUsd ? fmtARS(rate * usd) : '—'
const pdText = validUsd ? String(usd * 100) : '—'
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué son los métodos alternativos para Argentina?</span>
<p>Son métodos disponibles para Argentina para aquellos que no tienen acceso a PayPal.</p>
<p>Este método trabaja con Pesos Argentinos.</p>
<br />
<hr />
<br />
<span>Métodos disponibles</span>
<p>- Transferencia bancaria en pesos argentinos (ARS)</p>
<br />
<hr />
<br />
<span>¿Cuál es el valor de los PD?</span>
<p>La cantidad de Pesos según país será en base al cambio indicado en el momento de la transferencia.</p>
<p>El siguiente recuadro permite hacer una conversión de USD a Pesos argentinos mostrando el valor final y no habrá impuestos ni recargos.</p>
<p>Se puede cambiar los valores para ver el precio de la cantidad deseada de PD.</p>
<br />
<p className="centered">
<span>Calculadora de conversión</span>
<br /><br />
<input
type="number"
min="1"
step="1"
value={usd}
onChange={(e) => setUsd(parseInt(e.target.value, 10))}
style={{ width: '60px', textAlign: 'center' }}
/>{' '}
<span className="yellow-info">USD =</span>{' '}
<span id="ars-output">{arsText}</span> <span className="yellow-info">ARS =</span>{' '}
<span id="pd-output">{pdText}</span> <span className="yellow-info">PD</span>
</p>
<br />
<hr />
<br />
<span>¿Cómo adquirir PD por Transferencia?</span>
<p>Paso 1) Realiza la transferencia con los siguientes datos:</p>
<p>&nbsp;&nbsp;&nbsp;El valor en ARS puede obtenerse usando la calculadora disponible.</p>
<p>&nbsp;&nbsp;&nbsp;Ejemplo: Si deseas 200 PD, coloca 2 en la casilla USD y te dirá el monto en ARS</p>
<p>&nbsp;&nbsp;&nbsp;<span className="red-info2">Envía el monto exacto que indica la calculadora.</span></p>
<br />
<p>Paso 2) Envia el correo detallando:</p>
<p className="second-brown">&nbsp;- Nombre de cualquier PERSONAJE de la cuenta donde deseas recibir los PD.</p>
<p className="second-brown">&nbsp;- Cantidad de PD deseada.</p>
<p>Paso 3) Adjunta al correo una captura de pantalla (imagen, no pdf) del comprobante.</p>
<p>Paso 4) Envia el correo a la dirección indicada en <span>Contacto</span>.</p>
<p>Paso 5) Espera la respuesta al correo enviado.</p>
<p>Paso 6) Dentro del horario de atención, serás informado que has recibido los PD.</p>
<br />
<hr />
<br />
<span>Contacto</span>
<p>Correo: <a href="mailto:arg@ultimowow.com">arg@ultimowow.com</a></p>
<p>Lunes a Viernes por las tardes (Horario Argentina).</p>
<br /><br />
<p><i className="fas fa-exclamation-triangle"></i> Este método no es automático sino manual siendo realizado por la persona encargada.</p>
<p>Recomendamos no realizar ninguna acción sin comprobar primero si la persona encargada está disponible para entregar los PD.</p>
</div>
</div>
)
}
/* ----------------------------------- Chile ---------------------------------- */
function ChileTab() {
const rows: [string, string][] = [
['1.200 Pesos Chilenos', '100 PD'],
['3.600 Pesos Chilenos', '300 PD'],
['6.000 Pesos Chilenos', '500 PD'],
['9.600 Pesos Chilenos', '800 PD'],
['12.000 Pesos Chilenos', '1000 PD'],
['30.000 Pesos Chilenos', '2500 PD'],
['60.000 Pesos Chilenos', '5000 PD'],
]
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué son los métodos alternativos para Chile?</span>
<p>Son métodos disponibles para Chile para aquellos que no tienen acceso a PayPal.</p>
<p>Este método trabaja con Pesos Chilenos.</p>
<br />
<hr />
<br />
<span>Métodos disponibles</span>
<p>- Transferencia bancaria en pesos chilenos (CLP)</p>
<p>- Mach en pesos chilenos (CLP)</p>
<br />
<hr />
<br />
<span>¿Cuál es el valor de los PD?</span>
<table className="points-center-table">
<tbody>
<tr><th>Precio</th><th>PD</th></tr>
{rows.map(([p, pd]) => (<tr key={p}><td>{p}</td><td>{pd}</td></tr>))}
</tbody>
</table>
<br />
<hr />
<br />
<span>¿Cómo adquirir PD por Transferencia?</span>
<p>Paso 1) Contacta con la persona encargada mediante Discord. El medio de contacto aparece al final de esta sección.</p>
<p>Paso 2) Debes informarle:</p>
<p>- Nombre de cualquier personaje de la cuenta donde deseas recibir los PD.</p>
<p>- Cantidad de PD deseada.</p>
<p>Paso 3) La persona encargada te dará los datos necesarios para la transferencia.</p>
<p>Paso 4) Confirma con la persona encargada el costo final antes de realizar la transferencia.</p>
<p>Si usas el método Mach, se te entregará un código QR para realizar la transferencia.</p>
<p>Paso 5) Una vez realizada la transferencia, deberás enviarla como imagen (no como archivo pdf) a la persona encargada.</p>
<p>Paso 6) Serás informado que has recibido los PD.</p>
<br />
<hr />
<br />
<span>Contacto</span>
<p>- Discord al usuario <span className="blue-gm">Bhaal</span> (ID <span className="blue-gm">dakiron</span>)</p>
<br />
<button className="discord-button" onClick={() => window.open('https://discordapp.com/users/300063037958914048', '_blank')}><img className="discord-icon" src={`${LOGOS}/discord-logo.webp`} alt="Discord" /> Discord</button>
<br /><br />
<p>* Asegúrate que la persona que contactes tiene rango <span className="blue-gm">Maestro de Juego</span> en nuestro discord.</p>
<br />
<p><i className="fas fa-exclamation-triangle"></i> Este método no es automático sino manual siendo realizado por la persona encargada.</p>
<p>Recomendamos no realizar ninguna acción sin comprobar primero si la persona encargada está disponible para entregar los PD.</p>
</div>
</div>
)
}
/* --------------------------------- Colombia --------------------------------- */
function ColombiaTab() {
const rows: [string, string][] = [
['17.200 Pesos Colombianos', '300 PD'],
['23.000 Pesos Colombianos', '400 PD'],
['28.700 Pesos Colombianos', '500 PD'],
['34.400 Pesos Colombianos', '600 PD'],
['57.350 Pesos Colombianos', '1000 PD'],
['143.350 Pesos Colombianos', '2500 PD'],
]
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué son los métodos alternativos para Colombia?</span>
<p>Son métodos disponibles para Colombia para aquellos que no tienen acceso a PayPal que se lleva a cabo a través de un representante oficial, a través de diversos métodos de pago para un país en específico.</p>
<p>Este método trabaja con Pesos Colombianos.</p>
<br />
<hr />
<br />
<span>¿Cuál es el valor de los PD?</span>
<table className="points-center-table">
<tbody>
<tr><th>Precio</th><th>PD</th></tr>
{rows.map(([p, pd]) => (<tr key={p}><td>{p}</td><td>{pd}</td></tr>))}
</tbody>
</table>
<br />
<hr />
<br />
<span>¿Cómo adquiir PD por Representante?</span>
<p>Para realizar una adquisición y recibir tus PD, tienes varios medios disponibles:</p>
<p>- Recarga NEQUI en Efectivo</p>
<p>- Recarga NEQUI mediante PSE</p>
<p>- Transferencia mediante NEQUI</p>
<p>- Consignación o transferencia bancaria</p>
<br />
<fieldset>
<legend>Por Recarga NEQUI en Efectivo</legend><img className="bolivia-banks-icon" src={`${LOGOS}/nequi-logo.webp`} alt="Nequi" />
<div className="separate">
<p>1 - Ubica el punto más cercano a donde estés <a href="https://www.nequi.com.co/mapas/" target="_blank" rel="noopener noreferrer">aquí</a>.</p>
<p>2 - Al entrar te solicitará conocer tu ubicación y debes hacer clic al botón Permitir.</p>
<p>3 - Cuando la web te pregunte ¿Qué quieres hacer? Selecciona la opción Recargar.</p>
<p>4 - Mira las ubicaciones que te aparecen señalados en el mapa en color amarillo (Para ver la dirección exacta toca el punto que deseas ver en el mapa).</p>
<p>5 - Ve al establecimiento elegido e indica que quieres Recargar a NEQUI X Pesos donde X es la cantidad de pesos correspondiente a los PD deseados (Verifica la tabla y coloca el valor correctamente).</p>
<p>6 - Necesitarás dar la siguiente información:</p>
<br />
<ul>
<li>Número de convenio: <span>63703</span></li>
<li>Número de celular: <span>3133639892</span></li>
</ul>
<br />
<p>7 - Solicita recibo de la transacción.</p>
<p>8 - Escanea o toma una foto de dicho recibo y envíalo al representante junto con la cantidad de PD y el nombre del personaje donde deseas recibir los PD.</p>
<p>9 - Espera a que el encargado te envíe respuesta por el medio que le enviaste el mensaje donde te indicará que ya fueron cargados los PD.</p>
</div>
</fieldset>
<br />
<fieldset>
<legend>Por Recarga NEQUI mediante PSE</legend><img className="bolivia-banks-icon" src={`${LOGOS}/nequi-logo.webp`} alt="Nequi" />
<div className="separate">
<p>El recargar NEQUI por PSE es un método que permite enviar dinero de una cuenta bancaria (de casi cualquier banco) a NEQUI.</p>
<p>Nota: Este método puede implicar que tengas activo algunos servicios en el banco donde tienes tu cuenta para poder realizarlo.</p>
<br />
<p>1 - Ingresa a la web de <a href="https://www.nequi.com.co/" target="_blank" rel="noopener noreferrer">Nequi</a>.</p>
<p>2 - Haz clic en la opción Recarga.</p>
<p>3 - Aparecerá un formulario que solicitará la siguiente información:</p>
<br />
<ul>
<li>Número de celular: <span>3133639892</span></li>
<li>Confirma el número de celular: <span>3133639892</span></li>
<li>Tipo de persona: <span>Natural</span></li>
<li>Cantidad: <span>Cantidad correspondiente a los PD deseados (Verifica la tabla y coloca correctamente el valor)</span></li>
</ul>
<br />
<p>4 - Haz clic en No soy robot y de ahí a Continuar.</p>
<p>5 - Al momento de dar a Continuar serás redireccionado a la web oficial de PSE, donde deberás ingresar TU cuenta de PSE (si nunca antes haz hecho pagos por PSE deberás registrarte).</p>
<p>6 - Cuando ingreses la cuenta de PSE serás redireccionado a la web de tu banco que accede a este servicio donde deberás completar el proceso que tenga el banco de TU cuenta bancaria (puede que algunos bancos cobren adicional por el acceso o uso de este servicio).</p>
<p>7 - Una vez completado y finalizado el pago por PSE, serás re direccionado a NEQUI donde te mostrará un enlace con el mensaje Ver comprobante al cual debes hacerle clic.</p>
<p>8 - Descarga el comprobante de pago para generar un archivo en formato pdf y envíalo al representante junto con la cantidad de PD y el nombre del personaje donde deseas recibir los PD.</p>
<p>9 - Espera a que el encargado te envíe respuesta por el medio que le enviaste el mensaje donde te indicará que ya fueron cargados los PD.</p>
</div>
</fieldset>
<br />
<fieldset>
<legend>Transferencia mediante NEQUI</legend><img className="bolivia-banks-icon" src={`${LOGOS}/nequi-logo.webp`} alt="Nequi" />
<div className="separate">
<p>1 - Ingresa a tu cuenta NEQUI Desde la App móvil de NEQUI Colombia.</p>
<p>2 - Al ingresar, selecciona el botón con el símbolo $ de color rosa.</p>
<p>3 - Selecciona la opción Envía plata.</p>
<p>4 - Indica desde dónde enviarás el dinero.</p>
<p>5 - En los campos que están en Envía a NEQUI coloca la siguiente información:</p>
<br />
<ul>
<li>Número de celular: <span>3133639892</span></li>
<li>Cantidad: <span>Cantidad correspondiente a los PD deseados (Verifica la tabla y coloca correctamente el valor)</span></li>
<li>Mensaje: <span>PD UltimoWoW</span></li>
</ul>
<br />
<p>6 - Presiona Continuar.</p>
<p>7 - Una vez que se termine de procesar el pago te mostrará la lista de tus movimientos, en el último sale el nombre de la persona relacionada a la cuenta transferida que es ROBERT PENA.</p>
<p>8 - Selecciona dicho movimiento para que te muestre los detalles del mismo.</p>
<p>9 - Toma una captura de pantalla de los detalles y envíalo al representante junto con la cantidad de PD y el nombre del personaje donde deseas recibir los PD.</p>
<p>10 - Espera a que el encargado te envíe respuesta por el medio que le enviaste el mensaje donde te indicará que ya fueron cargados los PD.</p>
</div>
</fieldset>
<br />
<fieldset>
<legend>Consignación o Transferencia a cuenta bancaria</legend><img className="bolivia-banks-icon" src={`${LOGOS}/bancolombia-logo.webp`} alt="Bancolombia" />
<div className="separate">
<p>Mediante este método deberas realizar una consignación en un banco o corresponsal bancario por el valor de los PD que quieres adquirir (Verifica la tabla y coloca correctamente el valor).</p>
<br />
<p>1 - Cuando realices la consignación te solicitarán los siguientes datos:</p>
<br />
<ul>
<li>Número de cuenta: <span>203-133849-78</span></li>
<li>A nombre de: <span>Robert Camilo Peña</span></li>
<li>Tipo de cuenta: <span>De ahorros</span></li>
<li>Banco: <span>Bancolombia</span></li>
</ul>
<br />
<p>2 - Solicita copia del recibo de transacción.</p>
<p>3 - Escanea o toma una foto del recibo y envíalo al representante junto con la cantidad de PD y el nombre del personaje donde deseas recibir los PD.</p>
<p>4 - Espera a que el encargado te envíe respuesta por el medio que le enviaste el mensaje donde te indicará que ya fueron cargados los PD.</p>
</div>
</fieldset>
<br />
<hr />
<br />
<span>¿Cómo contactar con el Representante?</span>
<p>Nombre: Robert Peña</p>
<p>Usuario de Discord: <span className="blue-gm">Raziel#1403</span></p>
<p>Lunes a Jueves: 15:00 a 22:00 (Horario Colombia)</p>
<p>Sábados y Domingos: 18:00 a 23:00 (Horario Colombia)</p>
<p className="red-info2">Importante: Los días Viernes no hay atención</p>
<br />
<button className="discord-button" onClick={() => window.open('https://discordapp.com/users/1053037915065831425', '_blank')}><img className="discord-icon" src={`${LOGOS}/discord-logo.webp`} alt="Discord" /> Discord</button>
<button className="wp-button" onClick={() => window.open('https://wa.me/573195069649', '_blank')}><img className="wp-icon" src={`${LOGOS}/whatsapp-logo.webp`} alt="WhatsApp" /> Whatsapp</button>
<br />
</div>
</div>
)
}
/* ----------------------------------- Perú ----------------------------------- */
function PeruTab() {
const rows: [string, string][] = [
['5.5 Soles Peruanos', '100 PD'],
['25,5 Soles Peruanos', '500 PD'],
['40 Soles Peruanos', '800 PD'],
['48 Soles Peruanos', '1000 PD'],
['118 Soles Peruanos', '2500 PD'],
]
return (
<div className="box-content dnt-tab dnt-animate">
<div className="body-box-content">
<span>¿Qué son los métodos alternativos para Perú?</span>
<p>Son métodos disponibles para aquellos que no tienen acceso a PayPal que se lleva a cabo a través de un representante oficial, a través de diversos métodos de pago para un país en específico.</p>
<p>Este método trabaja con Soles Peruanos.</p>
<br />
<hr />
<br />
<span>¿Cuál es el valor de los PD?</span>
<table className="points-center-table">
<tbody>
<tr><th>Precio</th><th>PD</th></tr>
{rows.map(([p, pd]) => (<tr key={p}><td>{p}</td><td>{pd}</td></tr>))}
</tbody>
</table>
<br />
<hr />
<br />
<span>¿Cómo adquirir PD por Representante?</span>
<p>Para realizar una adquisición y recibir tus PD, tienes varios medios disponibles:</p>
<p>- Recarga por medio de YAPE</p>
<p>- Recarga por medio de PLIN</p>
<p>- Pago presencial mediante OFICINA (LIMA)</p>
<p>- Transferencia bancaria y interbancaria</p>
<br />
<fieldset>
<legend>Recarga por medio de YAPE</legend>
<div className="separate">
<img className="peru-logo" src={`${LOGOS}/yape-peru.png`} alt="Yape" />
</div>
</fieldset>
<br />
<fieldset>
<legend>Pago por medio de PLIN</legend>
<div className="separate">
<p>Pago mediante número.</p>
<br />
<img className="peru-logo" src={`${LOGOS}/plin-peru.webp`} alt="Plin" />
</div>
</fieldset>
<br />
<fieldset>
<legend>Pago presencial mediante OFICINA</legend>
<div className="separate">
<p>Contactarse con el número mediante 941841649 previamente cita.</p>
<br />
<img className="peru-logo" src={`${LOGOS}/oficina-peru.png`} alt="Oficina" />
</div>
</fieldset>
<br />
<fieldset>
<legend>Transferencia bancaria y interbancaria</legend>
<div className="separate">
<p>Mediante este método deberas realizar una consignación en un banco o corresponsal bancario por el valor de los PD que quieres adquirir (Verifica la tabla y coloca correctamente el valor).</p>
<br />
<p>- BANCO BCP</p>
<p>- BANCO BBVA</p>
<p>- BANCO SCOTIABANK</p>
<p>- BANCO DE LA NACIÓN</p>
<br />
<p>Comunicarse previamente con el representante.</p>
</div>
</fieldset>
<br />
<p>Una vez realizado el proceso por cualquiera de los medios indicados, contactar con el Representante.</p>
<p>Si es en los horarios de atención, los PD serán cargados en minutos en la cuenta.</p>
<br />
<hr />
<br />
<span>¿Cómo contactar con el Representante?</span>
<p>Nombre: Anibal Gavidia Correa</p>
<p>Lunes a Jueves: 9:00 a 22:00 (Horario Perú)</p>
<p>Sábados y Domingos: 9:00 a 23:00 (Horario Perú)</p>
<br />
<button className="discord-button" onClick={() => window.open('https://discordapp.com/users/642217524737605642', '_blank')}><img className="discord-icon" src={`${LOGOS}/discord-logo.webp`} alt="Discord" /> Discord</button>
<button className="wp-button" onClick={() => window.open('https://wa.me/51941841649', '_blank')}><img className="wp-icon" src={`${LOGOS}/whatsapp-logo.webp`} alt="WhatsApp" /> Contacto</button>
<br />
<AmountForm provider="sumup" sym={sym} buttonLabel="ADQUIRIR PD" />
</div>
</div>
)
+27
View File
@@ -0,0 +1,27 @@
import type { RowDataPacket } from 'mysql2'
import { db, DB } from './db'
/** Ratio de conversión: cada 1 unidad de moneda = 100 PD. */
export const PD_PER_UNIT = 100
/** Límites de compra por transacción (en unidades de moneda). */
export const DP_MIN_AMOUNT = 1
export const DP_MAX_AMOUNT = 200
/** Acredita PD (donate points) a una cuenta de juego en `home_api_points` (upsert). */
export async function creditDPoints(accountId: number, points: number): Promise<boolean> {
if (!accountId || !(points > 0)) return false
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id FROM home_api_points WHERE accountID = ?',
[accountId],
)
if (rows[0]) {
await db(DB.default).query('UPDATE home_api_points SET dp = dp + ? WHERE accountID = ?', [points, accountId])
} else {
await db(DB.default).query('INSERT INTO home_api_points (accountID, vp, dp) VALUES (?, 0, ?)', [accountId, points])
}
return true
} catch {
return false
}
}
+9
View File
@@ -1,5 +1,6 @@
import { executeSoapCommand } from './soap'
import { db, DB } from './db'
import { creditDPoints } from './dpoints'
import {
getRenamePrice,
getCustomizePrice,
@@ -78,6 +79,14 @@ export const PAID_SERVICES: Record<string, PaidServiceConfig> = {
fulfill: (c, m) => soapFulfill(`.char changeaccount ${m.destination_account} ${c}`),
extraFields: ['destination_account'],
},
// Compra de PD (Adquirir PD): acredita puntos a la cuenta que hizo el pago.
// El importe lo elige el usuario, por eso `price` lee metadata.amount.
dpoints: {
price: (m) => Promise.resolve(Number(m.amount)),
productName: (_c, m) => `${Number(m.points)} PD`,
fulfill: (_c, m) => creditDPoints(Number(m.accountId), Number(m.points)),
extraFields: [],
},
}
export function getPaidService(slug: string): PaidServiceConfig | null {
+108
View File
@@ -0,0 +1,108 @@
import type { RowDataPacket, ResultSetHeader } from 'mysql2'
import { db, DB } from './db'
import { creditDPoints, PD_PER_UNIT } from './dpoints'
const API = 'https://api.sumup.com/v0.1'
/** SumUp está disponible sólo si hay API key y merchant code configurados. */
export function sumupConfigured(): boolean {
return Boolean(process.env.SUMUP_API_KEY && process.env.SUMUP_MERCHANT_CODE)
}
function authHeaders(): Record<string, string> {
return {
Authorization: `Bearer ${process.env.SUMUP_API_KEY}`,
'Content-Type': 'application/json',
}
}
interface CreateParams {
accountId: number
username: string
email: string
acoreIp: string
amount: number // importe en la moneda del comercio
points: number
reference: string
description: string
returnUrl: string
}
/**
* Crea un checkout hospedado de SumUp y registra el pago en `home_stripelog`
* (reutilizamos la tabla de logs; `mode = 'sumup'`, `session_id = reference`).
* Devuelve la URL del checkout hospedado para redirigir al usuario.
*/
export async function createSumUpCheckout(p: CreateParams): Promise<{ success: boolean; url?: string; error?: string }> {
if (!sumupConfigured()) return { success: false, error: 'notConfigured' }
try {
const res = await fetch(`${API}/checkouts`, {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({
checkout_reference: p.reference,
amount: p.amount,
currency: process.env.SUMUP_CURRENCY || 'EUR',
merchant_code: process.env.SUMUP_MERCHANT_CODE,
description: p.description,
return_url: p.returnUrl,
hosted_checkout: { enabled: true },
}),
})
if (!res.ok) return { success: false, error: `sumup_${res.status}` }
const data: { id?: string; hosted_checkout_url?: string } = await res.json()
const url = data.hosted_checkout_url
if (!url) return { success: false, error: 'noHostedUrl' }
await db(DB.default).query(
'INSERT INTO home_stripelog (account_id, username, email, acore_ip, stripe_ip, product_name, amount, session_id, mode, character_name, timestamp, fulfilled) ' +
'VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, NOW(), 0)',
[p.accountId, p.username, p.email, p.acoreIp, p.description, p.amount, p.reference, 'sumup', p.username],
)
return { success: true, url }
} catch (e) {
return { success: false, error: String(e) }
}
}
/** ¿El checkout con esta referencia está PAGADO en SumUp? */
async function isReferencePaid(reference: string): Promise<boolean> {
try {
const res = await fetch(`${API}/checkouts?checkout_reference=${encodeURIComponent(reference)}`, {
headers: authHeaders(),
})
if (!res.ok) return false
const list: { status?: string }[] = await res.json()
return Array.isArray(list) && list.some((c) => c.status === 'PAID')
} catch {
return false
}
}
/**
* Entrega un checkout SumUp pagado: verifica el estado, reclama de forma atómica
* (fulfilled 0->1) y acredita los PD. Idempotente. Devuelve true si acreditó ahora.
*/
export async function fulfillSumUpCheckout(reference: string): Promise<{ ok: boolean; paid: boolean }> {
if (!reference) return { ok: false, paid: false }
const paid = await isReferencePaid(reference)
if (!paid) return { ok: false, paid: false }
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, account_id, amount FROM home_stripelog WHERE session_id = ? AND mode = ?',
[reference, 'sumup'],
)
const log = rows[0]
if (!log) return { ok: false, paid: true }
// Reclamo atómico: sólo gana quien pasa fulfilled 0->1.
const [res] = await db(DB.default).query<ResultSetHeader>(
'UPDATE home_stripelog SET fulfilled = 1 WHERE id = ? AND fulfilled = 0',
[log.id],
)
if (res.affectedRows === 0) return { ok: false, paid: true }
const points = Math.round(Number(log.amount) * PD_PER_UNIT)
const ok = await creditDPoints(Number(log.account_id), points)
return { ok, paid: true }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 46" role="img" aria-label="Stripe">
<rect x="0" y="0" width="150" height="46" rx="9" fill="#635BFF"/>
<text x="75" y="31" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-weight="700" font-size="24" letter-spacing="-1" fill="#ffffff">stripe</text>
</svg>

After

Width:  |  Height:  |  Size: 336 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Stripe">
<rect x="0" y="0" width="32" height="32" rx="7" fill="#635BFF"/>
<text x="16" y="23" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-weight="700" font-size="20" fill="#ffffff">S</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 170 46" role="img" aria-label="SumUp">
<rect x="0" y="0" width="46" height="46" rx="9" fill="#1E64FF"/>
<path d="M13 13 H33 V26 L26 33 H13 Z" fill="#ffffff"/>
<text x="105" y="31" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-weight="700" font-size="24" letter-spacing="-1" fill="#1E64FF">SumUp</text>
</svg>

After

Width:  |  Height:  |  Size: 391 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="SumUp">
<rect x="0" y="0" width="32" height="32" rx="7" fill="#1E64FF"/>
<path d="M9 9 H23 V18 L18 23 H9 Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 219 B