Files
dark-lord/app/account/add-funds/page.tsx
drjones 4354416f1c Integrate BTCPay Server: auto-settling invoice deposits
- lib/btcpay.ts: Greenfield API client (createBtcPayInvoice, getBtcPayInvoiceStatus)
- /api/btcpay/invoice: creates per-deposit invoice (USD amount + handle in metadata)
- /api/btcpay/status/[id]: polls invoice status + returns BTC address/amount
- add-funds page: BTCPay tab with amount picker, live address display,
  10s auto-poll, settlement auto-credits localStorage ledger
- WalletContext: creditBtcPayInvoice() with duplicate-invoice guard
- .env.example: BTCPAY_URL, BTCPAY_API_KEY, BTCPAY_STORE_ID, NEXT_PUBLIC_BTCPAY_ENABLED

Made-with: Cursor
2026-04-16 01:23:35 -07:00

730 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { FormEvent, useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Navbar from "@/components/Navbar";
import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
// ─── Types ─────────────────────────────────────────────────────────────────
type Tab = "BTCPAY" | "BTC" | "ETH" | "XMR";
type BtcPayInvoiceLocal = {
invoiceId: string;
handle: string;
usdAmount: number;
btcAddress: string | null;
btcAmount: string | null;
btcRate: string | null;
status: "New" | "Processing" | "Expired" | "Invalid" | "Settled";
createdAt: number;
credited: boolean;
};
type ManualDeposit = {
id: string;
coin: "ETH" | "XMR";
txid: string;
ts: number;
status: "pending" | "credited";
ref?: string;
};
// ─── localStorage helpers ───────────────────────────────────────────────────
const INVOICE_KEY = "cyberlux-btcpay-invoices-v1";
const MANUAL_KEY = "cyberlux-pending-deposits-v1";
function loadInvoices(): BtcPayInvoiceLocal[] {
if (typeof window === "undefined") return [];
try { return JSON.parse(localStorage.getItem(INVOICE_KEY) ?? "[]") as BtcPayInvoiceLocal[]; }
catch { return []; }
}
function saveInvoices(d: BtcPayInvoiceLocal[]) {
if (typeof window !== "undefined") localStorage.setItem(INVOICE_KEY, JSON.stringify(d));
}
function loadManual(): ManualDeposit[] {
if (typeof window === "undefined") return [];
try { return JSON.parse(localStorage.getItem(MANUAL_KEY) ?? "[]") as ManualDeposit[]; }
catch { return []; }
}
function saveManual(d: ManualDeposit[]) {
if (typeof window !== "undefined") localStorage.setItem(MANUAL_KEY, JSON.stringify(d));
}
function shortId() { return Math.random().toString(36).slice(2, 10).toUpperCase(); }
// ─── Style maps ────────────────────────────────────────────────────────────
const TAB_COLOR: Record<Tab, string> = {
BTCPAY: "text-neon-purple",
BTC: "text-amber-400",
ETH: "text-blue-400",
XMR: "text-orange-400",
};
const TAB_BORDER: Record<Tab, string> = {
BTCPAY: "border-neon-purple/30",
BTC: "border-amber-500/30",
ETH: "border-blue-500/30",
XMR: "border-orange-500/30",
};
const TAB_BG: Record<Tab, string> = {
BTCPAY: "bg-neon-purple/10",
BTC: "bg-amber-500/10",
ETH: "bg-blue-500/10",
XMR: "bg-orange-500/10",
};
// ─── Component ─────────────────────────────────────────────────────────────
export default function AddFundsPage() {
const router = useRouter();
const { user, hydrated } = useAccount();
const { usdStoreCredit, luxCredits, verifyBtcDeposit, creditBtcPayInvoice } = useWallet();
const btcPayEnabled = (process.env.NEXT_PUBLIC_BTCPAY_ENABLED ?? "").toLowerCase() === "true";
const [activeTab, setActiveTab] = useState<Tab>(btcPayEnabled ? "BTCPAY" : "BTC");
const [copied, setCopied] = useState<Tab | null>(null);
// ── BTCPay state ──────────────────────────────────────────────────────────
const [desiredUsd, setDesiredUsd] = useState("25");
const [bpBusy, setBpBusy] = useState(false);
const [bpError, setBpError] = useState<string | null>(null);
const [activeInvoice, setActiveInvoice] = useState<BtcPayInvoiceLocal | null>(null);
const [invoiceHistory, setInvoiceHistory] = useState<BtcPayInvoiceLocal[]>([]);
const [bpSuccess, setBpSuccess] = useState<string | null>(null);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
// ── Manual BTC verify state ───────────────────────────────────────────────
const [btcTxid, setBtcTxid] = useState("");
const [btcBusy, setBtcBusy] = useState(false);
const [btcMsg, setBtcMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
// ── ETH / XMR manual state ────────────────────────────────────────────────
const [manualTxid, setManualTxid] = useState("");
const [manualBusy, setManualBusy] = useState(false);
const [manualMsg, setManualMsg] = useState<{ kind: "ok" | "err" | "pending"; text: string; ref?: string } | null>(null);
const [manualHistory, setManualHistory] = useState<ManualDeposit[]>([]);
const btcAddr = getMerchantBtcAddress();
const btcReady = isMerchantBtcConfigured();
const ethAddr = (process.env.NEXT_PUBLIC_ETH_ADDRESS || "").trim();
const xmrAddr = (process.env.NEXT_PUBLIC_XMR_ADDRESS || "").trim();
// ── Auth guard ────────────────────────────────────────────────────────────
useEffect(() => {
if (hydrated && !user) router.replace("/sign-in?next=/account/add-funds");
}, [hydrated, user, router]);
// ── Load local state ──────────────────────────────────────────────────────
useEffect(() => {
setInvoiceHistory(loadInvoices());
setManualHistory(loadManual());
}, []);
// ── Poll active BTCPay invoice ────────────────────────────────────────────
const stopPoll = useCallback(() => {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
}, []);
const pollInvoice = useCallback(
async (invoice: BtcPayInvoiceLocal) => {
if (invoice.credited || invoice.status === "Settled" || invoice.status === "Expired" || invoice.status === "Invalid") {
stopPoll();
return;
}
try {
const res = await fetch(`/api/btcpay/status/${invoice.invoiceId}`);
const data = (await res.json()) as {
ok: boolean;
status?: string;
usdAmount?: number;
btcAddress?: string | null;
btcAmount?: string | null;
btcRate?: string | null;
settled?: boolean;
error?: string;
};
if (!data.ok) return;
const newStatus = (data.status ?? invoice.status) as BtcPayInvoiceLocal["status"];
// Update address/amount if BTCPay just returned them (first poll after creation)
const updated: BtcPayInvoiceLocal = {
...invoice,
status: newStatus,
btcAddress: data.btcAddress ?? invoice.btcAddress,
btcAmount: data.btcAmount ?? invoice.btcAmount,
btcRate: data.btcRate ?? invoice.btcRate,
credited: invoice.credited,
};
if (data.settled && !invoice.credited && user) {
const result = creditBtcPayInvoice(invoice.invoiceId, invoice.usdAmount);
if (result.ok) {
updated.credited = true;
setBpSuccess(`$${invoice.usdAmount.toFixed(2)} USD credited to @${user.username}!`);
}
stopPoll();
}
if (newStatus === "Expired" || newStatus === "Invalid") stopPoll();
setActiveInvoice(updated);
setInvoiceHistory((prev) => {
const next = prev.map((inv) => inv.invoiceId === updated.invoiceId ? updated : inv);
if (!next.find((i) => i.invoiceId === updated.invoiceId)) next.unshift(updated);
saveInvoices(next);
return next;
});
} catch { /* ignore network blips */ }
},
[user, creditBtcPayInvoice, stopPoll],
);
useEffect(() => {
if (!activeInvoice) { stopPoll(); return; }
if (activeInvoice.credited || activeInvoice.status === "Settled" || activeInvoice.status === "Expired") return;
stopPoll();
pollRef.current = setInterval(() => { void pollInvoice(activeInvoice); }, 10_000);
// also poll immediately to fetch btcAddress on first render
void pollInvoice(activeInvoice);
return stopPoll;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeInvoice?.invoiceId]);
useEffect(() => { return stopPoll; }, [stopPoll]);
// ── Handlers ──────────────────────────────────────────────────────────────
const createInvoice = async (e: FormEvent) => {
e.preventDefault();
if (!user) return;
const usd = parseFloat(desiredUsd);
if (!Number.isFinite(usd) || usd < 0.5) { setBpError("Enter an amount ≥ $0.50"); return; }
setBpBusy(true);
setBpError(null);
setBpSuccess(null);
try {
const res = await fetch("/api/btcpay/invoice", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ usdAmount: usd, handle: user.username }),
});
const data = (await res.json()) as {
ok: boolean;
invoiceId?: string;
checkoutLink?: string;
status?: string;
expiresAt?: number;
error?: string;
};
if (!data.ok || !data.invoiceId) {
setBpError(data.error ?? "Failed to create invoice. Check BTCPay config.");
return;
}
const newInv: BtcPayInvoiceLocal = {
invoiceId: data.invoiceId,
handle: user.username,
usdAmount: usd,
btcAddress: null,
btcAmount: null,
btcRate: null,
status: (data.status ?? "New") as BtcPayInvoiceLocal["status"],
createdAt: Date.now(),
credited: false,
};
setActiveInvoice(newInv);
setInvoiceHistory((prev) => { const n = [newInv, ...prev]; saveInvoices(n); return n; });
} catch {
setBpError("Network error. Is BTCPay reachable?");
} finally {
setBpBusy(false);
}
};
const copyText = async (text: string, tab: Tab) => {
try { await navigator.clipboard.writeText(text); setCopied(tab); setTimeout(() => setCopied(null), 1800); }
catch { /* ignore */ }
};
const onVerifyBtc = async (e: FormEvent) => {
e.preventDefault();
if (!btcTxid.trim()) return;
setBtcBusy(true); setBtcMsg(null);
try {
const res = await verifyBtcDeposit(btcTxid.trim());
if (res.ok) {
setBtcMsg({ kind: "ok", text: `$${(res.creditedUsd ?? 0).toFixed(2)} USD credited to @${user!.username}.` });
setBtcTxid("");
} else {
setBtcMsg({ kind: "err", text: res.error ?? "Verification failed." });
}
} finally { setBtcBusy(false); }
};
const onSubmitManual = async (e: FormEvent) => {
e.preventDefault();
if (!manualTxid.trim() || !user || (activeTab !== "ETH" && activeTab !== "XMR")) return;
setManualBusy(true); setManualMsg(null);
try {
const res = await fetch("/api/pending-deposit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ coin: activeTab, txid: manualTxid.trim(), handle: user.username }),
});
const json = (await res.json()) as { ok: boolean; message?: string; ref?: string; error?: string };
if (json.ok) {
const entry: ManualDeposit = {
id: shortId(), coin: activeTab as "ETH" | "XMR",
txid: manualTxid.trim(), ts: Date.now(), status: "pending", ref: json.ref,
};
setManualHistory((prev) => { const n = [entry, ...prev]; saveManual(n); return n; });
setManualMsg({ kind: "pending", text: json.message ?? "Submitted for manual review.", ref: json.ref });
setManualTxid("");
} else {
setManualMsg({ kind: "err", text: json.error ?? "Submission failed." });
}
} catch {
setManualMsg({ kind: "err", text: "Network error." });
} finally { setManualBusy(false); }
};
// ── Loading state ─────────────────────────────────────────────────────────
if (!hydrated || !user) {
return (
<div className="flex min-h-screen items-center justify-center bg-[#0a0a0a] font-mono text-sm text-zinc-500">
Loading
</div>
);
}
const tabs: Tab[] = btcPayEnabled ? ["BTCPAY", "BTC", "ETH", "XMR"] : ["BTC", "ETH", "XMR"];
return (
<div className="min-h-screen bg-[#0a0a0a] text-zinc-200">
<Navbar />
<div className="mx-auto max-w-3xl px-4 py-28">
{/* Header */}
<div className="mb-10">
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">
@{user.username} · deposit desk
</p>
<h1 className="mt-2 font-orbitron text-4xl font-bold">Fund Your Account</h1>
<p className="mt-3 max-w-2xl text-sm text-zinc-400 leading-relaxed">
Send crypto to the address below. We verify the transaction on-chain, convert to USD at current
market rate, and credit your account.{" "}
<strong className="text-zinc-200">You no longer hold the crypto</strong> we do.
When you purchase, we pay the vendor in crypto on your behalf.
</p>
</div>
{/* How it works */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-3">
{[
{ n: "1", icon: "₿", title: "Send crypto", body: "BTC via BTCPay, or BTC/ETH/XMR to a static address." },
{ n: "2", icon: "📊", title: "We verify", body: "On-chain confirmation. Credited at live market USD rate." },
{ n: "3", icon: "🛒", title: "You shop", body: "Spend USD credits site-wide. We pay vendors in crypto." },
].map((s) => (
<div key={s.n} className="glass rounded-2xl border border-white/10 p-5">
<div className="mb-3 flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-neon-cyan/15 text-sm font-bold text-neon-cyan">
{s.n}
</div>
<span className="text-xl">{s.icon}</span>
</div>
<div className="font-bold text-zinc-100">{s.title}</div>
<p className="mt-1 text-xs text-zinc-500">{s.body}</p>
</div>
))}
</div>
{/* Balance card */}
<div className="glass mb-8 rounded-2xl border border-neon-cyan/20 p-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<div className="text-xs text-zinc-500">Current balance @{user.username}</div>
<div className="mt-1 font-orbitron text-3xl text-neon-cyan">${usdStoreCredit.toFixed(2)} USD</div>
<div className="mt-0.5 text-sm text-neon-green">{luxCredits.toLocaleString()} LUX</div>
</div>
<div className="flex gap-3">
<Link href="/checkout" className="rounded-lg border border-neon-cyan/30 px-4 py-2 text-sm text-neon-cyan hover:bg-neon-cyan/10">
Spend
</Link>
<Link href="/wallets" className="rounded-lg border border-white/10 px-4 py-2 text-sm text-zinc-400 hover:border-white/20">
Passes
</Link>
</div>
</div>
</div>
{/* Tab selector */}
<div className="mb-6 flex gap-2 flex-wrap">
{tabs.map((tab) => (
<button
key={tab}
type="button"
onClick={() => { setActiveTab(tab); setBpError(null); setBtcMsg(null); setManualMsg(null); }}
className={`flex-1 min-w-[80px] rounded-xl border py-3 text-xs font-bold uppercase transition-all ${
activeTab === tab
? `${TAB_BORDER[tab]} ${TAB_BG[tab]} ${TAB_COLOR[tab]}`
: "border-white/10 text-zinc-500 hover:border-white/20"
}`}
>
{tab === "BTCPAY" ? "BTCPay ✦" : tab}
</button>
))}
</div>
{/* ── BTCPAY TAB ─────────────────────────────────────────────────── */}
{activeTab === "BTCPAY" && (
<div className="space-y-5">
<div className="glass rounded-2xl border border-neon-purple/30 p-6">
<div className="mb-1 flex items-center justify-between">
<h2 className="font-orbitron text-lg text-neon-purple">BTCPay Invoice</h2>
<span className="rounded-full bg-neon-green/15 px-3 py-1 text-xs font-bold text-neon-green">
Auto-settle
</span>
</div>
<p className="mb-5 text-xs text-zinc-500">
Generates a unique Bitcoin address per deposit. BTCPay monitors the chain your account is
credited automatically the moment the payment confirms. No txid entry needed.
</p>
{!activeInvoice || activeInvoice.status === "Expired" || activeInvoice.status === "Invalid" ? (
<form onSubmit={(e) => void createInvoice(e)} className="space-y-4">
<div>
<label className="mb-2 block text-xs text-zinc-500 uppercase tracking-wider">
Amount to deposit (USD)
</label>
<div className="flex gap-2">
{["10", "25", "50", "100", "250"].map((v) => (
<button
key={v}
type="button"
onClick={() => setDesiredUsd(v)}
className={`rounded-lg border px-3 py-1.5 text-xs font-bold transition-all ${
desiredUsd === v
? "border-neon-purple/50 bg-neon-purple/15 text-neon-purple"
: "border-white/10 text-zinc-500 hover:border-white/20"
}`}
>
${v}
</button>
))}
</div>
<input
type="number"
min="0.50"
step="0.01"
value={desiredUsd}
onChange={(e) => setDesiredUsd(e.target.value)}
className="mt-3 w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-sm placeholder-zinc-600 focus:border-neon-purple/40 focus:outline-none"
placeholder="Custom amount"
/>
</div>
<button
type="submit"
disabled={bpBusy}
className="w-full rounded-xl bg-gradient-to-r from-neon-purple to-neon-pink py-3 text-sm font-bold text-white disabled:opacity-40"
>
{bpBusy ? "Creating invoice…" : "Generate Payment Invoice →"}
</button>
{bpError && (
<div className="rounded-xl border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-400">
{bpError}
</div>
)}
</form>
) : (
/* Active invoice */
<div className="space-y-4">
{/* Status badge */}
<div className="flex items-center justify-between">
<span className={`rounded-full px-3 py-1 text-xs font-bold uppercase ${
activeInvoice.status === "Settled"
? "bg-neon-green/20 text-neon-green"
: activeInvoice.status === "Processing"
? "bg-amber-500/20 text-amber-400"
: "bg-neon-purple/20 text-neon-purple"
}`}>
{activeInvoice.status === "New" ? "Waiting for payment…" : activeInvoice.status}
</span>
<span className="text-xs text-zinc-500">
${activeInvoice.usdAmount.toFixed(2)} USD
</span>
</div>
{/* BTC address (fetched from payment-methods on first poll) */}
{activeInvoice.btcAddress ? (
<div>
<div className="mb-1 text-xs text-zinc-500 uppercase tracking-wider">Send exactly</div>
<div className="flex items-center gap-2">
<div className="flex-1 rounded-xl border border-amber-500/30 bg-black/60 px-4 py-3 font-mono text-sm text-amber-400 break-all">
{activeInvoice.btcAmount ?? "…"} BTC
</div>
</div>
<div className="mt-3 mb-1 text-xs text-zinc-500 uppercase tracking-wider">To this address</div>
<div className="rounded-xl border border-amber-500/30 bg-black/60 px-4 py-3 font-mono text-xs text-amber-400 break-all">
{activeInvoice.btcAddress}
</div>
<button
type="button"
onClick={() => void copyText(activeInvoice.btcAddress!, "BTC")}
className="mt-2 w-full rounded-xl border border-amber-500/30 bg-amber-500/10 py-2.5 text-xs font-bold text-amber-400 uppercase hover:opacity-90"
>
{copied === "BTC" ? "✓ Copied" : "Copy BTC Address"}
</button>
{activeInvoice.btcRate && (
<p className="mt-2 text-[11px] text-zinc-600">
Rate locked at 1 BTC ${parseFloat(activeInvoice.btcRate).toLocaleString()} USD
</p>
)}
</div>
) : (
<div className="animate-pulse text-xs text-zinc-500">Fetching payment address</div>
)}
{/* Success */}
{(bpSuccess || activeInvoice.credited) && (
<div className="rounded-xl border border-neon-green/40 bg-neon-green/10 p-4 text-sm font-bold text-neon-green">
{bpSuccess ?? `$${activeInvoice.usdAmount.toFixed(2)} USD credited to @${user.username}`}
</div>
)}
{!activeInvoice.credited && activeInvoice.status !== "Settled" && (
<p className="text-[11px] text-zinc-600">
Polling for confirmation every 10 s. Do not close this tab until confirmed.
This invoice is valid for 60 minutes.
</p>
)}
<button
type="button"
onClick={() => { setActiveInvoice(null); setBpSuccess(null); setBpError(null); }}
className="mt-2 rounded-xl border border-white/10 px-5 py-2 text-xs text-zinc-500 hover:border-white/20"
>
New invoice
</button>
</div>
)}
</div>
{/* Invoice history */}
{invoiceHistory.filter((i) => i.handle === user.username).length > 0 && (
<div>
<h3 className="mb-3 font-mono text-xs uppercase tracking-wider text-zinc-600">Invoice history</h3>
<div className="space-y-2">
{invoiceHistory.filter((i) => i.handle === user.username).map((inv) => (
<div key={inv.invoiceId} className="glass flex items-center justify-between gap-4 rounded-xl border border-white/10 px-4 py-3 text-xs">
<div className="font-mono text-zinc-400">
{inv.invoiceId.slice(0, 12)} · ${inv.usdAmount.toFixed(2)}
</div>
<div className="flex items-center gap-2">
<span className={`rounded-full px-2 py-0.5 text-[10px] font-bold uppercase ${
inv.credited ? "bg-neon-green/20 text-neon-green"
: inv.status === "Processing" ? "bg-amber-500/20 text-amber-400"
: inv.status === "Expired" ? "bg-red-500/20 text-red-400"
: "bg-zinc-500/20 text-zinc-400"
}`}>
{inv.credited ? "credited" : inv.status.toLowerCase()}
</span>
<span className="text-zinc-600">{new Date(inv.createdAt).toLocaleDateString()}</span>
{!inv.credited && inv.status !== "Expired" && inv.status !== "Invalid" && inv.status !== "Settled" && (
<button
type="button"
onClick={() => setActiveInvoice(inv)}
className="rounded border border-neon-purple/30 px-2 py-0.5 text-[10px] text-neon-purple hover:bg-neon-purple/10"
>
Resume
</button>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* ── BTC TAB (manual txid verify) ──────────────────────────────── */}
{activeTab === "BTC" && (
<div className="space-y-5">
<div className="glass rounded-2xl border border-amber-500/30 p-6">
<div className="mb-1 flex items-center justify-between">
<h2 className="font-orbitron text-lg text-amber-400">BTC Manual Verify</h2>
<span className="rounded-full bg-neon-green/15 px-3 py-1 text-xs font-bold text-neon-green">Auto-credit</span>
</div>
{!btcReady ? (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/5 p-4 text-sm text-amber-300">
<p className="font-bold">Address not configured</p>
<p className="mt-1 text-amber-400/70">
Set <code className="rounded bg-black/40 px-1">NEXT_PUBLIC_MERCHANT_BTC_ADDRESS</code> in{" "}
<code className="rounded bg-black/40 px-1">.env.local</code>
</p>
</div>
) : (
<>
<p className="mb-4 text-xs text-zinc-500">
Send BTC to the static address below, then paste the 64-character transaction ID.
We verify via mempool.space and credit instantly.
</p>
<div className="mb-2 rounded-xl border border-amber-500/30 bg-black/60 px-4 py-3 font-mono text-xs text-amber-400 break-all">
{btcAddr}
</div>
<button
type="button"
onClick={() => void copyText(btcAddr, "BTC")}
className="mb-5 w-full rounded-xl border border-amber-500/30 bg-amber-500/10 py-2.5 text-xs font-bold text-amber-400 uppercase hover:opacity-90"
>
{copied === "BTC" ? "✓ Copied" : "Copy BTC Address"}
</button>
<form onSubmit={(e) => void onVerifyBtc(e)} className="space-y-3">
<input
value={btcTxid}
onChange={(e) => setBtcTxid(e.target.value.trim())}
placeholder="Paste 64-character Bitcoin txid"
className="w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-xs placeholder-zinc-600 focus:border-amber-500/40 focus:outline-none"
maxLength={64}
/>
<button
type="submit"
disabled={btcBusy || !btcTxid.trim()}
className="w-full rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 py-3 text-sm font-bold text-black disabled:opacity-40"
>
{btcBusy ? "Verifying…" : "Verify & Credit Now"}
</button>
</form>
{btcMsg && (
<div className={`mt-4 rounded-xl border p-3 text-sm ${btcMsg.kind === "ok" ? "border-neon-green/40 bg-neon-green/10 text-neon-green" : "border-red-500/40 bg-red-500/10 text-red-400"}`}>
{btcMsg.text}
</div>
)}
</>
)}
</div>
</div>
)}
{/* ── ETH / XMR TAB (manual review) ─────────────────────────────── */}
{(activeTab === "ETH" || activeTab === "XMR") && (() => {
const addr = activeTab === "ETH" ? ethAddr : xmrAddr;
const addrReady = Boolean(addr);
const envVar = activeTab === "ETH" ? "NEXT_PUBLIC_ETH_ADDRESS" : "NEXT_PUBLIC_XMR_ADDRESS";
const note = activeTab === "ETH"
? "Send ERC-20 or native ETH · submit txhash below · credited within 224h after manual review"
: "Monero transactions take ~20 min · submit txid below · credited within 224h after manual review";
return (
<div className="glass rounded-2xl border p-6" style={{borderColor: activeTab === "ETH" ? "rgba(59,130,246,0.3)" : "rgba(249,115,22,0.3)"}}>
<div className="mb-1 flex items-center justify-between">
<h2 className={`font-orbitron text-lg ${TAB_COLOR[activeTab]}`}>{activeTab} Deposit</h2>
<span className="rounded-full bg-amber-500/15 px-3 py-1 text-xs font-bold text-amber-400">Manual review</span>
</div>
{!addrReady ? (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/5 p-4 text-sm text-amber-300">
<p className="font-bold">Address not configured</p>
<p className="mt-1 text-amber-400/70">
Set <code className="rounded bg-black/40 px-1">{envVar}</code> in{" "}
<code className="rounded bg-black/40 px-1">.env.local</code>
</p>
</div>
) : (
<>
<p className="mb-4 text-xs text-zinc-500">{note}</p>
<div className={`mb-2 rounded-xl border bg-black/60 px-4 py-3 font-mono text-xs break-all ${TAB_BORDER[activeTab]} ${TAB_COLOR[activeTab]}`}>
{addr}
</div>
<button
type="button"
onClick={() => void copyText(addr, activeTab)}
className={`mb-5 w-full rounded-xl border py-2.5 text-xs font-bold uppercase hover:opacity-90 ${TAB_BORDER[activeTab]} ${TAB_BG[activeTab]} ${TAB_COLOR[activeTab]}`}
>
{copied === activeTab ? `✓ Copied` : `Copy ${activeTab} Address`}
</button>
<form onSubmit={(e) => void onSubmitManual(e)} className="space-y-3">
<input
value={manualTxid}
onChange={(e) => setManualTxid(e.target.value.trim())}
placeholder={activeTab === "ETH" ? "Ethereum txhash — 0x…" : "Monero transaction ID"}
className="w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-xs placeholder-zinc-600 focus:outline-none"
maxLength={100}
/>
<button
type="submit"
disabled={manualBusy || !manualTxid.trim()}
className={`w-full rounded-xl py-3 text-sm font-bold text-white disabled:opacity-40 bg-gradient-to-r ${activeTab === "ETH" ? "from-blue-600 to-blue-400" : "from-orange-600 to-orange-400"}`}
>
{manualBusy ? "Submitting…" : `Submit ${activeTab} for Review`}
</button>
</form>
{manualMsg && (
<div className={`mt-4 rounded-xl border p-3 text-sm ${manualMsg.kind === "ok" ? "border-neon-green/40 bg-neon-green/10 text-neon-green" : manualMsg.kind === "pending" ? "border-amber-500/40 bg-amber-500/10 text-amber-300" : "border-red-500/40 bg-red-500/10 text-red-400"}`}>
<p>{manualMsg.text}</p>
{manualMsg.ref && <p className="mt-1 font-mono text-xs opacity-70">Ref: {manualMsg.ref}</p>}
</div>
)}
</>
)}
</div>
);
})()}
{/* Manual deposit history (ETH/XMR) */}
{manualHistory.length > 0 && (activeTab === "ETH" || activeTab === "XMR") && (
<div className="mt-6">
<h3 className="mb-3 font-mono text-xs uppercase tracking-wider text-zinc-600">Manual review history</h3>
<div className="space-y-2">
{manualHistory.filter((d) => d.coin === activeTab).map((d) => (
<div key={d.id} className="glass flex items-center justify-between gap-4 rounded-xl border border-white/10 px-4 py-3 text-xs">
<div>
<span className={`font-bold ${TAB_COLOR[d.coin]}`}>{d.coin}</span>
<span className="ml-2 font-mono text-zinc-500">{d.txid.slice(0, 12)}{d.txid.slice(-6)}</span>
{d.ref && <span className="ml-2 text-[10px] text-zinc-600">ref: {d.ref}</span>}
</div>
<div className="flex items-center gap-2">
<span className={`rounded-full px-2 py-0.5 text-[10px] font-bold uppercase ${d.status === "credited" ? "bg-neon-green/20 text-neon-green" : "bg-amber-500/20 text-amber-400"}`}>
{d.status}
</span>
<span className="text-zinc-600">{new Date(d.ts).toLocaleDateString()}</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Important notes */}
<div className="mt-10 rounded-2xl border border-white/10 bg-white/[0.02] p-6 text-sm">
<div className="mb-3 font-bold text-zinc-200">Important Read Before Depositing</div>
<ul className="space-y-2 text-zinc-500">
{[
"Always verify the receiving address on each new session before sending.",
"BTCPay invoices expire after 60 minutes. Static BTC deposits are valid until a new address is published.",
"BTC (BTCPay) credits automatically on settlement. Manual ETH/XMR review takes 224h.",
"Deposited funds become USD store credit. No withdrawals — credits are for purchases only.",
"Export your account bundle from /dashboard before clearing browser data.",
].map((note, i) => (
<li key={i} className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan"></span>
{note}
</li>
))}
</ul>
</div>
<div className="mt-8 flex flex-wrap justify-center gap-4 text-xs text-zinc-600">
<Link href="/dashboard" className="text-neon-cyan hover:underline">Dashboard</Link>
<Link href="/checkout" className="hover:text-zinc-400">Checkout</Link>
<Link href="/wallets" className="hover:text-zinc-400">Digital Passes</Link>
<Link href="/" className="hover:text-zinc-400">Hub</Link>
</div>
</div>
</div>
);
}