// library-page.jsx — where a buyer redeems an access code and downloads what their tier includes. // Gating is client-side (access.js); see the limits noted there. const QUIZ_KEY = 'vc_quiz_v1'; function readQuiz() { try { return JSON.parse(localStorage.getItem(QUIZ_KEY) || '{}') || {}; } catch (e) { return {}; } } function V4Library() { const v4 = window.v4; const accent = v4.yellow; const access = window.useVCAccess(); const [busy, setBusy] = React.useState(''); const [err, setErr] = React.useState(''); const has = !!access.product; const quiz = readQuiz(); const chapters = (window.VC_QUIZ && window.VC_QUIZ.chapters) || []; const passed = chapters.filter(c => quiz[c.n] && quiz[c.n].passed).length; React.useEffect(() => { if (location.hash === '#code') { const el = document.getElementById('code'); if (el) setTimeout(() => el.scrollIntoView({ block: 'start' }), 60); } }, []); // Back from Stripe Checkout: library.html?session_id=cs_… → ask the server for the code, then unlock this browser. const sessionId = new URLSearchParams(location.search).get('session_id'); const [claim, setClaim] = React.useState(sessionId ? { status: 'loading' } : null); const [copied, setCopied] = React.useState(false); React.useEffect(() => { if (!sessionId) return; let alive = true; (async () => { try { const r = await fetch('/api/claim?session_id=' + encodeURIComponent(sessionId), { cache: 'no-store' }); const j = await r.json().catch(() => ({ ok: false, reason: 'The server answered in an unexpected way.' })); if (!j.ok) { if (alive) setClaim({ status: 'error', reason: j.reason }); return; } const v = await window.VCAccess.redeem(j.code); // Always hand over the code the server issued, even if this browser could not use it. if (alive) setClaim({ status: 'ok', code: j.code, name: j.name, receipt: j.receipt, unlocked: v.ok, why: v.ok ? null : v.reason }); } catch (e) { if (alive) setClaim({ status: 'error', reason: 'We could not reach the server. Reload this page to try again.' }); } })(); return () => { alive = false; }; }, []); const copy = (text) => { const done = () => { setCopied(true); setTimeout(() => setCopied(false), 2500); }; const fallback = () => { const t = document.createElement('textarea'); t.value = text; document.body.appendChild(t); t.select(); try { document.execCommand('copy'); done(); } catch (e) {} t.remove(); }; if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, fallback); else fallback(); }; const contact = window.VC_CONTACT_EMAIL; const get = async (id) => { setErr(''); setBusy(id); try { await window.VCWorkbooks.download(id); } catch (e) { setErr(e.message || 'Download failed.'); } setBusy(''); }; const kicker = (t, c) =>
§ {t}
; const btn = (bg, color, extra) => ({ display: 'inline-block', background: bg, color, border: `1.5px solid ${v4.ink}`, padding: '11px 14px', fontFamily: v4.mono, fontSize: 11.5, fontWeight: 700, letterSpacing: 1.5, textTransform: 'uppercase', textDecoration: 'none', cursor: 'pointer', ...extra }); return (
{claim ? (
{claim.status === 'loading' ? (
Confirming your payment with Stripe…
) : claim.status === 'ok' ? ( <>
§ Payment received
{claim.unlocked ? <>Thank you. {claim.name} is unlocked in this browser. : <>Thank you. Here is your {claim.name} code.}
{!claim.unlocked ?
This browser could not use it ({claim.why}) Try pasting it into the Library page in another browser, or email us.
: null}
Save your access code somewhere safe, such as a password manager. Paste it into the Library page to read on another browser or device.{claim.receipt ? <> Stripe sent your receipt to {claim.receipt}. : null}
{claim.code}
This page’s address can show the code again, so keep it private.
) : ( <>
§ We could not issue your code
{claim.reason}
If you were charged, email {contact ? {contact} : 'us at the support address on your Stripe receipt'} with the receipt and we will send your code by hand.
)}
) : null}
{kicker('Library')}

{has ? <>Your
library. : <>Unlock
your copy.}

{has ? <>You have {access.name}{access.until ? <> until {access.until} : <>, with no expiry}. Everything it includes is below. Downloads are built in your browser. : <>Paste the access code from your purchase email. It is checked in this browser and kept here; there is no account. Use the same code on each browser you read on.}

{!has ? No code yet? See pricing → : null}
Lost your code? Email {contact ? {contact} : us} with your Stripe receipt and we will send it again.
{kicker(has ? 'This browser' : 'Enter your code', accent)} {has ? ( <>
{access.name}
{access.until ? `Access until ${access.until}` : 'No expiry'}
Have a different code? Paste it here; it replaces this one.
) : }
{/* workbooks */}
{kicker('Excel workbooks · live formulas')}
{window.VC_WORKBOOKS.map((w, i) => { const ok = has && window.VCAccess.workbookAllowed(w.tier); return (
2 ? `1px solid ${v4.lineSoft}` : 'none', background: ok ? v4.paper : 'transparent', display: 'flex', flexDirection: 'column', gap: 8 }}> {w.cover ? : null}
{w.tier === 'deal' ? 'Deal Pack · Complete' : 'Complete only'}
{w.t}
{w.d}
{ok ? : {has ? 'In Complete →' : 'Locked · see pricing →'}}
); })}
{err ?
{err}
: null}
Yellow cells are inputs; everything else is a formula. Opens in Excel, Numbers, LibreOffice and Google Sheets.
{/* calculator + lessons */}
{kicker('Earnings-bridge calculator', v4.ink)}
Run a deal’s numbers, then export the one-page PDF and the Excel.
{has ? 'Exports are unlocked in this browser.' : 'Results are free on screen; exports come with either paid tier.'}
Open the calculator →
{kicker('Lessons')} {chapters.map(c => ( {c.roman} {c.t} {c.n === 1 ? 'FREE' : has && access.lessons ? 'UNLOCKED' : 'FIRST LESSON FREE'} · {c.mins} MIN ))}
{/* quizzes + records */}
{kicker('Chapter quizzes and completion records', accent)} {access.quiz ? ( <>
Five questions per chapter; four correct passes. Each pass gives a printable record with the chapter’s reading time. Passed {passed} of {chapters.length}.
{chapters.map((c, i) => { const q = quiz[c.n]; return (
{q && q.passed ? `✓ PASSED ${q.score}/5` : q ? `LAST ${q.score}/5` : 'NOT TAKEN'}
{c.roman} · {c.t}
{q && q.passed ? 'Record · retake →' : 'Take the quiz →'}
); })}
WHOLE COURSE
{passed === chapters.length ? 'Course record →' : `Pass all ${chapters.length} to unlock`}
Results are kept in this browser. Records are self-issued and not accredited; check your professional body’s CPD rules.
) : (
Quizzes and completion records come with Complete. {has ? See Complete → : See pricing →}
)}
); } window.V4Library = V4Library;