/* BasedMTA — Checkout modal. Real flow: the backend creates a CryptoCloud invoice and returns a hosted payment URL. We never touch wallet addresses or on-chain detection in the browser — CryptoCloud's hosted page does that, then posts back to /v1/postback/cryptocloud (JWT-verified server-side), which mints the key and emails it. Steps: 1. Review order + email + accept terms 2. Creating invoice -> redirect to CryptoCloud hosted checkout API: POST /v1/checkout { email, tier_id, duration_days } -> { invoice_id, pay_url, price_usd_cents, expires_at } Served same-origin via Caddy reverse-proxy to the auth backend, so no CORS and no cross-origin secrets in the page. */ const { useState: cuState, useEffect: cuEffect } = React; // Frontend tier slug -> backend numeric tier_id. Verified against the // live /v1/pricing snapshot (Basic=1, Pro=2, VIP=3). The price the // customer is actually charged is recomputed server-side from this // tier_id + duration_days, so the displayed total can never diverge // from the invoice amount. const TIER_ID_BY_SLUG = { basic: 1, pro: 2, vip: 3 }; // Optional override for local dev; empty string == same-origin. const API_BASE = (typeof window !== "undefined" && window.BMTA_API_BASE) || ""; function CheckoutModal({ open, order, onClose }) { const [step, setStep] = cuState(1); const [email, setEmail] = cuState(""); const [emailErr, setEmailErr] = cuState(""); const [accept, setAccept] = cuState(true); const [submitting, setSubmitting] = cuState(false); const [fatalErr, setFatalErr] = cuState(""); cuEffect(() => { if (open) { setStep(1); setEmail(""); setEmailErr(""); setAccept(true); setSubmitting(false); setFatalErr(""); } }, [open]); cuEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === "Escape" && !submitting) onClose(); }; document.addEventListener("keydown", onKey); document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = ""; }; }, [open, onClose, submitting]); if (!open || !order) return null; const validEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v.trim()); // Base tier price for the selected duration. This is exactly what the // backend charges; add-ons (if any) are arranged separately via a // ticket and are NOT folded into the crypto invoice. const baseAmount = order.tier.durations[order.duration.id]; const tierID = TIER_ID_BY_SLUG[order.tier.id]; const goPay = async () => { if (!validEmail(email)) { setEmailErr("Enter a valid email so we can deliver your key."); return; } if (!accept) { setEmailErr("Please accept the terms of sale to continue."); return; } if (!tierID) { setFatalErr("This tier can't be purchased online yet — open a Discord ticket."); return; } setEmailErr(""); setFatalErr(""); setSubmitting(true); setStep(2); try { const resp = await fetch(API_BASE + "/v1/checkout", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ email: email.trim(), tier_id: tierID, duration_days: order.duration.days, }), }); const data = await resp.json().catch(() => ({})); if (!resp.ok) { let msg = "We couldn't start your payment. Please try again."; if (resp.status === 503) { msg = "Crypto payments are briefly unavailable. Please try again in a minute, or open a Discord ticket."; } else if (resp.status === 429) { msg = "Too many attempts. Wait a moment and try again."; } else if (data && data.error === "invalid email") { msg = "That email looks invalid. Please double-check it."; } else if (data && data.detail) { msg = String(data.detail); } setFatalErr(msg); setSubmitting(false); setStep(1); return; } const payURL = data && data.pay_url; if (!payURL || typeof payURL !== "string" || !/^https:\/\//i.test(payURL)) { setFatalErr("The payment processor did not return a valid checkout link. Please try again."); setSubmitting(false); setStep(1); return; } // Stash for an optional post-payment status check (key checker can // surface "we emailed your key"). Best-effort; ignore failures. try { localStorage.setItem("bmta_last_invoice", JSON.stringify({ invoice_id: data.invoice_id || null, email: email.trim(), tier: order.tier.name, duration: order.duration.label, ts: Date.now(), })); } catch (_) {} // Hand off to CryptoCloud's hosted, PCI-out-of-scope payment page. window.location.assign(payURL); } catch (err) { setFatalErr("Network error reaching the payment server. Check your connection and try again."); setSubmitting(false); setStep(1); } }; const hasAddons = order.addons && order.addons.length > 0; return (
{ if (!submitting) onClose(); }}>
e.stopPropagation()}>
B asedMTA
1} />
{step === 1 && (

Review your order

Your license key is delivered by email. We never share it, sell it, or send marketing.

{fatalErr && {fatalErr}}
)} {step === 2 && (

Opening secure checkout

Creating your ${baseAmount} USD invoice with CryptoCloud and redirecting you to their hosted payment page. Once it clears on-chain, your key is emailed to {email}.

ProcessorCryptoCloud
Order{order.tier.name} · {order.duration.label}
Amount${baseAmount} USD

If you are not redirected automatically, please don't refresh — open a Discord ticket and we'll sort it out.

)}
); } function CkStep({ n, label, active, done }) { return (
{done ? : n} {label}
); } window.CheckoutModal = CheckoutModal;