Tienda Battlepay: pago de órdenes pendientes vía Stripe
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>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -29,6 +29,9 @@ export async function Header() {
|
||||
</Link>
|
||||
{loggedIn ? (
|
||||
<>
|
||||
<Link href="/battlepay" className="text-amber-200 hover:text-amber-400">
|
||||
{t('store')}
|
||||
</Link>
|
||||
<Link href="/account" className="text-amber-200 hover:text-amber-400">
|
||||
{t('account')}
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user