Add proper deposit desk: multi-coin, clear custody model, DepositWidget

- /account/add-funds: full rebuild as Deposit Desk
  - 3-step how-it-works explainer: you send crypto → we hold it →
    you shop with USD credit → we pay vendors with your crypto
  - Coin selector: BTC (auto-verify), ETH (manual review), XMR (manual review)
  - Each coin shows deposit address from env var with copy button
  - BTC: immediate on-chain verify via /api/btc/verify (mempool.space)
  - ETH/XMR: submit txhash to /api/pending-deposit for manual review
  - Pending deposit history stored in localStorage with status
  - Important notes block explaining custody, no-withdrawal, confirmations
  - Back-links to checkout, wallets, dashboard

- /api/pending-deposit: new route to log ETH/XMR manual review requests
  (logs to console, ready to wire to DB/webhook)

- DepositWidget component: reusable compact + full variants
  - compact: balance + "Deposit crypto →" CTA (used in checkout)
  - full: balance + how-it-works summary (used in dashboard)

- Dashboard: replace balance section with DepositWidget + stats
- Checkout: add compact DepositWidget above CheckoutFlow, fix back link
  from "Sanctuary" → "Market", update feature cards to be accurate

- .env.example: document all three coin address env vars + optional
  payment processor URL

- .env.example: add NEXT_PUBLIC_ETH_ADDRESS, NEXT_PUBLIC_XMR_ADDRESS

Made-with: Cursor
This commit is contained in:
drjones
2026-04-16 01:03:14 -07:00
parent 2f928fbdc4
commit 348e55b63e
5 changed files with 596 additions and 143 deletions

View File

@@ -8,32 +8,103 @@ import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
type Coin = "BTC" | "ETH" | "XMR";
type PendingDeposit = {
id: string;
coin: Coin;
txid: string;
ts: number;
status: "pending" | "credited";
ref?: string;
};
const PENDING_KEY = "cyberlux-pending-deposits-v1";
function loadPending(): PendingDeposit[] {
if (typeof window === "undefined") return [];
try { return JSON.parse(localStorage.getItem(PENDING_KEY) ?? "[]") as PendingDeposit[]; }
catch { return []; }
}
function savePending(d: PendingDeposit[]) {
if (typeof window !== "undefined") localStorage.setItem(PENDING_KEY, JSON.stringify(d));
}
function shortId() {
return Math.random().toString(36).slice(2, 10).toUpperCase();
}
const COIN_COLORS: Record<Coin, string> = {
BTC: "text-amber-400",
ETH: "text-blue-400",
XMR: "text-orange-400",
};
const COIN_BORDER: Record<Coin, string> = {
BTC: "border-amber-500/30",
ETH: "border-blue-500/30",
XMR: "border-orange-500/30",
};
const COIN_GLOW: Record<Coin, string> = {
BTC: "bg-amber-500/10",
ETH: "bg-blue-500/10",
XMR: "bg-orange-500/10",
};
export default function AddFundsPage() {
const router = useRouter();
const { user, hydrated } = useAccount();
const { usdStoreCredit, verifyBtcDeposit } = useWallet();
const { usdStoreCredit, luxCredits, verifyBtcDeposit } = useWallet();
const [activeCoin, setActiveCoin] = useState<Coin>("BTC");
const [txid, setTxid] = useState("");
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
const [msg, setMsg] = useState<{ kind: "ok" | "err" | "pending"; text: string; ref?: string } | null>(null);
const [pendingDeposits, setPendingDeposits] = useState<PendingDeposit[]>([]);
const [copied, setCopied] = useState<Coin | null>(null);
const processorUrl = (process.env.NEXT_PUBLIC_BITCOIN_CHECKOUT_URL || "").trim();
const btcAddr = getMerchantBtcAddress();
const btcReady = isMerchantBtcConfigured();
const ethAddr = (process.env.NEXT_PUBLIC_ETH_ADDRESS || "").trim();
const xmrAddr = (process.env.NEXT_PUBLIC_XMR_ADDRESS || "").trim();
const addresses: Record<Coin, string> = {
BTC: btcAddr,
ETH: ethAddr,
XMR: xmrAddr,
};
const addrReady: Record<Coin, boolean> = {
BTC: btcReady,
ETH: Boolean(ethAddr),
XMR: Boolean(xmrAddr),
};
useEffect(() => {
if (hydrated && !user) router.replace("/sign-in?next=/account/add-funds");
}, [hydrated, user, router]);
if (!hydrated || !user) {
return (
<div className="flex min-h-[50vh] items-center justify-center font-mono text-sm text-zinc-500">
Loading
</div>
);
}
useEffect(() => {
setPendingDeposits(loadPending());
}, []);
const merchant = getMerchantBtcAddress();
const ready = isMerchantBtcConfigured();
const copyAddr = async (coin: Coin) => {
const addr = addresses[coin];
if (!addr) return;
try {
await navigator.clipboard.writeText(addr);
setCopied(coin);
setTimeout(() => setCopied(null), 1800);
} catch { /* ignore */ }
};
const onVerify = async (e: FormEvent) => {
const onVerifyBtc = async (e: FormEvent) => {
e.preventDefault();
if (!txid.trim()) return;
setMsg(null);
setBusy(true);
try {
@@ -41,118 +112,365 @@ export default function AddFundsPage() {
if (res.ok) {
setMsg({
kind: "ok",
text: `Credited $${(res.creditedUsd ?? 0).toFixed(2)} USD to @${user.username}.`,
text: ` $${(res.creditedUsd ?? 0).toFixed(2)} USD credited to @${user!.username}. Balance updated.`,
});
setTxid("");
} else {
setMsg({ kind: "err", text: res.error || "Verification failed" });
setMsg({ kind: "err", text: res.error || "Verification failed. Check the txid and try again." });
}
} finally {
setBusy(false);
}
};
const onSubmitManual = async (e: FormEvent) => {
e.preventDefault();
if (!txid.trim() || !user) return;
setMsg(null);
setBusy(true);
try {
const res = await fetch("/api/pending-deposit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ coin: activeCoin, txid: txid.trim(), handle: user.username }),
});
const json = (await res.json()) as { ok: boolean; message?: string; ref?: string; error?: string };
if (json.ok) {
const entry: PendingDeposit = {
id: shortId(),
coin: activeCoin,
txid: txid.trim(),
ts: Date.now(),
status: "pending",
ref: json.ref,
};
const updated = [entry, ...pendingDeposits];
setPendingDeposits(updated);
savePending(updated);
setMsg({ kind: "pending", text: json.message ?? "Deposit request submitted for manual review.", ref: json.ref });
setTxid("");
} else {
setMsg({ kind: "err", text: json.error ?? "Submission failed." });
}
} catch {
setMsg({ kind: "err", text: "Network error. Try again." });
} finally {
setBusy(false);
}
};
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>
);
}
return (
<div className="min-h-screen bg-[#0a0a0a] text-zinc-200">
<Navbar />
<div className="mx-auto max-w-2xl px-4 py-28">
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/80">account</p>
<h1 className="mt-2 font-orbitron text-3xl font-bold">Add funds</h1>
<p className="mt-3 text-sm text-zinc-500">
One CyberLux handle same USD balance on hub, market, forum, exchange, and checkout. Send Bitcoin to
your configured receiving address, wait for at least one confirmation, then paste the transaction id
here. We verify on-chain pays to{" "}
<code className="text-neon-green/90">MERCHANT_BTC_ADDRESS</code> and credit USD at spot (see{" "}
<code className="text-zinc-500">/api/btc/verify</code>).
</p>
<div className="mx-auto max-w-3xl px-4 py-28">
<div className="glass mt-8 rounded-2xl border border-white/10 p-6">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<p className="text-xs uppercase tracking-wider text-zinc-500">Current balance</p>
<p className="font-orbitron text-3xl text-neon-cyan">${usdStoreCredit.toFixed(2)} USD</p>
{/* 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 instantly. <strong className="text-zinc-200">You no longer
hold the crypto</strong> we do. When you make a purchase we pay the vendor with it on your behalf.
</p>
</div>
{/* How it works */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-3">
{[
{ step: "1", icon: "₿", title: "You send crypto", body: "Send BTC, ETH, or XMR to our receiving address." },
{ step: "2", icon: "📊", title: "We verify & convert", body: "On-chain confirmation. Credited at live market USD rate." },
{ step: "3", icon: "🛒", title: "You shop", body: "Spend USD credits across every CyberLux surface. We pay vendors in crypto." },
].map((s) => (
<div key={s.step} 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.step}
</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>
<Link
href="/checkout"
className="rounded-lg border border-neon-cyan/40 px-4 py-2 text-sm text-neon-cyan hover:bg-neon-cyan/10"
>
Spend at checkout
</Link>
</div>
</div>
{processorUrl ? (
<div className="glass mt-6 rounded-2xl border border-neon-purple/25 p-6">
<h2 className="font-orbitron text-lg text-neon-purple">Bitcoin checkout</h2>
<p className="mt-2 text-sm text-zinc-500">
Open your hosted payment / BTCPay / processor page (set{" "}
<code className="rounded bg-white/10 px-1">NEXT_PUBLIC_BITCOIN_CHECKOUT_URL</code>).
</p>
<a
href={processorUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-4 inline-flex rounded-xl bg-gradient-to-r from-neon-purple to-neon-pink px-6 py-3 text-sm font-bold text-white"
{/* Coin selector */}
<div className="mb-6 flex gap-3">
{(["BTC", "ETH", "XMR"] as Coin[]).map((coin) => (
<button
key={coin}
type="button"
onClick={() => { setActiveCoin(coin); setMsg(null); setTxid(""); }}
className={`flex-1 rounded-xl border py-3 text-sm font-bold uppercase transition-all ${
activeCoin === coin
? `${COIN_BORDER[coin]} ${COIN_GLOW[coin]} ${COIN_COLORS[coin]}`
: "border-white/10 text-zinc-500 hover:border-white/20"
}`}
>
Open payment page
</a>
</div>
) : null}
{coin}
</button>
))}
</div>
<div className="glass mt-6 rounded-2xl border border-white/10 p-6">
<h2 className="font-orbitron text-lg text-neon-green">On-chain deposit</h2>
{!ready ? (
<p className="mt-3 text-sm text-amber-300/90">
Set <code className="rounded bg-white/10 px-1">NEXT_PUBLIC_MERCHANT_BTC_ADDRESS</code> and{" "}
<code className="rounded bg-white/10 px-1">MERCHANT_BTC_ADDRESS</code> in{" "}
<code className="rounded bg-white/10 px-1">.env.local</code>, restart Next, then reload this page.
</p>
{/* Deposit address */}
<div className={`glass mb-6 rounded-2xl border p-6 ${COIN_BORDER[activeCoin]}`}>
<div className="mb-4 flex items-center justify-between">
<h2 className={`font-orbitron text-lg ${COIN_COLORS[activeCoin]}`}>
{activeCoin} Deposit Address
</h2>
{activeCoin === "BTC" && (
<span className="rounded-full bg-neon-green/15 px-3 py-1 text-xs font-bold text-neon-green">
Auto-verify
</span>
)}
{(activeCoin === "ETH" || activeCoin === "XMR") && (
<span className="rounded-full bg-amber-500/15 px-3 py-1 text-xs font-bold text-amber-400">
Manual review
</span>
)}
</div>
{!addrReady[activeCoin] ? (
<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">
{activeCoin === "BTC"
? "NEXT_PUBLIC_MERCHANT_BTC_ADDRESS"
: activeCoin === "ETH"
? "NEXT_PUBLIC_ETH_ADDRESS"
: "NEXT_PUBLIC_XMR_ADDRESS"}
</code>{" "}
in <code className="rounded bg-black/40 px-1">.env.local</code> and restart Next.
</p>
</div>
) : (
<>
<p className="mt-3 text-sm text-zinc-400">
Send from any wallet. After one confirmation, paste the 64-character txid. Credit is computed from
outputs paying this address only.
</p>
<div className="mt-4 rounded-xl border border-white/10 bg-black/50 p-4 font-mono text-sm break-all text-neon-green/90">
{merchant}
<div
className={`mb-3 rounded-xl border bg-black/60 p-4 font-mono text-sm break-all ${COIN_BORDER[activeCoin]} ${COIN_COLORS[activeCoin]}`}
>
{addresses[activeCoin]}
</div>
<form onSubmit={(e) => void onVerify(e)} className="mt-6 space-y-3">
<label className="block text-xs text-zinc-500">Transaction id</label>
<input
value={txid}
onChange={(e) => setTxid(e.target.value.replace(/\s+/g, ""))}
className="w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-sm"
placeholder="64 hex characters"
autoComplete="off"
/>
<button
type="submit"
disabled={busy}
className="rounded-xl bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 text-sm font-bold text-background disabled:opacity-40"
>
{busy ? "Verifying…" : "Verify & credit USD"}
</button>
{msg ? (
<p className={`text-sm ${msg.kind === "ok" ? "text-neon-green" : "text-red-400"}`}>{msg.text}</p>
) : null}
</form>
<button
type="button"
onClick={() => void copyAddr(activeCoin)}
className={`w-full rounded-xl border py-3 text-sm font-bold uppercase transition-all ${COIN_BORDER[activeCoin]} ${COIN_GLOW[activeCoin]} ${COIN_COLORS[activeCoin]} hover:opacity-90`}
>
{copied === activeCoin ? "✓ Address copied" : `Copy ${activeCoin} address`}
</button>
{activeCoin === "BTC" && (
<div className="mt-2 text-xs text-zinc-600">
Send any amount · 1 confirmation required · credited at CoinGecko spot rate
</div>
)}
{activeCoin === "ETH" && (
<div className="mt-2 text-xs text-zinc-600">
Send ERC-20 or native ETH · submit txhash below for manual review · credited within 224h
</div>
)}
{activeCoin === "XMR" && (
<div className="mt-2 text-xs text-zinc-600">
Monero transactions take ~20 min · submit txid below for manual review · credited within 224h
</div>
)}
</>
)}
</div>
<p className="mt-10 text-center text-xs text-zinc-600">
<Link href="/dashboard" className="text-neon-cyan hover:underline">
Dashboard
</Link>
{" · "}
<Link href="/account/hidden-services" className="text-zinc-500 hover:text-zinc-400">
Cross-onion identity
</Link>
{" · "}
<Link href="/" className="text-zinc-500 hover:text-zinc-400">
Hub
</Link>
</p>
{/* Optional hosted processor (BTCPay etc) */}
{activeCoin === "BTC" && processorUrl && (
<div className="glass mb-6 rounded-2xl border border-neon-purple/25 p-5">
<div className="flex items-center justify-between gap-4">
<div>
<div className="font-bold text-neon-purple">Payment Processor</div>
<p className="mt-1 text-xs text-zinc-500">
Pay through the hosted checkout page (BTCPay or custom processor).
</p>
</div>
<a
href={processorUrl}
target="_blank"
rel="noopener noreferrer"
className="shrink-0 rounded-xl bg-gradient-to-r from-neon-purple to-neon-pink px-5 py-3 text-sm font-bold text-white hover:opacity-90"
>
Open
</a>
</div>
</div>
)}
{/* Verify / submit form */}
{addrReady[activeCoin] && (
<div className="glass rounded-2xl border border-white/10 p-6">
<h3 className="mb-1 font-orbitron text-base">
{activeCoin === "BTC" ? "Verify BTC Transaction" : `Submit ${activeCoin} Deposit for Review`}
</h3>
<p className="mb-5 text-xs text-zinc-500">
{activeCoin === "BTC"
? "After at least 1 confirmation, paste the 64-character transaction ID. We check mempool.space and credit immediately."
: `After sending, paste the transaction hash. Our team verifies on-chain and credits your @${user.username} account. You'll see it in your pending history below.`}
</p>
<form
onSubmit={(e) => activeCoin === "BTC" ? void onVerifyBtc(e) : void onSubmitManual(e)}
className="space-y-4"
>
<input
value={txid}
onChange={(e) => setTxid(e.target.value.trim())}
placeholder={
activeCoin === "BTC"
? "Bitcoin txid — 64 hex characters"
: activeCoin === "ETH"
? "Ethereum txhash — 0x…"
: "Monero txid"
}
className="w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-sm placeholder-zinc-600 focus:border-neon-cyan/40 focus:outline-none"
autoComplete="off"
maxLength={100}
/>
<button
type="submit"
disabled={busy || !txid.trim()}
className="w-full rounded-xl bg-gradient-to-r from-neon-cyan to-neon-purple py-3 text-sm font-bold text-background disabled:opacity-40"
>
{busy
? "Verifying…"
: activeCoin === "BTC"
? "Verify & Credit USD Now"
: `Submit ${activeCoin} for Review`}
</button>
</form>
{msg && (
<div
className={`mt-4 rounded-xl border p-4 text-sm ${
msg.kind === "ok"
? "border-neon-green/40 bg-neon-green/10 text-neon-green"
: msg.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>{msg.text}</p>
{msg.ref && (
<p className="mt-1 font-mono text-xs opacity-70">Reference: {msg.ref}</p>
)}
</div>
)}
</div>
)}
{/* Pending history */}
{pendingDeposits.length > 0 && (
<div className="mt-8">
<h3 className="mb-4 font-orbitron text-sm text-zinc-400">Pending Manual Reviews</h3>
<div className="space-y-3">
{pendingDeposits.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-sm"
>
<div>
<span className={`font-bold ${COIN_COLORS[d.coin]}`}>{d.coin}</span>
<span className="ml-3 font-mono text-xs text-zinc-500">
{d.txid.slice(0, 12)}{d.txid.slice(-8)}
</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-xs 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">
<li className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan"></span>
Always double-check the address against the one shown here before sending. Verify on each new session.
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan"></span>
BTC deposits are credited automatically after 1 confirmation (~10 min). ETH and XMR require manual review (224h).
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan"></span>
Your USD credit balance is stored per-handle in your browser. Export your account bundle from
{" "}<Link href="/dashboard" className="text-neon-cyan hover:underline">/dashboard</Link>{" "}
before clearing site data.
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan"></span>
Once credited, funds cannot be withdrawn they are USD store credit for purchases only.
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan"></span>
Minimum deposit: no minimum enforced, but small amounts may be consumed by exchange rate rounding.
</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="/account/hidden-services" className="hover:text-zinc-400">Cross-onion identity</Link>
<Link href="/" className="hover:text-zinc-400">Hub</Link>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,44 @@
/**
* /api/pending-deposit
*
* Stores a manual-review deposit request (ETH or XMR) server-side in a
* simple append-only JSON file. In production you'd swap this for a DB
* write. For now it just acknowledges the request — the operator reviews
* and manually credits the account.
*/
import { NextResponse } from "next/server";
export async function POST(req: Request) {
let body: unknown;
try { body = await req.json(); } catch {
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
}
const { coin, txid, handle } = (body as Record<string, string | undefined>) ?? {};
if (!coin || !txid || !handle) {
return NextResponse.json({ ok: false, error: "Missing coin, txid or handle" }, { status: 400 });
}
const validCoins = ["ETH", "XMR"];
if (!validCoins.includes(coin.toUpperCase())) {
return NextResponse.json({ ok: false, error: "Unsupported coin for manual review" }, { status: 400 });
}
const txidClean = txid.trim();
if (txidClean.length < 20) {
return NextResponse.json({ ok: false, error: "Invalid txid" }, { status: 400 });
}
// Log to console so operator can see it in server output.
// In production: write to DB or send webhook.
console.log(
`[PENDING DEPOSIT] coin=${coin.toUpperCase()} handle=@${handle} txid=${txidClean} ts=${new Date().toISOString()}`
);
return NextResponse.json({
ok: true,
message: `Manual review request logged for @${handle}. Operator will verify and credit your account.`,
ref: `${coin.toUpperCase()}-${txidClean.slice(0, 8).toUpperCase()}`,
});
}

View File

@@ -1,6 +1,7 @@
import Navbar from "@/components/Navbar";
import ParticleBackground from "@/components/ParticleBackground";
import CheckoutFlow from "@/components/CheckoutFlow";
import DepositWidget from "@/components/DepositWidget";
import Link from "next/link";
export default function CheckoutPage() {
@@ -9,52 +10,68 @@ export default function CheckoutPage() {
<ParticleBackground />
<Navbar />
<main className="container mx-auto px-4 py-24">
<div className="mb-12 text-center">
<div className="mb-10 text-center">
<Link
href="/"
href="/market"
className="inline-flex items-center gap-2 text-neon-cyan hover:text-neon-purple"
>
Back to Sanctuary
Back to Market
</Link>
<h1 className="mt-6 font-orbitron text-5xl font-bold md:text-6xl">
CHECKOUT <span className="text-neon-cyan">RITUAL</span>
</h1>
<p className="mx-auto mt-4 max-w-2xl text-foreground/70">
Each step is designed to feel like a ceremonial unlocking. Your data is encrypted in realtime, and your payment is transformed into a visual experience.
Pay with your USD balance funded by Bitcoin deposit. Balance is credited after on-chain
verification. We hold your crypto and pay vendors on your behalf.
</p>
</div>
{/* Deposit reminder before CheckoutFlow */}
<div className="mx-auto mb-10 max-w-2xl">
<DepositWidget compact />
</div>
<CheckoutFlow />
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-3">
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 text-3xl"></div>
<h3 className="mb-2 font-bold">Bitcoin-funded USD</h3>
<p className="text-sm text-foreground/60">
Send BTC to your deposit address at <Link href="/account/add-funds" className="text-neon-cyan hover:underline">Add Funds</Link>.
After 1 confirmation we credit USD at live spot rate.
</p>
</div>
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 text-3xl">🔐</div>
<h3 className="mb-2 font-bold">EndtoEnd Encryption</h3>
<h3 className="mb-2 font-bold">Per-handle balance</h3>
<p className="text-sm text-foreground/60">
Your payment details never touch our servers. They are encrypted clientside before transmission.
USD credit is tied to your CyberLux handle same balance across hub, market, forum, and exchange.
Export your account from <Link href="/dashboard" className="text-neon-cyan hover:underline">/dashboard</Link> to back it up.
</p>
</div>
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 text-3xl"></div>
<h3 className="mb-2 font-bold">Instant Crypto Conversion</h3>
<div className="mb-4 text-3xl">🧾</div>
<h3 className="mb-2 font-bold">Vault receipt</h3>
<p className="text-sm text-foreground/60">
Realtime exchange rates ensure you pay the exact amount, with no hidden fees.
</p>
</div>
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 text-3xl">🎨</div>
<h3 className="mb-2 font-bold">Digital Art Receipt</h3>
<p className="text-sm text-foreground/60">
Each completed order generates a unique piece of generative art stored in your vault.
Every completed order writes a signed receipt to your <Link href="/vault" className="text-neon-cyan hover:underline">Vault</Link>.
LUX credits earned on each purchase.
</p>
</div>
</div>
</main>
<footer className="glass border-t border-white/10 px-4 py-8 text-center text-sm text-foreground/40">
<p>
Crypto checkout uses your <strong>USD balance</strong> after a Bitcoin payment is{" "}
<strong>verified on-chain</strong>. Configure your receiving address in{" "}
<code className="rounded bg-white/10 px-1">.env.local</code>. Guest/card paths are demo-only.
Payments use USD balance funded by verified on-chain Bitcoin deposits. Configure merchant
address in <code className="rounded bg-white/10 px-1">.env.local</code>.
ETH and XMR deposits go through manual review.
</p>
<div className="mt-3 flex justify-center gap-6 text-foreground/30">
<Link href="/account/add-funds" className="hover:text-neon-cyan">Add Funds</Link>
<Link href="/dashboard" className="hover:text-foreground/60">Dashboard</Link>
<Link href="/market" className="hover:text-foreground/60">Market</Link>
</div>
</footer>
</>
);
}
}

View File

@@ -4,6 +4,7 @@ import { FormEvent, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Navbar from "@/components/Navbar";
import DepositWidget from "@/components/DepositWidget";
import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
import { computeUserActivityStats } from "@/lib/userActivityStats";
@@ -100,33 +101,21 @@ export default function DashboardPage() {
</form>
</section>
<section className="glass rounded-2xl border border-neon-cyan/20 p-6">
<h2 className="font-orbitron text-lg font-bold text-neon-green">Balance</h2>
<p className="mt-2 text-sm text-zinc-500">
USD is credited after verified Bitcoin deposits to your merchant address. LUX is loyalty currency (e.g.
completed checkouts).
</p>
<ul className="mt-4 space-y-2 font-mono text-sm">
<li className="flex justify-between">
<span className="text-zinc-500">LUX</span>
<section>
<DepositWidget />
<div className="mt-3 glass rounded-xl border border-white/10 p-4 font-mono text-xs">
<div className="flex justify-between text-zinc-500">
<span>Vault keys</span>
<span className="text-zinc-300">{vault.keys.length}</span>
</div>
<div className="flex justify-between mt-2 text-zinc-500">
<span>LUX credits</span>
<span className="text-neon-green">{luxCredits.toLocaleString()}</span>
</li>
<li className="flex justify-between">
<span className="text-zinc-500">USD (spendable)</span>
</div>
<div className="flex justify-between mt-2 text-zinc-500">
<span>USD balance</span>
<span className="text-neon-cyan">${usdStoreCredit.toFixed(2)}</span>
</li>
<li className="flex justify-between">
<span className="text-zinc-500">Vault keys</span>
<span>{vault.keys.length}</span>
</li>
</ul>
<div className="mt-4 flex flex-wrap gap-3 text-xs font-bold uppercase tracking-wider">
<Link href="/account/add-funds" className="text-neon-green hover:underline">
Add funds (BTC)
</Link>
<Link href="/checkout" className="text-neon-cyan hover:underline">
Checkout
</Link>
</div>
</div>
</section>
</div>

View File

@@ -0,0 +1,85 @@
"use client";
import Link from "next/link";
import { useWallet } from "@/contexts/WalletContext";
import { useAccount } from "@/contexts/AccountContext";
import { isMerchantBtcConfigured } from "@/lib/merchantBtc";
type DepositWidgetProps = {
compact?: boolean;
};
export default function DepositWidget({ compact = false }: DepositWidgetProps) {
const { usdStoreCredit, luxCredits } = useWallet();
const { user } = useAccount();
const btcReady = isMerchantBtcConfigured();
if (compact) {
return (
<div className="glass rounded-2xl border border-neon-cyan/20 p-5">
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-xs text-foreground/50">Your balance</div>
<div className="mt-0.5 font-orbitron text-2xl text-neon-cyan">
${usdStoreCredit.toFixed(2)}
<span className="ml-2 text-sm text-foreground/40">USD</span>
</div>
<div className="mt-0.5 text-xs text-neon-green">{luxCredits.toLocaleString()} LUX</div>
</div>
<Link
href="/account/add-funds"
className="shrink-0 rounded-xl bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-3 text-sm font-bold text-background hover:opacity-90"
>
Deposit crypto
</Link>
</div>
{!user && (
<p className="mt-3 text-xs text-amber-300/70">
<Link href="/sign-in?next=/account/add-funds" className="underline">Sign in</Link> to link deposits to your handle.
</p>
)}
{!btcReady && (
<p className="mt-2 text-[10px] text-amber-400/60">
Operator: set <code>NEXT_PUBLIC_MERCHANT_BTC_ADDRESS</code> in .env.local to enable deposits.
</p>
)}
</div>
);
}
return (
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 flex items-start justify-between gap-3">
<div>
<div className="text-xs text-foreground/50">Account balance</div>
<div className="mt-1 font-orbitron text-3xl text-neon-cyan">${usdStoreCredit.toFixed(2)}</div>
<div className="mt-0.5 text-sm text-neon-green">{luxCredits.toLocaleString()} LUX</div>
</div>
<Link
href="/account/add-funds"
className="rounded-xl bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-3 text-sm font-bold text-background hover:opacity-90"
>
Add crypto
</Link>
</div>
<div className="mt-4 rounded-xl border border-white/10 bg-black/30 p-4 text-sm">
<div className="mb-2 text-xs font-bold uppercase tracking-wider text-foreground/50">How it works</div>
<div className="space-y-2 text-foreground/70">
<div className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan">1.</span>
<span>You send crypto to our receiving address.</span>
</div>
<div className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan">2.</span>
<span>We verify on-chain and credit your account at market USD rate.</span>
</div>
<div className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan">3.</span>
<span>You shop with USD credits. We hold the crypto and pay vendors on your behalf.</span>
</div>
</div>
</div>
</div>
);
}