// Peach Codex — generation view. // // Builds the printed PDF Codex (Aztec barcodes + optional recovery words) by: // 1. Fetching a vault snapshot from the SW (GET_CODEX_VAULT_DATA). // 2. Constructing a CodexDeps the popup-side `generateCodex` can drive. // 3. Triggering a PDF download. // // Pro-gated: a non-Pro user is bounced to the license section on mount. // The actual barcode/PDF/erasure-coding work lives in src/popup/codex/* and // is owned by Subagents 1 and 2 — do not import internals from there beyond // the published contract. // // Behavioural notes: // - `getAllEntries` only feeds an entry-count filter on the SW; here we hand // back synthetic placeholders of length `entryCount` so the filter passes // through unchanged without leaking entries through the popup. // - Restoration is intentionally NOT gated by Pro (a printed Codex must stay // restorable forever, regardless of license state). import { createSignal, createMemo, onMount, Show } from "solid-js"; import { sendMessage } from "../../shared/bridge"; import { isNativePlatform } from "../../shared/native"; import type { CodexDeps, CodexError, CodexManifest, CodexVaultSnapshot, } from "../../shared/codex-types"; import { STORAGE_KEY_LAST_GENERATED } from "../../shared/codex-types"; import { generateCodex, estimateCodexPages } from "../codex/generate"; import type { VaultEntry } from "../../shared/types"; interface CodexGenerateViewProps { isPro: boolean; onBack: () => void; onUpgradePro: () => void; } export function CodexGenerateView(props: CodexGenerateViewProps) { const [snapshot, setSnapshot] = createSignal(null); const [estimatedPages, setEstimatedPages] = createSignal(null); const [loading, setLoading] = createSignal(true); const [loadError, setLoadError] = createSignal(null); const [includeRecoveryWords, setIncludeRecoveryWords] = createSignal(true); const [generating, setGenerating] = createSignal(false); const [genError, setGenError] = createSignal(null); const [completed, setCompleted] = createSignal(null); onMount(async () => { if (!props.isPro) { // Pro-gated: bounce immediately. Render nothing meaningful in the // meantime so the user does not flash see the form before redirect. props.onUpgradePro(); return; } const res = (await sendMessage({ type: "GET_CODEX_VAULT_DATA" as any, })) as { ok: boolean; data?: CodexVaultSnapshot; error?: string }; if (!res.ok || !res.data) { setLoadError(res.error ?? "could not load vault data"); setLoading(false); return; } setSnapshot(res.data); if (res.data.slotA) { try { const pages = await estimateCodexPages(res.data.slotA, res.data.slotB); setEstimatedPages(pages); } catch { // Estimation is informational; failure is non-fatal. setEstimatedPages(null); } } setLoading(false); }); // Description copy mirrors the spec — softens the toggle's consequence so // the user understands what a "no recovery words" Codex actually means. const toggleDescription = createMemo(() => includeRecoveryWords() ? "your 12 recovery words will be printed on the last page. anyone holding the paper can restore the vault." : "no recovery words on paper. you will need your master password to restore from this Codex.", ); function buildDeps(snap: CodexVaultSnapshot): CodexDeps { return { getSlotA: async () => snap.slotA, getSlotB: async () => snap.slotB, getLastModified: async () => snap.lastModified, // `getAllEntries` is consumed only as a count filter inside the // generator. We synthesise a list of the right length so we don't have // to surface real entries through the popup boundary. getAllEntries: async () => Array.from( { length: snap.entryCount }, () => ({ deletedAt: null, archived: false }) as unknown as VaultEntry, ), getRecoveryWords: async () => snap.recoveryWords, getVerifyCode: async () => snap.verifyCode, isVaultUnlocked: () => snap.vaultUnlocked, }; } function codexFilename(): string { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `peach-codex-${y}-${m}-${day}.pdf`; } function downloadPdf(bytes: Uint8Array): void { const a = document.createElement("a"); a.download = codexFilename(); if (isNativePlatform()) { // Capacitor WebView: blob: URLs are opaque to native, but data: URLs // are intercepted by MainActivity and written to public Downloads via // MediaStore. Encode in chunks to avoid blowing the call stack on large // PDFs. let bin = ""; const chunk = 0x8000; for (let i = 0; i < bytes.length; i += chunk) { const slice = bytes.subarray(i, Math.min(i + chunk, bytes.length)); bin += String.fromCharCode(...slice); } a.href = `data:application/pdf;base64,${btoa(bin)}`; } else { // Browser: copy into a fresh Uint8Array to ensure a tightly-owned // ArrayBuffer (Blob is happier with this than a subarray view). const owned = new Uint8Array(bytes); const blob = new Blob([owned.buffer], { type: "application/pdf" }); a.href = URL.createObjectURL(blob); setTimeout(() => URL.revokeObjectURL(a.href), 4000); } document.body.appendChild(a); a.click(); a.remove(); } function explainCodexError(code: string, message: string): string { switch (code) { case "VAULT_LOCKED": return "your vault is locked. unlock and try again."; case "VAULT_EMPTY": return "your vault is empty — nothing to print yet."; case "RECOVERY_WORDS_MISSING": return "your 12 recovery words could not be found. try toggling recovery words off."; case "RECOVERY_WORDS_INVALID": return "your recovery words look wrong. try toggling recovery words off."; case "PRO_REQUIRED": return "Codex generation is a Pro feature."; case "PDF_GENERATION_FAILED": case "AZTEC_GENERATION_FAILED": return "could not render the Codex. please try again."; case "COMPRESSION_ROUNDTRIP_FAILED": case "ERASURE_CODING_VERIFICATION_FAILED": case "ERASURE_CODING_CONTENT_MISMATCH": case "AZTEC_ROUNDTRIP_FAILED": case "AZTEC_CONTENT_MISMATCH": case "RECONSTRUCTION_SIMULATION_FAILED": case "RECONSTRUCTION_VERIFY_CODE_MISMATCH": case "RECONSTRUCTION_FINGERPRINT_MISMATCH": case "RECONSTRUCTION_COUNT_MISMATCH": return "a self-check failed while building the Codex. nothing was saved. please try again."; default: return message || "Codex generation failed."; } } async function handleGenerate() { const snap = snapshot(); if (!snap || generating()) return; setGenError(null); setGenerating(true); try { const result = await generateCodex(buildDeps(snap), { includeRecoveryWords: includeRecoveryWords(), }); downloadPdf(result.pdfBytes); setCompleted(result.manifest); try { await chrome.storage.local?.set({ [STORAGE_KEY_LAST_GENERATED]: Date.now(), }); } catch { // Bookkeeping; failure is non-fatal. } } catch (err) { const codexErr = err as CodexError; const code = (codexErr && (codexErr as { code?: string }).code) ?? ""; const message = err instanceof Error ? err.message : "Codex generation failed."; setGenError(explainCodexError(code, message)); } finally { setGenerating(false); } } return (
peach codex

redirecting…

loading vault…

{loadError()}

a printable paper backup of your encrypted vault, encoded as Aztec barcodes. store it somewhere only you can reach.

estimated {estimatedPages()}{" "} {estimatedPages() === 1 ? "page" : "pages"} ·{" "} {snapshot()!.entryCount}{" "} {snapshot()!.entryCount === 1 ? "entry" : "entries"}

Include recovery words

{toggleDescription()}

{genError()}

print and store your Codex somewhere safe. anyone with the paper and your master password (or recovery words, if printed) can restore your vault.

✓ Codex generated

{completed()!.pageCount}{" "} {completed()!.pageCount === 1 ? "page" : "pages"} ·{" "} {completed()!.totalChunks} codes · verify code{" "} {completed()!.verifyCode}

the PDF has been downloaded. print it on plain A4 paper — colour or laser, both work. store it somewhere only you can reach.

); }