Capture the current CyberLux UI, commerce, messaging, and Tor ops updates so local main can be pushed to the remote. Made-with: Cursor
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { useAccount } from "@/contexts/AccountContext";
|
|
import { useWallet } from "@/contexts/WalletContext";
|
|
|
|
type Props = {
|
|
/** USD amount to charge (2 decimals) */
|
|
amountUsd: number;
|
|
/** Receipt / vault title */
|
|
title: string;
|
|
description?: string;
|
|
className?: string;
|
|
size?: "sm" | "md";
|
|
/** Override LUX granted (default: based on USD amount) */
|
|
luxBonus?: number;
|
|
/** Called after a successful charge */
|
|
onSuccess?: () => void;
|
|
};
|
|
|
|
/**
|
|
* Spend verified Bitcoin-funded USD balance without going through full checkout cart.
|
|
*/
|
|
export default function PayWithUsdBalanceButton({
|
|
amountUsd,
|
|
title,
|
|
description,
|
|
className = "",
|
|
size = "md",
|
|
luxBonus,
|
|
onSuccess,
|
|
}: Props) {
|
|
const { user } = useAccount();
|
|
const { usdStoreCredit, spendUsdStoreCredit, earnLuxCredits, addVaultReceipt } = useWallet();
|
|
const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
const safe = Math.round(amountUsd * 100) / 100;
|
|
const pad = size === "sm" ? "px-3 py-1.5 text-xs" : "px-5 py-2.5 text-sm";
|
|
|
|
const pay = () => {
|
|
setMsg(null);
|
|
if (!user) {
|
|
setMsg({ kind: "err", text: "Sign in to spend your USD balance." });
|
|
return;
|
|
}
|
|
if (!(safe > 0)) {
|
|
setMsg({ kind: "err", text: "Invalid amount." });
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
try {
|
|
if (!spendUsdStoreCredit(safe)) {
|
|
setMsg({
|
|
kind: "err",
|
|
text: `Need $${safe.toFixed(2)} — you have $${usdStoreCredit.toFixed(2)}. Add funds first.`,
|
|
});
|
|
return;
|
|
}
|
|
const lux =
|
|
luxBonus != null && Number.isFinite(luxBonus)
|
|
? Math.max(0, Math.floor(luxBonus))
|
|
: Math.min(500, Math.max(10, Math.floor(safe)));
|
|
earnLuxCredits(lux);
|
|
const oid =
|
|
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
? `PAY-${crypto.randomUUID().slice(0, 8).toUpperCase()}`
|
|
: `PAY-${Date.now().toString(36).toUpperCase()}`;
|
|
addVaultReceipt({
|
|
id: oid,
|
|
title,
|
|
desc: description ?? `${title} · $${safe.toFixed(2)} USD`,
|
|
date: new Date().toISOString().slice(0, 10),
|
|
severity: "normal",
|
|
});
|
|
setMsg({ kind: "ok", text: `Paid $${safe.toFixed(2)}. +${lux} LUX · receipt ${oid}` });
|
|
onSuccess?.();
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<button
|
|
type="button"
|
|
disabled={busy || safe <= 0}
|
|
onClick={pay}
|
|
className={`rounded-full border border-neon-cyan/40 bg-neon-cyan/15 font-bold text-neon-cyan transition hover:bg-neon-cyan/25 disabled:opacity-40 ${pad} ${className}`}
|
|
>
|
|
{busy ? "Processing…" : `Pay $${safe.toFixed(2)} with balance`}
|
|
</button>
|
|
{!user ? (
|
|
<p className="text-xs text-foreground/55">
|
|
<Link href="/sign-in" className="text-neon-cyan underline">
|
|
Sign in
|
|
</Link>{" "}
|
|
to use BTC-funded USD credit.
|
|
</p>
|
|
) : (
|
|
<p className="text-xs text-foreground/45">
|
|
Balance: <span className="font-orbitron text-neon-green">${usdStoreCredit.toFixed(2)}</span>
|
|
</p>
|
|
)}
|
|
{msg ? (
|
|
<p className={`text-xs ${msg.kind === "ok" ? "text-neon-green" : "text-red-400"}`}>{msg.text}</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|