6be04e4e37
- revive/unstuck (acciones SOAP gratuitas): reutilizan CharacterPurchaseForm; se hace el precio opcional (botón sin "por X €" cuando no hay precio). El componente ya maneja la respuesta sin session_id mostrando el mensaje. - gold: variante GoldForm (personaje + cantidad de oro -> Stripe). La vista pasa ahora character_names y gold_options (serializables) por json_script. - transfer: variante TransferForm (personaje + cuenta destino + token -> Stripe). La vista pasa character_names por json_script. Verificado: check OK, plantillas renderizan las islas, las 4 páginas redirigen a login sin sesión. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
3.2 KiB
TypeScript
111 lines
3.2 KiB
TypeScript
import { useState } from 'react'
|
|
import { redirectToStripeCheckout } from '../stripe'
|
|
|
|
interface Props {
|
|
url: string
|
|
csrfToken: string
|
|
characters: string[]
|
|
price: string
|
|
label: string
|
|
/** nombre del campo POST del personaje (por defecto "character") */
|
|
field?: string
|
|
/** clase CSS del botón (por defecto "rename-button") */
|
|
buttonClass?: string
|
|
}
|
|
|
|
/**
|
|
* Formulario reutilizable para los servicios de personaje de pago: selector de
|
|
* personaje + botón con precio. Al enviar, la vista devuelve un session_id de
|
|
* Stripe y redirige al Checkout. Sirve para rename/customize/race/faction/level/
|
|
* gold/transfer/... cambiando solo los data-* de la plantilla.
|
|
*/
|
|
export function CharacterPurchaseForm({
|
|
url,
|
|
csrfToken,
|
|
characters,
|
|
price,
|
|
label,
|
|
field = 'character',
|
|
buttonClass = 'rename-button',
|
|
}: Props) {
|
|
const [character, setCharacter] = useState('')
|
|
const [busy, setBusy] = useState(false)
|
|
const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null)
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault()
|
|
if (busy) return
|
|
setBusy(true)
|
|
setMessage(null)
|
|
|
|
const body = new URLSearchParams()
|
|
body.set(field, character)
|
|
body.set('csrfmiddlewaretoken', csrfToken)
|
|
|
|
try {
|
|
const resp = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRFToken': csrfToken,
|
|
Accept: 'application/json',
|
|
},
|
|
credentials: 'same-origin',
|
|
body: body.toString(),
|
|
})
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
const data: {
|
|
success?: boolean
|
|
message?: string
|
|
session_id?: string
|
|
stripe_public_key?: string
|
|
} = await resp.json()
|
|
|
|
if (data.success && data.session_id && data.stripe_public_key) {
|
|
await redirectToStripeCheckout(data.stripe_public_key, data.session_id)
|
|
return // redirigiendo a Stripe
|
|
}
|
|
setMessage({ ok: !!data.success, html: data.message || '' })
|
|
setBusy(false)
|
|
} catch {
|
|
setMessage({
|
|
ok: false,
|
|
html: '<span class="red-form-response">Error al iniciar el pago. Inténtalo más tarde.</span>',
|
|
})
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="centered">
|
|
<br />
|
|
<p>Escoge el personaje</p>
|
|
<br />
|
|
<form onSubmit={handleSubmit} acceptCharset="utf-8">
|
|
<select name={field} value={character} onChange={(e) => setCharacter(e.target.value)} required>
|
|
<option value="" disabled>
|
|
Seleccione un personaje
|
|
</option>
|
|
{characters.map((c) => (
|
|
<option className="priest big-font" value={c} key={c}>
|
|
{c}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<br />
|
|
<button className={buttonClass} type="submit" disabled={busy || !character}>
|
|
{busy ? 'Procesando…' : price ? `${label} por ${price} €` : label}
|
|
</button>
|
|
</form>
|
|
<br />
|
|
<hr />
|
|
<center>
|
|
<div className="alert-message" style={{ display: message ? 'block' : 'none' }}>
|
|
{message && <span dangerouslySetInnerHTML={{ __html: message.html }} />}
|
|
</div>
|
|
</center>
|
|
<br />
|
|
</div>
|
|
)
|
|
}
|