"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 = { BTCPAY: "text-neon-purple", BTC: "text-amber-400", ETH: "text-blue-400", XMR: "text-orange-400", }; const TAB_BORDER: Record = { BTCPAY: "border-neon-purple/30", BTC: "border-amber-500/30", ETH: "border-blue-500/30", XMR: "border-orange-500/30", }; const TAB_BG: Record = { 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(btcPayEnabled ? "BTCPAY" : "BTC"); const [copied, setCopied] = useState(null); // ── BTCPay state ────────────────────────────────────────────────────────── const [desiredUsd, setDesiredUsd] = useState("25"); const [bpBusy, setBpBusy] = useState(false); const [bpError, setBpError] = useState(null); const [activeInvoice, setActiveInvoice] = useState(null); const [invoiceHistory, setInvoiceHistory] = useState([]); const [bpSuccess, setBpSuccess] = useState(null); const pollRef = useRef | 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([]); 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) { // Credit client-side wallet (USD store credit) const result = creditBtcPayInvoice(invoice.invoiceId, invoice.usdAmount); if (result.ok) { updated.credited = true; setBpSuccess(`✓ $${invoice.usdAmount.toFixed(2)} USD credited to @${user.username}!`); } // Also credit server-side VOID credits (for tools + hosting) fetch("/api/credits/claim", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ invoiceId: invoice.invoiceId, handle: user.username }), }) .then((r) => r.json()) .then((d: { ok: boolean; voidCredited?: number }) => { if (d.ok && d.voidCredited) { setBpSuccess((prev) => (prev ?? "") + ` ✦ ${d.voidCredited} VOID credits added to tool vault.`, ); } }) .catch(() => void 0); 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 (
Loading…
); } const tabs: Tab[] = btcPayEnabled ? ["BTCPAY", "BTC", "ETH", "XMR"] : ["BTC", "ETH", "XMR"]; return (
{/* Header */}

@{user.username} · deposit desk

Fund Your Account

Send crypto to the address below. We verify the transaction on-chain, convert to USD at current market rate, and credit your account.{" "} You no longer hold the crypto — we do. When you purchase, we pay the vendor in crypto on your behalf.

{/* How it works */}
{[ { 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) => (
{s.n}
{s.icon}
{s.title}

{s.body}

))}
{/* Balance card */}
Current balance — @{user.username}
${usdStoreCredit.toFixed(2)} USD
{luxCredits.toLocaleString()} LUX
Spend → Passes
{/* Tab selector */}
{tabs.map((tab) => ( ))}
{/* ── BTCPAY TAB ─────────────────────────────────────────────────── */} {activeTab === "BTCPAY" && (

BTCPay Invoice

Auto-settle

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.

{!activeInvoice || activeInvoice.status === "Expired" || activeInvoice.status === "Invalid" ? (
void createInvoice(e)} className="space-y-4">
{["10", "25", "50", "100", "250"].map((v) => ( ))}
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" />
{bpError && (
{bpError}
)}
) : ( /* Active invoice */
{/* Status badge */}
{activeInvoice.status === "New" ? "Waiting for payment…" : activeInvoice.status} ${activeInvoice.usdAmount.toFixed(2)} USD
{/* BTC address (fetched from payment-methods on first poll) */} {activeInvoice.btcAddress ? (
Send exactly
{activeInvoice.btcAmount ?? "…"} BTC
To this address
{activeInvoice.btcAddress}
{activeInvoice.btcRate && (

Rate locked at 1 BTC ≈ ${parseFloat(activeInvoice.btcRate).toLocaleString()} USD

)}
) : (
Fetching payment address…
)} {/* Success */} {(bpSuccess || activeInvoice.credited) && (
{bpSuccess ?? `✓ $${activeInvoice.usdAmount.toFixed(2)} USD credited to @${user.username}`}
)} {!activeInvoice.credited && activeInvoice.status !== "Settled" && (

Polling for confirmation every 10 s. Do not close this tab until confirmed. This invoice is valid for 60 minutes.

)}
)}
{/* Invoice history */} {invoiceHistory.filter((i) => i.handle === user.username).length > 0 && (

Invoice history

{invoiceHistory.filter((i) => i.handle === user.username).map((inv) => (
{inv.invoiceId.slice(0, 12)}… · ${inv.usdAmount.toFixed(2)}
{inv.credited ? "credited" : inv.status.toLowerCase()} {new Date(inv.createdAt).toLocaleDateString()} {!inv.credited && inv.status !== "Expired" && inv.status !== "Invalid" && inv.status !== "Settled" && ( )}
))}
)}
)} {/* ── BTC TAB (manual txid verify) ──────────────────────────────── */} {activeTab === "BTC" && (

BTC — Manual Verify

Auto-credit
{!btcReady ? (

Address not configured

Set NEXT_PUBLIC_MERCHANT_BTC_ADDRESS in{" "} .env.local

) : ( <>

Send BTC to the static address below, then paste the 64-character transaction ID. We verify via mempool.space and credit instantly.

{btcAddr}
void onVerifyBtc(e)} className="space-y-3"> 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} />
{btcMsg && (
{btcMsg.text}
)} )}
)} {/* ── 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 2–24h after manual review" : "Monero transactions take ~20 min · submit txid below · credited within 2–24h after manual review"; return (

{activeTab} Deposit

Manual review
{!addrReady ? (

Address not configured

Set {envVar} in{" "} .env.local

) : ( <>

{note}

{addr}
void onSubmitManual(e)} className="space-y-3"> 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} />
{manualMsg && (

{manualMsg.text}

{manualMsg.ref &&

Ref: {manualMsg.ref}

}
)} )}
); })()} {/* Manual deposit history (ETH/XMR) */} {manualHistory.length > 0 && (activeTab === "ETH" || activeTab === "XMR") && (

Manual review history

{manualHistory.filter((d) => d.coin === activeTab).map((d) => (
{d.coin} {d.txid.slice(0, 12)}…{d.txid.slice(-6)} {d.ref && ref: {d.ref}}
{d.status} {new Date(d.ts).toLocaleDateString()}
))}
)} {/* Important notes */}
Important — Read Before Depositing
    {[ "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 2–24h.", "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) => (
  • {note}
  • ))}
Dashboard Checkout Digital Passes Hub
); }