f721aac7e5
Reutiliza la infraestructura Stripe (sin depender de SumUp/credenciales externas): - lib/battlepay.ts: getPendingOrders/getPendingOrder/markOrderPaid sobre acore_auth.battlepay_orders (PENDING->PAID, el mod-battlepay entrega en juego). - fulfill.ts: rama battlepay (metadata.battlepay_reference -> markOrderPaid), idempotente vía el claim atómico compartido con el webhook. - API POST /api/battlepay/pay (valida orden PENDING de la cuenta, crea checkout). - UI: /battlepay (lista + pagar) y /battlepay-success (entrega compartida), enlace "Tienda" en la cabecera para usuarios logueados. - i18n es/en: namespace Battlepay + Nav.store. Verificado: build OK, /battlepay 307->login, API 401 sin sesión, success 200. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.2 KiB
TypeScript
68 lines
2.2 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
import type { BattlepayOrder } from '@/lib/battlepay'
|
|
|
|
export function BattlepayList({ orders }: { orders: BattlepayOrder[] }) {
|
|
const t = useTranslations('Battlepay')
|
|
const [busy, setBusy] = useState<string | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
async function pay(reference: string) {
|
|
if (busy) return
|
|
setBusy(reference)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch('/api/battlepay/pay', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ reference }),
|
|
})
|
|
const d: { success?: boolean; url?: string; error?: string } = await res.json()
|
|
if (d.success && d.url) {
|
|
window.location.href = d.url
|
|
} else {
|
|
setError(t(d.error === 'orderNotFound' ? 'orderNotFound' : 'genericError'))
|
|
setBusy(null)
|
|
}
|
|
} catch {
|
|
setError(t('genericError'))
|
|
setBusy(null)
|
|
}
|
|
}
|
|
|
|
if (orders.length === 0) return <p className="text-amber-200/70">{t('noOrders')}</p>
|
|
|
|
return (
|
|
<div>
|
|
{error && <p className="mb-4 text-red-400">{error}</p>}
|
|
<ul className="space-y-3">
|
|
{orders.map((o) => (
|
|
<li key={o.id} className="flex flex-wrap items-center justify-between gap-3 nw-card">
|
|
<div>
|
|
<p className="font-semibold text-amber-300">{o.product_name}</p>
|
|
<p className="text-xs text-amber-200/50">
|
|
{t('reference')}: {o.reference}
|
|
{o.created_at ? ` · ${new Date(o.created_at * 1000).toLocaleDateString()}` : ''}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="font-semibold text-nw-gold-light">{o.price_eur.toFixed(2)} €</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => pay(o.reference)}
|
|
disabled={busy !== null}
|
|
className="nw-btn text-sm disabled:opacity-60"
|
|
>
|
|
{busy === o.reference ? t('redirecting') : t('pay')}
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|