// calc-app.jsx — shell + the live purchase flow. // hero → quote (build / budget) → "Make this video": // • anonymous → signup gate → real /login?next=/calculator // • signed-in → POST /api/calculator/create-checkout → hosted Stripe redirect // Stripe success_url returns to /calculator?purchase= → poll purchase-status // → render fires server-side (webhook). Stripe is in TEST mode here (no real money). const CALC_TWEAK_DEFAULTS = { stage: "hero", returningBuyer: true, avatar: "twin", length: "60s", cinematic: false, margin: 3, quoteMode: "build", }; function App() { const [t, setTweak] = useTweaks(CALC_TWEAK_DEFAULTS); const [sel, setSel] = React.useState({ avatar: t.avatar, length: t.length, style: t.cinematic ? 'cinematic' : 'talking', voice: t.avatar === 'twin' ? 'cloned' : 'stock', music: false, script: '', }); const set = (patch) => setSel((s) => ({ ...s, ...patch })); const [modal, setModal] = React.useState(null); // null | 'gate' | 'upsell' const [budget, setBudget] = React.useState(30); const [purchase, setPurchase] = React.useState(null); // {session_id, status, video_id} const effectiveSel = t.quoteMode === 'budget' ? { ...window.optimizeForBudget(budget, t.margin).sel, script: sel.script } : sel; React.useEffect(() => { if (t.quoteMode === 'budget') setSel((s) => ({ ...window.optimizeForBudget(budget, t.margin).sel, script: s.script })); }, [t.quoteMode, budget, t.margin]); // Stripe return: /calculator?purchase= → land on the "rendering" screen + poll. React.useEffect(() => { const params = new URLSearchParams(window.location.search); const sess = params.get('purchase'); if (sess) { setTweak('stage', 'done'); setPurchase({ session_id: sess, status: 'paid' }); } }, []); // Auto-resume after login: an anonymous "Make this video" stashes the selection and // sends the visitor to /login; on return we re-attempt checkout automatically — a 401 // means still-anonymous (no-op, normal page), a {url} means now-authed → straight to Stripe. const [resuming, setResuming] = React.useState(false); React.useEffect(() => { const params = new URLSearchParams(window.location.search); if (params.get('purchase')) return; // Stripe-return flow, not a resume let stored = null; try { stored = JSON.parse(localStorage.getItem('calc_resume') || 'null'); } catch (e) {} if (!stored || !stored.sel) return; if (Date.now() - (stored.ts || 0) > 15 * 60 * 1000) { localStorage.removeItem('calc_resume'); return; } setResuming(true); (async () => { try { const r = await fetch('/api/calculator/create-checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ selection: stored.sel }), }); if (r.status === 401) { setResuming(false); return; } // still anonymous → leave page normal const d = await r.json(); if (d.url) { localStorage.removeItem('calc_resume'); window.location.href = d.url; return; } localStorage.removeItem('calc_resume'); setResuming(false); } catch (e) { setResuming(false); } })(); }, []); React.useEffect(() => { if (!purchase || !purchase.session_id) return; if (purchase.status === 'ready' || purchase.status === 'render_failed') return; const iv = setInterval(async () => { try { const r = await fetch('/api/calculator/purchase-status?session_id=' + encodeURIComponent(purchase.session_id)); if (r.ok) { const d = await r.json(); setPurchase((p) => ({ ...p, status: d.status, video_id: d.video_id })); } } catch (e) { /* keep polling */ } }, 4000); return () => clearInterval(iv); }, [purchase]); const v30 = window.quotePrice({ avatar: 'ready', length: '30s', style: 'talking', voice: 'stock', music: false }, t.margin).render; const priorHistory = t.returningBuyer ? [v30, v30] : []; const stage = t.stage; // 'hero' | 'quote' | 'done' const goQuote = () => { setTweak('stage', 'quote'); setModal(null); window.scrollTo({ top: 0, behavior: 'smooth' }); }; // "Make this video" — auth-aware live checkout. const onBuy = async () => { try { const r = await fetch('/api/calculator/create-checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ selection: effectiveSel }), }); if (r.status === 401) { // anonymous → stash selection, show gate try { localStorage.setItem('calc_resume', JSON.stringify({ sel: effectiveSel, ts: Date.now() })); } catch (e) {} setModal('gate'); return; } const d = await r.json(); if (d.url) { window.location.href = d.url; return; } // signed-in → Stripe setModal('gate'); } catch (e) { setModal('gate'); } }; // The gate's CTA → the real login/signup wall, returning to the calculator. const goLogin = () => { window.location.href = '/login?next=/calculator'; }; const sampleSel = { avatar: 'twin', length: '60s', style: 'talking', voice: 'cloned', music: false, script: '' }; const purchaseHistory = [...priorHistory, window.quotePrice(effectiveSel, t.margin).render]; const renderStatus = purchase ? purchase.status : null; const failed = renderStatus === 'render_failed'; if (resuming) { return (
Taking you to checkout…
Picking up where you left off.
); } return (
{stage === 'hero' ? ( ) : stage === 'done' ? (

{failed ? "We hit a snag" : "Your video is being made"}

{failed ? "Your payment went through but the render didn't start. We've been alerted and will sort it — you won't be charged again." : "Payment received. It'll be ready in a few minutes — we'll email you, and it'll be waiting in your account."}

See it in My Videos {purchaseHistory.length >= 3 ? : null}
) : ( setTweak('quoteMode', m)} budget={budget} setBudget={setBudget} /> )} {modal === 'gate' ? setModal(null)} onSignup={goLogin} margin={t.margin} /> : null} {modal === 'upsell' ? setModal(null)} onSubscribe={() => { window.location.href = '/pricing'; }} /> : null}
); } function CalcFooter() { return (
MyAvatar
One-off videos · no subscription required · prices exclude VAT, added at checkout for your country.
© MyAvatar
); } ReactDOM.createRoot(document.getElementById('root')).render();