From 6be04e4e373a34863a8f570a0c3d6f95524755f8 Mon Sep 17 00:00:00 2001 From: adevopg Date: Sun, 12 Jul 2026 21:42:35 +0000 Subject: [PATCH] Fase 3: migra gold, transfer, revive y unstuck a islas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../src/components/CharacterPurchaseForm.tsx | 2 +- frontend/src/components/GoldForm.tsx | 90 +++++++++++++++++ frontend/src/components/TransferForm.tsx | 96 +++++++++++++++++++ frontend/src/entries/gold_character.tsx | 16 ++++ frontend/src/entries/transfer_character.tsx | 19 ++++ frontend/vite.config.ts | 2 + home/templates/partials/gold_character.html | 26 ++--- home/templates/partials/revive_character.html | 26 ++--- .../partials/transfer_character.html | 23 ++--- .../templates/partials/unstuck_character.html | 23 ++--- home/views/characters.py | 13 ++- 11 files changed, 270 insertions(+), 66 deletions(-) create mode 100644 frontend/src/components/GoldForm.tsx create mode 100644 frontend/src/components/TransferForm.tsx create mode 100644 frontend/src/entries/gold_character.tsx create mode 100644 frontend/src/entries/transfer_character.tsx diff --git a/frontend/src/components/CharacterPurchaseForm.tsx b/frontend/src/components/CharacterPurchaseForm.tsx index 3c62d58..57e987f 100644 --- a/frontend/src/components/CharacterPurchaseForm.tsx +++ b/frontend/src/components/CharacterPurchaseForm.tsx @@ -94,7 +94,7 @@ export function CharacterPurchaseForm({

diff --git a/frontend/src/components/GoldForm.tsx b/frontend/src/components/GoldForm.tsx new file mode 100644 index 0000000..05d3f0d --- /dev/null +++ b/frontend/src/components/GoldForm.tsx @@ -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: 'Error al iniciar el pago. Inténtalo más tarde.' }) + setBusy(false) + } + } + + return ( +
+
+ +
+ +
+ +
+
+
+
+ {message && } +
+
+
+ ) +} diff --git a/frontend/src/components/TransferForm.tsx b/frontend/src/components/TransferForm.tsx new file mode 100644 index 0000000..c62d6a2 --- /dev/null +++ b/frontend/src/components/TransferForm.tsx @@ -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: 'Error al iniciar el pago. Inténtalo más tarde.' }) + setBusy(false) + } + } + + return ( +
+
+ +
+ setDestination(e.target.value)} + /> +
+ setToken(e.target.value)} + /> +
+ +
+
+
+
+ {message && } +
+
+
+ ) +} diff --git a/frontend/src/entries/gold_character.tsx b/frontend/src/entries/gold_character.tsx new file mode 100644 index 0000000..55c0415 --- /dev/null +++ b/frontend/src/entries/gold_character.tsx @@ -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( + + + , + ) +} diff --git a/frontend/src/entries/transfer_character.tsx b/frontend/src/entries/transfer_character.tsx new file mode 100644 index 0000000..5092a9f --- /dev/null +++ b/frontend/src/entries/transfer_character.tsx @@ -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( + + + , + ) +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 73bf2e3..5ba45c9 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -25,6 +25,8 @@ export default defineConfig({ security_token: resolve(__dirname, 'src/entries/security_token.tsx'), change_email: resolve(__dirname, 'src/entries/change_email.tsx'), character_purchase: resolve(__dirname, 'src/entries/character_purchase.tsx'), + gold_character: resolve(__dirname, 'src/entries/gold_character.tsx'), + transfer_character: resolve(__dirname, 'src/entries/transfer_character.tsx'), }, }, }, diff --git a/home/templates/partials/gold_character.html b/home/templates/partials/gold_character.html index 60b1b8b..19b9d2a 100644 --- a/home/templates/partials/gold_character.html +++ b/home/templates/partials/gold_character.html @@ -9,24 +9,14 @@

Selecciona un personaje y la cantidad de oro:

- - -
- - - -
-
+ {% load django_vite %} + +
+ {{ character_names|json_script:"gold-characters-data" }} + {{ gold_options|json_script:"gold-options-data" }} + {% vite_asset 'src/entries/gold_character.tsx' %}
diff --git a/home/templates/partials/revive_character.html b/home/templates/partials/revive_character.html index 50a6f96..f842970 100644 --- a/home/templates/partials/revive_character.html +++ b/home/templates/partials/revive_character.html @@ -15,24 +15,18 @@ {% if characters %}

Escoge el personaje que quieres revivir:


- -
- {% csrf_token %} - -
- -
-
+ {% load django_vite %} + +
+ {{ characters|json_script:"character-purchase-data" }} + {% vite_asset 'src/entries/character_purchase.tsx' %} {% else %} -

No hay personajes disponibles para revivir.

+

No tienes personajes para revivir.

{% endif %} -
-
diff --git a/home/templates/partials/transfer_character.html b/home/templates/partials/transfer_character.html index 00bbea4..7328d99 100644 --- a/home/templates/partials/transfer_character.html +++ b/home/templates/partials/transfer_character.html @@ -8,21 +8,14 @@
- - -
- - - - -
-
-
+ {% load django_vite %} + +
+ {{ character_names|json_script:"transfer-characters-data" }} + {% vite_asset 'src/entries/transfer_character.tsx' %}
diff --git a/home/templates/partials/unstuck_character.html b/home/templates/partials/unstuck_character.html index 04b0e79..4803513 100644 --- a/home/templates/partials/unstuck_character.html +++ b/home/templates/partials/unstuck_character.html @@ -22,20 +22,15 @@

Escoge el personaje que quieres desbloquear

- -
- {% csrf_token %} - -
- -
-
-
+ {% load django_vite %} + +
+ {{ characters|json_script:"character-purchase-data" }} + {% vite_asset 'src/entries/character_purchase.tsx' %}
diff --git a/home/views/characters.py b/home/views/characters.py index f29ecf6..b22365f 100644 --- a/home/views/characters.py +++ b/home/views/characters.py @@ -702,7 +702,12 @@ def gold_character_view(request): else: return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - return render(request, 'account/gold_character.html', {'characters': characters, 'gold_prices': gold_prices}) + return render(request, 'account/gold_character.html', { + 'characters': characters, + 'gold_prices': gold_prices, + 'character_names': list(characters.keys()), + 'gold_options': [{'gold_amount': p.gold_amount, 'price': str(p.price)} for p in gold_prices], + }) @require_paid_stripe() def gold_success_view(request): # Procesar el pago exitoso @@ -843,7 +848,11 @@ def transfer_character_view(request): else: return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - return render(request, 'account/transfer_character.html', {'characters': characters, 'price': price}) + return render(request, 'account/transfer_character.html', { + 'characters': characters, + 'price': price, + 'character_names': [c[0] for c in characters], + }) @require_paid_stripe() def transfer_success_view(request): transfer_data = request.session.get('transfer_data')