feat: add AFC Store with MercadoPago purchases and prize redemption
Some checks failed
Deploy / deploy (push) Has been cancelled

Players can now buy AfterCoin with real money (MercadoPago Checkout Pro,
$15 MXN/AFC) and redeem AFC for gift cards or cash withdrawals. Admin
fulfills redemptions manually.

- Bridge: payments + redemptions tables, CRUD routes, PATCH auth
- Next.js API: verify-disk, balance, create-preference, webhook (idempotent
  minting with HMAC signature verification), redeem, payment/redemption history
- Frontend: hub, buy flow (4 packages + custom), redeem flow (gift cards +
  cash out), success/failure/pending pages, history with tabs, 8 components
- i18n: full English + Spanish translations
- Infra: nginx /api/afc/ → Next.js, docker-compose env vars, .env.example

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
consultoria-as
2026-02-26 02:26:13 +00:00
parent 7dc1d2e0e5
commit a76d513659
38 changed files with 2142 additions and 5 deletions

View File

@@ -0,0 +1,82 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
interface RedeemFormProps {
prizeType: string;
prizeDetail: string;
costAfc: number;
onSubmit: (deliveryInfo: string) => void;
onCancel: () => void;
loading: boolean;
}
export function RedeemForm({
prizeType,
prizeDetail,
costAfc,
onSubmit,
onCancel,
loading,
}: RedeemFormProps) {
const t = useTranslations("afc");
const [deliveryInfo, setDeliveryInfo] = useState("");
const isBankTransfer = prizeType === "bank_transfer";
const isMercadoPago = prizeType === "mercadopago";
const placeholder = isBankTransfer
? t("clabe_placeholder")
: isMercadoPago
? t("mp_account_placeholder")
: t("delivery_placeholder");
const label = isBankTransfer
? t("clabe_label")
: isMercadoPago
? t("mp_account_label")
: t("delivery_label");
return (
<div className="bg-gray-900 border border-white/10 rounded-2xl p-6 space-y-5">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-white">{prizeDetail}</h3>
<p className="text-sm text-amber-400">{costAfc} AFC</p>
</div>
<button
onClick={onCancel}
className="text-sm text-gray-500 hover:text-gray-300 transition-colors"
>
{t("cancel")}
</button>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-400">
{label}
</label>
<input
type="text"
value={deliveryInfo}
onChange={(e) => setDeliveryInfo(e.target.value)}
placeholder={placeholder}
className="w-full bg-gray-800 border border-white/10 rounded-xl px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-amber-500/50 focus:ring-1 focus:ring-amber-500/25 transition-all"
/>
</div>
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4 text-sm text-amber-300/80">
{t("redeem_warning")}
</div>
<button
onClick={() => onSubmit(deliveryInfo)}
disabled={loading || !deliveryInfo.trim()}
className="w-full py-3 bg-amber-500 hover:bg-amber-400 disabled:bg-gray-700 disabled:text-gray-500 text-black font-bold rounded-xl transition-colors"
>
{loading ? t("processing") : t("confirm_redeem")}
</button>
</div>
);
}