// checkout.jsx — Buy button + modal with card form. Validates locally and // transitions to a success state. No real payment is processed. function luhnOk(num) { const s = String(num).replace(/\s+/g, ""); if (!/^\d{12,19}$/.test(s)) return false; let sum = 0, alt = false; for (let i = s.length - 1; i >= 0; i--) { let n = +s[i]; if (alt) { n *= 2; if (n > 9) n -= 9; } sum += n; alt = !alt; } return sum % 10 === 0; } function formatCardNumber(v) { return v.replace(/\D/g, "").slice(0, 19).replace(/(\d{4})(?=\d)/g, "$1 "); } function formatExp(v) { const d = v.replace(/\D/g, "").slice(0, 4); if (d.length <= 2) return d; return d.slice(0, 2) + "/" + d.slice(2); } function BuyButton({ label = "Buy ThumbForge", className = "btn primary xl" }) { const [open, setOpen] = React.useState(false); return ( <> setOpen(true)}> {label} → {open && setOpen(false)} />} > ); } function CheckoutModal({ onClose }) { const [email, setEmail] = React.useState(""); const [name, setName] = React.useState(""); const [card, setCard] = React.useState(""); const [exp, setExp] = React.useState(""); const [cvc, setCvc] = React.useState(""); const [zip, setZip] = React.useState(""); const [step, setStep] = React.useState("form"); // form | processing | done const [touched, setTouched] = React.useState({}); // read price from data-price-num (synced w/ tweaks) const priceEl = typeof document !== "undefined" ? document.querySelector("[data-price-num]") : null; const price = priceEl ? +priceEl.textContent : 29; const errs = { email: !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) ? "Enter a valid email." : "", name: name.trim().length < 2 ? "Required." : "", card: !luhnOk(card) ? "Card number doesn't look right." : "", exp: !/^\d{2}\/\d{2}$/.test(exp) ? "MM/YY" : "", cvc: !/^\d{3,4}$/.test(cvc) ? "3–4 digits" : "", zip: zip.trim().length < 3 ? "Required." : "", }; const valid = Object.values(errs).every(e => !e); function submit(e) { e.preventDefault(); setTouched({email:1,name:1,card:1,exp:1,cvc:1,zip:1}); if (!valid) return; setStep("processing"); setTimeout(() => setStep("done"), 1400); } function onScrim(e) { if (e.target === e.currentTarget) onClose(); } React.useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = ""; }; }, [onClose]); function F({k, label, children}) { const showErr = touched[k] && errs[k]; return ( {label} {showErr && — {errs[k]}} {children} ); } return ( {step === "form" && ( Order · one-time purchase × ThumbForgefor Windows Single-seat license · lifetime Includes one year of updates · email support ${price} USD setEmail(e.target.value)} onBlur={()=>setTouched(t=>({...t,email:1}))} placeholder="you@example.com" autoFocus /> setName(e.target.value)} onBlur={()=>setTouched(t=>({...t,name:1}))} placeholder="Jordan Maker" /> setCard(formatCardNumber(e.target.value))} onBlur={()=>setTouched(t=>({...t,card:1}))} placeholder="4242 4242 4242 4242" inputMode="numeric" /> setExp(formatExp(e.target.value))} onBlur={()=>setTouched(t=>({...t,exp:1}))} placeholder="MM/YY" inputMode="numeric" /> setCvc(e.target.value.replace(/\D/g,"").slice(0,4))} onBlur={()=>setTouched(t=>({...t,cvc:1}))} placeholder="123" inputMode="numeric" /> setZip(e.target.value.slice(0,10))} onBlur={()=>setTouched(t=>({...t,zip:1}))} placeholder="94110" /> Pay ${price} and download Payments are processed in test mode for this demo — no card is charged. )} {step === "processing" && ( PROCESSING PAYMENT Authorizing card · generating license key… )} {step === "done" && ( ✓ Thanks, you're set. We've sent a download link and your license key to {email}. The installer is ~28 MB. LICENSE KEY TF-{Math.random().toString(36).slice(2,6).toUpperCase()}-{Math.random().toString(36).slice(2,6).toUpperCase()}-{Math.random().toString(36).slice(2,6).toUpperCase()} Download ThumbForge Close )} ); } window.BuyButton = BuyButton;
Payments are processed in test mode for this demo — no card is charged.
Authorizing card · generating license key…
We've sent a download link and your license key to {email}. The installer is ~28 MB.