Files
dark-lord/components/CheckoutFlow.tsx
drjones 78a071ba02 Harden onion boot flow and deepen site surfaces
Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation.

Made-with: Cursor
2026-04-07 21:35:52 -07:00

563 lines
25 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 Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCart } from "@/contexts/CartContext";
import { useWallet } from "@/contexts/WalletContext";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
import { estimateUsdForCryptoAmount } from "@/lib/shopFx";
const ENCRYPTION_BTC_FEE: Record<"standard" | "enhanced" | "quantum", number> = {
standard: 0,
enhanced: 0.001,
quantum: 0.005,
};
const CheckoutFlow = () => {
const { lines, setLineQty, removeLine, clearCart, hydrated: cartHydrated } = useCart();
const [step, setStep] = useState(1);
const [paymentMethod, setPaymentMethod] = useState<"crypto" | "guest" | "card">("crypto");
const [cryptoType, setCryptoType] = useState<"BTC" | "ETH" | "XMR">("BTC");
const [guestEmail, setGuestEmail] = useState("");
const [encryptionLevel, setEncryptionLevel] = useState<"standard" | "enhanced" | "quantum">("enhanced");
const [btcUsd, setBtcUsd] = useState<number | null>(null);
const [txidInput, setTxidInput] = useState("");
const [verifyMsg, setVerifyMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
const [verifyBusy, setVerifyBusy] = useState(false);
const [payError, setPayError] = useState<string | null>(null);
const [orderComplete, setOrderComplete] = useState(false);
const { usdStoreCredit, spendUsdStoreCredit, verifyBtcDeposit } = useWallet();
useEffect(() => {
let cancelled = false;
(async () => {
try {
const res = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
);
const j = (await res.json()) as { bitcoin?: { usd?: number } };
const p = j.bitcoin?.usd;
if (!cancelled && p && Number.isFinite(p)) setBtcUsd(p);
} catch {
if (!cancelled) setBtcUsd(96000);
}
})();
return () => {
cancelled = true;
};
}, []);
const spotBtc = btcUsd ?? 96000;
const subtotalUsd = useMemo(
() =>
Math.round(
lines.reduce(
(sum, l) => sum + estimateUsdForCryptoAmount(l.price, l.currency, spotBtc) * l.qty,
0,
) * 100,
) / 100,
[lines, spotBtc],
);
const encryptionFeeUsd = useMemo(() => {
const rate = btcUsd ?? 96000;
return Math.round(ENCRYPTION_BTC_FEE[encryptionLevel] * rate * 100) / 100;
}, [encryptionLevel, btcUsd]);
const orderTotalUsd = useMemo(
() => Math.round((subtotalUsd + encryptionFeeUsd) * 100) / 100,
[subtotalUsd, encryptionFeeUsd],
);
const orderTotalBtc = useMemo(() => {
const r = btcUsd && btcUsd > 0 ? btcUsd : spotBtc;
if (!r || r <= 0) return null;
return Math.round((orderTotalUsd / r) * 1e8) / 1e8;
}, [orderTotalUsd, btcUsd, spotBtc]);
const merchantAddr = getMerchantBtcAddress();
const merchantReady = isMerchantBtcConfigured();
const btcPayUri = useMemo(() => {
if (!merchantReady || orderTotalBtc == null) return "";
return `bitcoin:${merchantAddr}?amount=${orderTotalBtc}`;
}, [merchantReady, merchantAddr, orderTotalBtc]);
const handleVerify = useCallback(async () => {
setVerifyMsg(null);
setVerifyBusy(true);
try {
const result = await verifyBtcDeposit(txidInput.trim());
if (result.ok) {
setVerifyMsg({
kind: "ok",
text: `Confirmed. Added $${result.creditedUsd?.toFixed(2) ?? "0.00"} USD to your spendable balance.`,
});
setTxidInput("");
} else {
setVerifyMsg({ kind: "err", text: result.error || "Verification failed" });
}
} finally {
setVerifyBusy(false);
}
}, [txidInput, verifyBtcDeposit]);
const tryCompleteOrder = () => {
setPayError(null);
if (paymentMethod === "crypto") {
if (!spendUsdStoreCredit(orderTotalUsd)) {
setPayError(
`Insufficient USD balance. You need $${orderTotalUsd.toFixed(2)}. Send Bitcoin to the address below, wait for at least one confirmation, then paste the transaction ID and click Verify.`,
);
return;
}
}
setOrderComplete(true);
clearCart();
};
const steps = [
{ number: 1, title: "Cart", description: "Review items" },
{ number: 2, title: "Payment", description: "Choose method" },
{ number: 3, title: "Encryption", description: "Select privacy level" },
{ number: 4, title: "Confirmation", description: "Complete ritual" },
];
const handleNext = () => {
if (step === 1 && lines.length === 0) return;
if (step < 4) setStep(step + 1);
};
const handlePrev = () => {
if (step > 1) setStep(step - 1);
};
return (
<div className="glass mx-auto max-w-4xl rounded-3xl border border-white/10 p-8 md:p-12">
<div className="mb-12">
<div className="flex justify-between">
{steps.map((s) => (
<div key={s.number} className="flex flex-col items-center">
<div
className={`flex h-12 w-12 items-center justify-center rounded-full border-2 text-lg font-bold ${step >= s.number
? "border-neon-cyan bg-neon-cyan/20 text-neon-cyan"
: "border-white/20 text-foreground/40"
}`}
>
{s.number}
</div>
<div className="mt-2 text-center">
<div className="text-sm font-medium">{s.title}</div>
<div className="text-xs text-foreground/50">{s.description}</div>
</div>
</div>
))}
</div>
<div className="relative -top-6 -z-10 h-1 bg-white/10">
<div
className="h-1 bg-gradient-to-r from-neon-cyan to-neon-purple transition-all duration-500"
style={{ width: `${((step - 1) / 3) * 100}%` }}
/>
</div>
</div>
<div className="mb-10">
{step === 1 && (
<div>
<h3 className="mb-6 font-orbitron text-2xl font-bold">YOUR CART</h3>
{!cartHydrated ? (
<p className="text-sm text-foreground/50">Loading cart</p>
) : lines.length === 0 ? (
<div className="rounded-xl border border-dashed border-white/15 p-10 text-center">
<p className="text-foreground/70">Your cart is empty.</p>
<p className="mt-2 text-sm text-foreground/50">Add SKUs from the hub or candy market.</p>
<Link
href="/market"
className="mt-6 inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-3 text-sm font-bold text-background"
>
Browse catalog
</Link>
</div>
) : (
<div className="space-y-4">
{lines.map((item) => {
const unitUsd = estimateUsdForCryptoAmount(item.price, item.currency, spotBtc);
const lineUsd = Math.round(unitUsd * item.qty * 100) / 100;
return (
<div
key={item.productId}
className="flex flex-col gap-4 rounded-xl bg-white/5 p-4 sm:flex-row sm:items-center sm:justify-between"
>
<div className="min-w-0 flex-1">
<div className="font-medium">{item.name}</div>
<div className="mt-1 font-mono text-xs text-foreground/50">
{item.price} {item.currency} each · ${unitUsd.toFixed(2)} USD @ spot
</div>
<div className="mt-3 flex flex-wrap items-center gap-2">
<span className="text-xs text-foreground/50">Qty</span>
<button
type="button"
className="rounded border border-white/15 px-2 py-0.5 text-sm hover:bg-white/10"
onClick={() => setLineQty(item.productId, item.qty - 1)}
aria-label="Decrease quantity"
>
</button>
<span className="w-8 text-center font-mono text-sm">{item.qty}</span>
<button
type="button"
className="rounded border border-white/15 px-2 py-0.5 text-sm hover:bg-white/10"
onClick={() => setLineQty(item.productId, item.qty + 1)}
aria-label="Increase quantity"
>
+
</button>
<button
type="button"
className="ml-2 text-xs text-red-400 hover:underline"
onClick={() => removeLine(item.productId)}
>
Remove
</button>
</div>
</div>
<div className="text-right sm:shrink-0">
<div className="font-orbitron text-xl text-neon-cyan">${lineUsd.toLocaleString()}</div>
<div className="text-xs text-foreground/50">USD est.</div>
</div>
</div>
);
})}
</div>
)}
{lines.length > 0 ? (
<div className="mt-8 flex justify-between border-t border-white/10 pt-6 text-xl">
<span>Subtotal (USD est.)</span>
<span className="font-orbitron font-bold text-neon-cyan">
${subtotalUsd.toLocaleString()}
</span>
</div>
) : null}
</div>
)}
{step === 2 && (
<div>
<h3 className="mb-6 font-orbitron text-2xl font-bold">SELECT PAYMENT METHOD</h3>
<div className="mb-6 rounded-xl border border-neon-green/30 bg-neon-green/5 px-4 py-3 text-sm">
<span className="font-bold text-neon-green">USD balance: </span>
<span className="font-orbitron">${usdStoreCredit.toFixed(2)}</span>
<span className="text-foreground/60">
{" "}
use after Bitcoin deposit is verified (on-chain).
</span>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<button
type="button"
className={`glass rounded-2xl border p-6 text-center transition-all ${paymentMethod === "crypto" ? "border-neon-cyan ring-2 ring-neon-cyan/30" : "border-white/10"
}`}
onClick={() => setPaymentMethod("crypto")}
>
<div className="mb-4 text-4xl"></div>
<div className="font-bold">Cryptocurrency</div>
<div className="mt-2 text-sm text-foreground/60">Bitcoin checkout balance</div>
</button>
<button
type="button"
className={`glass rounded-2xl border p-6 text-center transition-all ${paymentMethod === "guest" ? "border-neon-cyan ring-2 ring-neon-cyan/30" : "border-white/10"
}`}
onClick={() => setPaymentMethod("guest")}
>
<div className="mb-4 text-4xl">👤</div>
<div className="font-bold">Guest Checkout</div>
<div className="mt-2 text-sm text-foreground/60">Demo no USD charge</div>
</button>
<button
type="button"
className={`glass rounded-2xl border p-6 text-center transition-all ${paymentMethod === "card" ? "border-neon-cyan ring-2 ring-neon-cyan/30" : "border-white/10"
}`}
onClick={() => setPaymentMethod("card")}
>
<div className="mb-4 text-4xl">💳</div>
<div className="font-bold">Card (Secure)</div>
<div className="mt-2 text-sm text-foreground/60">Demo no USD charge</div>
</button>
</div>
{paymentMethod === "crypto" && (
<div className="mt-8">
<label className="mb-2 block font-medium">Select Cryptocurrency</label>
<div className="flex gap-4">
{(["BTC", "ETH", "XMR"] as const).map((coin) => (
<button
key={coin}
type="button"
className={`glass flex-1 rounded-xl border py-4 ${cryptoType === coin ? "border-neon-cyan bg-neon-cyan/10" : "border-white/10"
}`}
onClick={() => setCryptoType(coin)}
>
<div className="text-2xl">{coin === "BTC" ? "₿" : coin === "ETH" ? "Ξ" : "⏣"}</div>
<div className="font-bold">{coin}</div>
</button>
))}
</div>
{cryptoType !== "BTC" && (
<p className="mt-4 rounded-lg bg-white/5 p-4 text-sm text-foreground/70">
Deposits that add USD balance use <strong>Bitcoin</strong> only. Switch to BTC to see the
deposit address. (ETH/XMR shown for display.)
</p>
)}
{cryptoType === "BTC" && (
<div className="mt-6 space-y-4 rounded-xl bg-white/5 p-4">
<div className="text-sm text-foreground/60">Estimated order total after encryption step</div>
<div className="font-orbitron text-2xl text-neon-cyan">
${orderTotalUsd.toLocaleString()} USD
{orderTotalBtc != null && btcUsd != null && (
<span className="ml-3 text-lg text-foreground/70">
( {orderTotalBtc} BTC @ ${btcUsd.toLocaleString()}/BTC)
</span>
)}
</div>
<p className="text-xs text-foreground/50">
Final total includes the encryption add-on you pick in the next step. Fund your balance
with <strong>any amount</strong> of BTC; verified value is credited in USD at spot rate.
You can pay this order once your balance covers the total.
</p>
</div>
)}
</div>
)}
{paymentMethod === "guest" && (
<div className="mt-8">
<label className="mb-2 block font-medium">Email for receipt (optional)</label>
<input
type="email"
className="glass w-full rounded-xl border border-white/10 bg-transparent px-4 py-3"
placeholder="anonymous@example.com"
value={guestEmail}
onChange={(e) => setGuestEmail(e.target.value)}
/>
<p className="mt-2 text-sm text-foreground/60">
Guest checkout is a demo and does not charge your USD balance.
</p>
</div>
)}
<div className="mt-10 border-t border-white/10 pt-8">
<h4 className="mb-4 font-orbitron text-lg font-bold text-neon-cyan">Bitcoin deposit (all checkouts)</h4>
{!merchantReady ? (
<p className="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>, then restart the app.
</p>
) : (
<>
<p className="mb-3 text-sm text-foreground/70">
Send Bitcoin to this address. After the network confirms your payment (at least one
block), paste the transaction ID to add the equivalent USD to your balance.
</p>
<div className="rounded-xl border border-white/10 bg-black/40 p-4 font-mono text-sm break-all">
{merchantAddr}
</div>
{btcPayUri && (
<div className="mt-4 flex flex-col gap-4 md:flex-row md:items-start">
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(btcPayUri)}`}
alt="Bitcoin payment QR"
width={160}
height={160}
className="rounded-lg border border-white/10"
/>
<div className="text-xs text-foreground/50">
QR encodes a suggested payment URI for the current cart estimate (you may send any
amount; all verified BTC to this address becomes USD credit).
</div>
</div>
)}
<div className="mt-6 space-y-2">
<label className="text-sm font-medium">Transaction ID (after confirmed)</label>
<input
className="glass w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-sm"
placeholder="64-character txid"
value={txidInput}
onChange={(e) => setTxidInput(e.target.value.replace(/\s+/g, ""))}
/>
<button
type="button"
disabled={verifyBusy}
onClick={() => void handleVerify()}
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-2 text-sm font-bold text-background disabled:opacity-40"
>
{verifyBusy ? "Verifying…" : "Verify & add USD credit"}
</button>
{verifyMsg && (
<p
className={`text-sm ${verifyMsg.kind === "ok" ? "text-neon-green" : "text-red-400"}`}
>
{verifyMsg.text}
</p>
)}
</div>
</>
)}
</div>
</div>
)}
{step === 3 && (
<div>
<h3 className="mb-6 font-orbitron text-2xl font-bold">ENCRYPTION LEVEL</h3>
<p className="mb-8 text-foreground/70">
Choose how securely your data is protected. Higher levels add to your order total in USD.
</p>
<div className="space-y-4">
{[
{ level: "standard" as const, name: "Stealth Encryption", desc: "Militarygrade encryption, basic anonymity", icon: "🕵️" },
{ level: "enhanced" as const, name: "Ghost Protocol", desc: "Postquantum cryptography, zerotrace", icon: "👻" },
{ level: "quantum" as const, name: "ZeroTrace", desc: "Experimental, quantumentangled proofs", icon: "⚛️" },
].map((opt) => (
<button
key={opt.level}
type="button"
className={`glass flex w-full items-center justify-between rounded-2xl border p-6 text-left ${encryptionLevel === opt.level ? "border-neon-cyan ring-2 ring-neon-cyan/30" : "border-white/10"
}`}
onClick={() => setEncryptionLevel(opt.level)}
>
<div>
<div className="flex items-center gap-4">
<span className="text-3xl">{opt.icon}</span>
<div>
<div className="font-bold">{opt.name}</div>
<div className="text-sm text-foreground/60">{opt.desc}</div>
</div>
</div>
</div>
<div className="text-right font-orbitron text-xl font-bold text-neon-cyan">
{opt.level === "standard"
? "FREE"
: `+$${(Math.round(ENCRYPTION_BTC_FEE[opt.level] * (btcUsd ?? 96000) * 100) / 100).toFixed(2)}`}
</div>
</button>
))}
</div>
<div className="mt-8 rounded-xl bg-gradient-to-r from-neon-cyan/10 to-neon-purple/10 p-6">
<div className="font-orbitron font-bold">Order total so far</div>
<div className="mt-2 text-2xl text-neon-cyan">
${orderTotalUsd.toLocaleString()} USD
</div>
</div>
</div>
)}
{step === 4 && !orderComplete && (
<div className="text-center">
<h3 className="mb-4 font-orbitron text-3xl font-bold">COMPLETE PAYMENT</h3>
<div className="glass mx-auto mb-8 max-w-md rounded-2xl border border-white/10 p-6 text-left">
<div className="flex justify-between text-sm">
<span className="text-foreground/60">Total due</span>
<span className="font-orbitron font-bold text-neon-cyan">
${orderTotalUsd.toLocaleString()}
</span>
</div>
<div className="mt-2 flex justify-between text-sm">
<span className="text-foreground/60">Your USD balance</span>
<span className="font-orbitron">${usdStoreCredit.toFixed(2)}</span>
</div>
<div className="mt-2 flex justify-between text-sm">
<span className="text-foreground/60">Payment path</span>
<span className="font-bold">
{paymentMethod === "crypto" ? "Crypto (USD balance)" : "Demo (guest/card)"}
</span>
</div>
</div>
{payError && <p className="mx-auto mb-6 max-w-xl text-sm text-red-400">{payError}</p>}
<button
type="button"
onClick={tryCompleteOrder}
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-10 py-3 font-bold text-background"
>
{paymentMethod === "crypto" ? "Charge my USD balance & complete" : "Complete demo order"}
</button>
{paymentMethod === "crypto" && (
<p className="mx-auto mt-6 max-w-xl text-xs text-foreground/50">
Crypto checkout pulls from your verified USD balance only. Send BTC on the payment step and
verify the txid so your balance matches the total above.
</p>
)}
</div>
)}
{step === 4 && orderComplete && (
<div className="text-center">
<div className="mb-8">
<div className="inline-flex h-24 w-24 items-center justify-center rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple">
<span className="text-4xl"></span>
</div>
</div>
<h3 className="mb-4 font-orbitron text-3xl font-bold">RITUAL COMPLETE</h3>
<p className="mx-auto max-w-2xl text-foreground/70">
Your order is now encrypted with {encryptionLevel} protection. A unique digital artwork receipt
has been generated and stored in your vault.
</p>
<div className="my-10">
<div className="glass mx-auto max-w-md rounded-2xl border border-white/10 p-6">
<div className="font-orbitron text-2xl font-bold text-neon-cyan">ORDER #CYBR9A2FB8E1</div>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div className="text-left text-foreground/60">Amount</div>
<div className="text-right font-bold">${orderTotalUsd.toLocaleString()} USD</div>
<div className="text-left text-foreground/60">Privacy</div>
<div className="text-right font-bold">{encryptionLevel.toUpperCase()}</div>
<div className="text-left text-foreground/60">Payment</div>
<div className="truncate text-right font-mono text-neon-green">
{paymentMethod === "crypto" ? "USD balance (BTC-funded)" : "Demo guest/card"}
</div>
</div>
</div>
</div>
<p className="text-sm text-foreground/50">
Your digital receipt is available in your vault. Thank you for choosing CyberLux.
</p>
</div>
)}
</div>
<div className="flex justify-between">
<button
type="button"
className="glass rounded-full px-8 py-3 font-medium disabled:opacity-30"
onClick={handlePrev}
disabled={step === 1 || (step === 4 && orderComplete)}
>
Previous
</button>
<div className="flex items-center gap-4">
{step < 4 ? (
<button
type="button"
disabled={step === 1 && lines.length === 0}
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-3 font-bold text-background disabled:opacity-40"
onClick={handleNext}
>
Continue
</button>
) : orderComplete ? (
<button
type="button"
className="rounded-full bg-gradient-to-r from-neon-green to-neon-cyan px-10 py-3 font-bold text-background"
>
ENTER THE VAULT
</button>
) : null}
</div>
</div>
</div>
);
};
export default CheckoutFlow;