- {t('successTitle')}
+ {service ? t(`${service}.title`) : ''}
{okName ? (
- {t('success', { name: okName })}
+ {t(`${service}.success`, { name: okName })}
) : (
{t('error')}
)}
diff --git a/web-next/app/api/character/[service]/checkout/route.ts b/web-next/app/api/character/[service]/checkout/route.ts
new file mode 100644
index 0000000..4e14a37
--- /dev/null
+++ b/web-next/app/api/character/[service]/checkout/route.ts
@@ -0,0 +1,45 @@
+import { getSession } from '@/lib/session'
+import { getGameCharacters } from '@/lib/characters'
+import { createCheckoutSession } from '@/lib/stripe'
+import { getPaidService } from '@/lib/paid-services'
+
+export async function POST(request: Request, { params }: { params: Promise<{ service: string }> }) {
+ const { service } = await params
+ const cfg = getPaidService(service)
+ if (!cfg) return Response.json({ success: false, error: 'invalidService' }, { status: 404 })
+
+ const session = await getSession()
+ if (!session.accountId) return Response.json({ success: false, error: 'notAuthenticated' }, { status: 401 })
+
+ let character = ''
+ try {
+ const body = await request.json()
+ character = String(body.character ?? '')
+ } catch {
+ return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
+ }
+ if (!character) return Response.json({ success: false, error: 'invalidCharacter' })
+
+ const chars = await getGameCharacters(session.accountId)
+ if (!chars.find((c) => c.name === character)) {
+ return Response.json({ success: false, error: 'invalidCharacter' })
+ }
+
+ const price = await cfg.getPrice()
+ const site = process.env.SITE_URL || ''
+ const ip = (request.headers.get('x-forwarded-for') || '').split(',')[0].trim() || '0.0.0.0'
+
+ const r = await createCheckoutSession({
+ accountId: session.accountId,
+ username: session.username || '',
+ email: session.bnetEmail || '',
+ acoreIp: ip,
+ amount: price,
+ characterName: character,
+ productName: cfg.productName(character),
+ successUrl: `${site}/service-success?service=${service}`,
+ cancelUrl: `${site}/${service}`,
+ })
+ if (!r.success || !r.url) return Response.json({ success: false, error: 'stripeError' })
+ return Response.json({ success: true, url: r.url })
+}
diff --git a/web-next/components/ServicePageContent.tsx b/web-next/components/ServicePageContent.tsx
new file mode 100644
index 0000000..b2a857b
--- /dev/null
+++ b/web-next/components/ServicePageContent.tsx
@@ -0,0 +1,32 @@
+import { notFound } from 'next/navigation'
+import { getTranslations, setRequestLocale } from 'next-intl/server'
+import { redirect } from '@/i18n/navigation'
+import { getSession } from '@/lib/session'
+import { getGameCharacters } from '@/lib/characters'
+import { getPaidService } from '@/lib/paid-services'
+import { PaidServiceForm } from '@/components/PaidServiceForm'
+
+/** Contenido común de una página de servicio de pago (guardas + form). */
+export async function ServicePageContent({ service, locale }: { service: string; locale: string }) {
+ setRequestLocale(locale)
+ const cfg = getPaidService(service)
+ if (!cfg) notFound()
+
+ const t = await getTranslations('Paid')
+ const session = await getSession()
+ if (!session.bnetId) redirect({ href: '/login', locale })
+ if (!session.username) redirect({ href: '/select-account', locale })
+
+ const [chars, price] = await Promise.all([getGameCharacters(session.accountId!), cfg!.getPrice()])
+
+ return (
+
+ {t(`${service}.title`)}
+ c.name)}
+ checkoutEndpoint={`/api/character/${service}/checkout`}
+ payLabel={t(`${service}.pay`, { price })}
+ />
+
+ )
+}
diff --git a/web-next/lib/paid-services.ts b/web-next/lib/paid-services.ts
new file mode 100644
index 0000000..9589e02
--- /dev/null
+++ b/web-next/lib/paid-services.ts
@@ -0,0 +1,47 @@
+import {
+ getRenamePrice,
+ getCustomizePrice,
+ getChangeRacePrice,
+ getChangeFactionPrice,
+ getLevelUpPrice,
+} from './prices'
+
+export interface PaidServiceConfig {
+ command: (character: string) => string
+ getPrice: () => Promise
+ productName: (character: string) => string
+}
+
+// Servicios de personaje de pago. slug = ruta (/rename, /customize, ...) y clave de
+// catálogo (Paid.). Comandos SOAP tal cual las success views de Django.
+export const PAID_SERVICES: Record = {
+ rename: {
+ command: (c) => `.char rename ${c}`,
+ getPrice: getRenamePrice,
+ productName: (c) => `Renombrar personaje: ${c}`,
+ },
+ customize: {
+ command: (c) => `.char customi ${c}`,
+ getPrice: getCustomizePrice,
+ productName: (c) => `Personalizar personaje: ${c}`,
+ },
+ 'change-race': {
+ command: (c) => `.char changerace ${c}`,
+ getPrice: getChangeRacePrice,
+ productName: (c) => `Cambiar raza: ${c}`,
+ },
+ 'change-faction': {
+ command: (c) => `.char changef ${c}`,
+ getPrice: getChangeFactionPrice,
+ productName: (c) => `Cambiar facción: ${c}`,
+ },
+ 'level-up': {
+ command: (c) => `.char level ${c} 80`,
+ getPrice: getLevelUpPrice,
+ productName: (c) => `Subir a nivel 80: ${c}`,
+ },
+}
+
+export function getPaidService(slug: string): PaidServiceConfig | null {
+ return PAID_SERVICES[slug] ?? null
+}
diff --git a/web-next/messages/en.json b/web-next/messages/en.json
index 4404216..4a1d35b 100644
--- a/web-next/messages/en.json
+++ b/web-next/messages/en.json
@@ -136,13 +136,32 @@
"soapError": "Server communication error. Please try again later.",
"genericError": "Something went wrong. Please try again later."
},
- "Rename": {
- "title": "Rename character",
- "pay": "Rename for {price} €",
- "successTitle": "Rename character",
- "success": "Character {name} has been flagged for rename. The change will apply on next login.",
- "error": "The operation could not be completed. If you were charged, contact support.",
- "invalidCharacter": "Select a valid character.",
- "stripeError": "Could not start the payment. Please try again later."
+ "Paid": {
+ "rename": {
+ "title": "Rename character",
+ "pay": "Rename for {price} €",
+ "success": "Character {name} has been flagged for rename (applies on next login)."
+ },
+ "customize": {
+ "title": "Customize character",
+ "pay": "Customize for {price} €",
+ "success": "Character {name} has been flagged for customization."
+ },
+ "change-race": {
+ "title": "Change race",
+ "pay": "Change race for {price} €",
+ "success": "Character {name} has been flagged for race change."
+ },
+ "change-faction": {
+ "title": "Change faction",
+ "pay": "Change faction for {price} €",
+ "success": "Character {name} has changed faction successfully."
+ },
+ "level-up": {
+ "title": "Level up to 80",
+ "pay": "Level up to 80 for {price} €",
+ "success": "Character {name} has been leveled to 80."
+ },
+ "error": "The operation could not be completed. If you were charged, contact support."
}
}
diff --git a/web-next/messages/es.json b/web-next/messages/es.json
index 7825f9b..2076e93 100644
--- a/web-next/messages/es.json
+++ b/web-next/messages/es.json
@@ -136,13 +136,32 @@
"soapError": "Error de comunicación con el servidor. Inténtalo más tarde.",
"genericError": "Algo ha salido mal. Inténtalo más tarde."
},
- "Rename": {
- "title": "Renombrar personaje",
- "pay": "Renombrar por {price} €",
- "successTitle": "Renombrar personaje",
- "success": "El personaje {name} ha sido marcado para renombrar. El cambio se aplicará en el próximo inicio de sesión.",
- "error": "No se pudo completar la operación. Si se te ha cobrado, contacta con soporte.",
- "invalidCharacter": "Selecciona un personaje válido.",
- "stripeError": "No se pudo iniciar el pago. Inténtalo más tarde."
+ "Paid": {
+ "rename": {
+ "title": "Renombrar personaje",
+ "pay": "Renombrar por {price} €",
+ "success": "El personaje {name} ha sido marcado para renombrar (se aplica al próximo inicio de sesión)."
+ },
+ "customize": {
+ "title": "Personalizar personaje",
+ "pay": "Personalizar por {price} €",
+ "success": "El personaje {name} ha sido marcado para personalización."
+ },
+ "change-race": {
+ "title": "Cambiar de raza",
+ "pay": "Cambiar raza por {price} €",
+ "success": "El personaje {name} ha sido marcado para cambio de raza."
+ },
+ "change-faction": {
+ "title": "Cambiar de facción",
+ "pay": "Cambiar facción por {price} €",
+ "success": "El personaje {name} ha cambiado de facción con éxito."
+ },
+ "level-up": {
+ "title": "Subir a nivel 80",
+ "pay": "Subir a nivel 80 por {price} €",
+ "success": "El personaje {name} ha sido subido al nivel 80."
+ },
+ "error": "No se pudo completar la operación. Si se te ha cobrado, contacta con soporte."
}
}