Fase 3: migra gold, transfer, revive y unstuck a islas
- 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>
This commit is contained in:
@@ -94,7 +94,7 @@ export function CharacterPurchaseForm({
|
||||
</select>
|
||||
<br />
|
||||
<button className={buttonClass} type="submit" disabled={busy || !character}>
|
||||
{busy ? 'Procesando…' : `${label} por ${price} €`}
|
||||
{busy ? 'Procesando…' : price ? `${label} por ${price} €` : label}
|
||||
</button>
|
||||
</form>
|
||||
<br />
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState } from 'react'
|
||||
import { redirectToStripeCheckout } from '../stripe'
|
||||
|
||||
interface GoldOption {
|
||||
gold_amount: number
|
||||
price: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
csrfToken: string
|
||||
characters: string[]
|
||||
options: GoldOption[]
|
||||
}
|
||||
|
||||
export function GoldForm({ url, csrfToken, characters, options }: Props) {
|
||||
const [character, setCharacter] = useState('')
|
||||
const [amount, setAmount] = 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('character', character)
|
||||
body.set('gold_amount', amount)
|
||||
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
|
||||
}
|
||||
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">
|
||||
<form onSubmit={handleSubmit} acceptCharset="utf-8">
|
||||
<select name="character" value={character} onChange={(e) => setCharacter(e.target.value)} required>
|
||||
<option value="" disabled>Seleccione un personaje</option>
|
||||
{characters.map((c) => (
|
||||
<option value={c} key={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
<br />
|
||||
<select name="gold_amount" value={amount} onChange={(e) => setAmount(e.target.value)} required>
|
||||
<option value="" disabled>Seleccione cantidad de oro</option>
|
||||
{options.map((o) => (
|
||||
<option value={o.gold_amount} key={o.gold_amount}>
|
||||
{o.gold_amount} oro - {o.price} EUR
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<br />
|
||||
<button type="submit" className="gold-button" disabled={busy || !character || !amount}>
|
||||
{busy ? 'Procesando…' : 'Enviar oro'}
|
||||
</button>
|
||||
</form>
|
||||
<hr />
|
||||
<center>
|
||||
<div className="alert-message" style={{ display: message ? 'block' : 'none' }}>
|
||||
{message && <span dangerouslySetInnerHTML={{ __html: message.html }} />}
|
||||
</div>
|
||||
</center>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState } from 'react'
|
||||
import { redirectToStripeCheckout } from '../stripe'
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
csrfToken: string
|
||||
characters: string[]
|
||||
price: string
|
||||
}
|
||||
|
||||
export function TransferForm({ url, csrfToken, characters, price }: Props) {
|
||||
const [character, setCharacter] = useState('')
|
||||
const [destination, setDestination] = useState('')
|
||||
const [token, setToken] = 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('character', character)
|
||||
body.set('destination_account', destination.trim())
|
||||
body.set('security_token', token.trim())
|
||||
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
|
||||
}
|
||||
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">
|
||||
<form onSubmit={handleSubmit} acceptCharset="utf-8">
|
||||
<select name="character" value={character} onChange={(e) => setCharacter(e.target.value)} required>
|
||||
<option value="" disabled>Seleccione un personaje</option>
|
||||
{characters.map((c) => (
|
||||
<option value={c} key={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
<br />
|
||||
<input
|
||||
type="text"
|
||||
name="destination_account"
|
||||
placeholder="Cuenta destino"
|
||||
required
|
||||
value={destination}
|
||||
onChange={(e) => setDestination(e.target.value)}
|
||||
/>
|
||||
<br />
|
||||
<input
|
||||
type="password"
|
||||
name="security_token"
|
||||
placeholder="Token de seguridad"
|
||||
required
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
<br />
|
||||
<button type="submit" className="transfer-button" disabled={busy || !character || !destination || !token}>
|
||||
{busy ? 'Procesando…' : `Transferir por ${price} €`}
|
||||
</button>
|
||||
</form>
|
||||
<hr />
|
||||
<center>
|
||||
<div id="transfer-character-response" className="alert-message" style={{ display: message ? 'block' : 'none' }}>
|
||||
{message && <span dangerouslySetInnerHTML={{ __html: message.html }} />}
|
||||
</div>
|
||||
</center>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { GoldForm } from '../components/GoldForm'
|
||||
|
||||
const el = document.getElementById('gold-app')
|
||||
if (el) {
|
||||
const cEl = document.getElementById('gold-characters-data')
|
||||
const oEl = document.getElementById('gold-options-data')
|
||||
const characters: string[] = cEl ? JSON.parse(cEl.textContent || '[]') : []
|
||||
const options = oEl ? JSON.parse(oEl.textContent || '[]') : []
|
||||
createRoot(el).render(
|
||||
<StrictMode>
|
||||
<GoldForm url={el.dataset.url || ''} csrfToken={el.dataset.csrf || ''} characters={characters} options={options} />
|
||||
</StrictMode>,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { TransferForm } from '../components/TransferForm'
|
||||
|
||||
const el = document.getElementById('transfer-app')
|
||||
if (el) {
|
||||
const cEl = document.getElementById('transfer-characters-data')
|
||||
const characters: string[] = cEl ? JSON.parse(cEl.textContent || '[]') : []
|
||||
createRoot(el).render(
|
||||
<StrictMode>
|
||||
<TransferForm
|
||||
url={el.dataset.url || ''}
|
||||
csrfToken={el.dataset.csrf || ''}
|
||||
characters={characters}
|
||||
price={el.dataset.price || ''}
|
||||
/>
|
||||
</StrictMode>,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user