1234567891011121314151617181920import { createSignal, createMemo, onMount, Show } from "solid-js";21import { sendMessage } from "../../shared/bridge";22import { isNativePlatform } from "../../shared/native";23import type {24 CodexDeps,25 CodexError,26 CodexManifest,27 CodexVaultSnapshot,28} from "../../shared/codex-types";29import { STORAGE_KEY_LAST_GENERATED } from "../../shared/codex-types";30import { generateCodex, estimateCodexPages } from "../codex/generate";31import type { VaultEntry } from "../../shared/types";3233interface CodexGenerateViewProps {34 isPro: boolean;35 onBack: () => void;36 onUpgradePro: () => void;37}3839export function CodexGenerateView(props: CodexGenerateViewProps) {40 const [snapshot, setSnapshot] = createSignal<CodexVaultSnapshot | null>(null);41 const [estimatedPages, setEstimatedPages] = createSignal<number | null>(null);42 const [loading, setLoading] = createSignal(true);43 const [loadError, setLoadError] = createSignal<string | null>(null);44 const [includeRecoveryWords, setIncludeRecoveryWords] = createSignal(true);45 const [generating, setGenerating] = createSignal(false);46 const [genError, setGenError] = createSignal<string | null>(null);47 const [completed, setCompleted] = createSignal<CodexManifest | null>(null);4849 onMount(async () => {50 if (!props.isPro) {51 52 53 props.onUpgradePro();54 return;55 }5657 const res = (await sendMessage({58 type: "GET_CODEX_VAULT_DATA" as any,59 })) as { ok: boolean; data?: CodexVaultSnapshot; error?: string };6061 if (!res.ok || !res.data) {62 setLoadError(res.error ?? "could not load vault data");63 setLoading(false);64 return;65 }6667 setSnapshot(res.data);6869 if (res.data.slotA) {70 try {71 const pages = await estimateCodexPages(res.data.slotA, res.data.slotB);72 setEstimatedPages(pages);73 } catch {74 75 setEstimatedPages(null);76 }77 }7879 setLoading(false);80 });8182 83 84 const toggleDescription = createMemo(() =>85 includeRecoveryWords()86 ? "your 12 recovery words will be printed on the last page. anyone holding the paper can restore the vault."87 : "no recovery words on paper. you will need your master password to restore from this Codex.",88 );8990 function buildDeps(snap: CodexVaultSnapshot): CodexDeps {91 return {92 getSlotA: async () => snap.slotA,93 getSlotB: async () => snap.slotB,94 getLastModified: async () => snap.lastModified,95 96 97 98 getAllEntries: async () =>99 Array.from(100 { length: snap.entryCount },101 () => ({ deletedAt: null, archived: false }) as unknown as VaultEntry,102 ),103 getRecoveryWords: async () => snap.recoveryWords,104 getVerifyCode: async () => snap.verifyCode,105 isVaultUnlocked: () => snap.vaultUnlocked,106 };107 }108109 function codexFilename(): string {110 const d = new Date();111 const y = d.getFullYear();112 const m = String(d.getMonth() + 1).padStart(2, "0");113 const day = String(d.getDate()).padStart(2, "0");114 return `peach-codex-${y}-${m}-${day}.pdf`;115 }116117 function downloadPdf(bytes: Uint8Array): void {118 const a = document.createElement("a");119 a.download = codexFilename();120 if (isNativePlatform()) {121 122 123 124 125 let bin = "";126 const chunk = 0x8000;127 for (let i = 0; i < bytes.length; i += chunk) {128 const slice = bytes.subarray(i, Math.min(i + chunk, bytes.length));129 bin += String.fromCharCode(...slice);130 }131 a.href = `data:application/pdf;base64,${btoa(bin)}`;132 } else {133 134 135 const owned = new Uint8Array(bytes);136 const blob = new Blob([owned.buffer], { type: "application/pdf" });137 a.href = URL.createObjectURL(blob);138 setTimeout(() => URL.revokeObjectURL(a.href), 4000);139 }140 document.body.appendChild(a);141 a.click();142 a.remove();143 }144145 function explainCodexError(code: string, message: string): string {146 switch (code) {147 case "VAULT_LOCKED":148 return "your vault is locked. unlock and try again.";149 case "VAULT_EMPTY":150 return "your vault is empty — nothing to print yet.";151 case "RECOVERY_WORDS_MISSING":152 return "your 12 recovery words could not be found. try toggling recovery words off.";153 case "RECOVERY_WORDS_INVALID":154 return "your recovery words look wrong. try toggling recovery words off.";155 case "PRO_REQUIRED":156 return "Codex generation is a Pro feature.";157 case "PDF_GENERATION_FAILED":158 case "AZTEC_GENERATION_FAILED":159 return "could not render the Codex. please try again.";160 case "COMPRESSION_ROUNDTRIP_FAILED":161 case "ERASURE_CODING_VERIFICATION_FAILED":162 case "ERASURE_CODING_CONTENT_MISMATCH":163 case "AZTEC_ROUNDTRIP_FAILED":164 case "AZTEC_CONTENT_MISMATCH":165 case "RECONSTRUCTION_SIMULATION_FAILED":166 case "RECONSTRUCTION_VERIFY_CODE_MISMATCH":167 case "RECONSTRUCTION_FINGERPRINT_MISMATCH":168 case "RECONSTRUCTION_COUNT_MISMATCH":169 return "a self-check failed while building the Codex. nothing was saved. please try again.";170 default:171 return message || "Codex generation failed.";172 }173 }174175 async function handleGenerate() {176 const snap = snapshot();177 if (!snap || generating()) return;178179 setGenError(null);180 setGenerating(true);181182 try {183 const result = await generateCodex(buildDeps(snap), {184 includeRecoveryWords: includeRecoveryWords(),185 });186 downloadPdf(result.pdfBytes);187 setCompleted(result.manifest);188189 try {190 await chrome.storage.local?.set({191 [STORAGE_KEY_LAST_GENERATED]: Date.now(),192 });193 } catch {194 195 }196 } catch (err) {197 const codexErr = err as CodexError;198 const code = (codexErr && (codexErr as { code?: string }).code) ?? "";199 const message =200 err instanceof Error ? err.message : "Codex generation failed.";201 setGenError(explainCodexError(code, message));202 } finally {203 setGenerating(false);204 }205 }206207 return (208 <div class="peach-overlay-view ov-enter-right" style="display:flex">209 <div class="peach-overlay-header">210 <button211 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"212 onClick={props.onBack}213 aria-label="Back"214 >215 <svg width="7" height="12" viewBox="0 0 7 12" fill="none"216 stroke="currentColor" stroke-width="1.1"217 stroke-linecap="round" stroke-linejoin="round">218 <path d="M6 1L1 6L6 11" />219 </svg>220 </button>221 <span class="peach-overlay-title">peach codex</span>222 <div class="peach-overlay-spacer"></div>223 </div>224225 <div class="peach-settings-body peach-codex-body">226 <Show when={!props.isPro}>227 <p class="peach-hint">redirecting…</p>228 </Show>229230 <Show when={props.isPro && loading()}>231 <p class="peach-hint">loading vault…</p>232 </Show>233234 <Show when={props.isPro && !loading() && loadError()}>235 <p class="peach-hint peach-hint-error">{loadError()}</p>236 </Show>237238 <Show when={props.isPro && !loading() && !loadError() && snapshot()}>239 <div class="peach-settings-group peach-codex-intro">240 <p class="peach-codex-blurb">241 a printable paper backup of your encrypted vault, encoded as242 Aztec barcodes. store it somewhere only you can reach.243 </p>244 <Show when={estimatedPages() !== null}>245 <p class="peach-codex-meta">246 estimated <strong>{estimatedPages()}</strong>{" "}247 {estimatedPages() === 1 ? "page" : "pages"} ·{" "}248 {snapshot()!.entryCount}{" "}249 {snapshot()!.entryCount === 1 ? "entry" : "entries"}250 </p>251 </Show>252 </div>253254 <Show when={!completed()}>255 <div class="peach-settings-group">256 <div class="peach-settings-row">257 <span>Include recovery words</span>258 <button259 type="button"260 class={`peach-toggle ${includeRecoveryWords() ? "is-on" : ""}`}261 onClick={() =>262 setIncludeRecoveryWords(!includeRecoveryWords())263 }264 aria-label="Toggle recovery words on the Codex"265 disabled={generating()}266 >267 <span class="peach-toggle-knob"></span>268 </button>269 </div>270 <p class="peach-codex-toggle-desc">{toggleDescription()}</p>271 </div>272273 <Show when={genError()}>274 <p class="peach-hint peach-hint-error peach-codex-error">275 {genError()}276 </p>277 </Show>278279 <button280 class="peach-btn peach-btn--accent peach-btn--md peach-btn--full peach-codex-generate-btn"281 disabled={generating()}282 onClick={() => void handleGenerate()}283 >284 {generating() ? "building Codex…" : "generate Codex"}285 </button>286287 <p class="peach-codex-footnote">288 print and store your Codex somewhere safe. anyone with the paper289 and your master password (or recovery words, if printed) can290 restore your vault.291 </p>292 </Show>293294 <Show when={completed()}>295 <div class="peach-settings-group peach-codex-done">296 <p class="peach-hint peach-hint--success">✓ Codex generated</p>297 <p class="peach-codex-meta">298 {completed()!.pageCount}{" "}299 {completed()!.pageCount === 1 ? "page" : "pages"} ·{" "}300 {completed()!.totalChunks} codes · verify code{" "}301 <span class="peach-codex-verify">302 {completed()!.verifyCode}303 </span>304 </p>305 <p class="peach-codex-footnote">306 the PDF has been downloaded. print it on plain A4 paper —307 colour or laser, both work. store it somewhere only you can308 reach.309 </p>310 <button311 class="peach-btn peach-btn--subtle peach-btn--sm peach-btn--full"312 onClick={() => {313 setCompleted(null);314 setGenError(null);315 }}316 >317 generate another318 </button>319 </div>320 </Show>321 </Show>322 </div>323 </div>324 );325}