1234567891011121314151617import { createSignal, createMemo, For, onMount, Show } from "solid-js";18import { sendMessage } from "../../shared/bridge";19import { isNativePlatform } from "../../shared/native";20import { RecoveryRuneBackground } from "../components/RecoveryRuneBackground";21import type {22 CodexDeps,23 CodexError,24 CodexManifest,25 CodexProgress,26 CodexVaultSnapshot,27} from "../../shared/codex-types";28import { STORAGE_KEY_LAST_GENERATED } from "../../shared/codex-types";29import { pickConfirmationPositions } from "../codex/confirmation";30import type { VaultEntry } from "../../shared/types";3132interface CodexGenerateViewProps {33 isPro: boolean;34 onBack: () => void;35 onUpgradePro: () => void;36 onOpenSecurity: () => void;37 onVaultLocked: () => void;38}3940type Step = "words" | "generating" | "done";41type CodexGenerateModule = typeof import("../codex/generate");4243let codexGenerateModulePromise: Promise<CodexGenerateModule> | null = null;4445function loadCodexGenerateModule(): Promise<CodexGenerateModule> {46 codexGenerateModulePromise ??= import("../codex/generate");47 return codexGenerateModulePromise;48}495051export function CodexGenerateView(props: CodexGenerateViewProps) {52 const [snapshot, setSnapshot] = createSignal<CodexVaultSnapshot | null>(null);53 const [estimatedPages, setEstimatedPages] = createSignal<number | null>(null);54 const [estimatedParity, setEstimatedParity] = createSignal<number | null>(null);55 const [loading, setLoading] = createSignal(true);56 const [loadError, setLoadError] = createSignal<string | null>(null);57 const [libraryLoading, setLibraryLoading] = createSignal(false);58 const [libraryReady, setLibraryReady] = createSignal(false);59 const [libraryError, setLibraryError] = createSignal<string | null>(null);60 const [missingRecoveryWords, setMissingRecoveryWords] = createSignal(false);6162 const [step, setStep] = createSignal<Step>("words");63 const [confirmPositions, setConfirmPositions] = createSignal<64 [number, number, number]65 >([3, 7, 11]);66 const [confirmInputs, setConfirmInputs] = createSignal<67 [string, string, string]68 >(["", "", ""]);6970 const [progressStep, setProgressStep] = createSignal<string>("");71 const [progressCurrent, setProgressCurrent] = createSignal<number>(0);72 const [progressTotal, setProgressTotal] = createSignal<number>(0);7374 const [genError, setGenError] = createSignal<string | null>(null);75 const [completed, setCompleted] = createSignal<CodexManifest | null>(null);7677 onMount(async () => {78 if (!props.isPro) {79 props.onUpgradePro();80 return;81 }8283 const res = (await sendMessage({84 type: "GET_CODEX_VAULT_DATA",85 })) as { ok: boolean; data?: CodexVaultSnapshot; error?: string };8687 if (!res.ok || !res.data) {88 89 90 if (res.error?.startsWith("VAULT_LOCKED")) {91 props.onVaultLocked();92 return;93 }94 setLoadError(res.error ?? "could not load vault data");95 setLoading(false);96 return;97 }9899 setSnapshot(res.data);100101 const words = res.data.recoveryWords;102 if (!words || words.length !== 12) {103 setMissingRecoveryWords(true);104 setLoading(false);105 return;106 }107108 setConfirmPositions(pickConfirmationPositions());109 setLoading(false);110 void warmCodexLibrary(res.data);111 });112113 const allCorrect = createMemo(() => {114 const snap = snapshot();115 const words = snap?.recoveryWords;116 if (!words || words.length !== 12) return false;117 const positions = confirmPositions();118 const inputs = confirmInputs();119 return positions.every(120 (pos, i) =>121 inputs[i]!.toLowerCase().trim() === words[pos - 1]!.toLowerCase(),122 );123 });124125 function buildDeps(snap: CodexVaultSnapshot): CodexDeps {126 return {127 getSlotA: async () => snap.slotA,128 getSlotB: async () => snap.slotB,129 getLastModified: async () => snap.lastModified,130 131 132 133 getAllEntries: async () =>134 Array.from(135 { length: snap.entryCount },136 () => ({ deletedAt: null, archived: false }) as unknown as VaultEntry,137 ),138 getVerifyCode: async () => snap.verifyCode,139 isVaultUnlocked: () => snap.vaultUnlocked,140 };141 }142143 async function warmCodexLibrary(144 snap?: CodexVaultSnapshot,145 ): Promise<CodexGenerateModule | null> {146 if (libraryReady()) return loadCodexGenerateModule();147148 setLibraryLoading(true);149 setLibraryError(null);150 try {151 const [generateModule, rsModule] = await Promise.all([152 loadCodexGenerateModule(),153 import("../codex/rs-loader"),154 ]);155 await rsModule.getReedSolomon();156 setLibraryReady(true);157158 if (snap?.slotA) {159 try {160 const { pages, parityShards } = await generateModule.estimateCodexPages(161 snap.slotA,162 snap.slotB,163 );164 setEstimatedPages(pages);165 setEstimatedParity(parityShards);166 } catch {167 168 setEstimatedPages(null);169 setEstimatedParity(null);170 }171 }172173 return generateModule;174 } catch (err) {175 codexGenerateModulePromise = null;176 setLibraryError(177 `Failed to load Codex library: ${err instanceof Error ? err.message : String(err)}`,178 );179 return null;180 } finally {181 setLibraryLoading(false);182 }183 }184185 function codexFilename(): string {186 const d = new Date();187 const y = d.getFullYear();188 const m = String(d.getMonth() + 1).padStart(2, "0");189 const day = String(d.getDate()).padStart(2, "0");190 return `peach-codex-${y}-${m}-${day}.pdf`;191 }192193 function downloadPdf(bytes: Uint8Array): void {194 const a = document.createElement("a");195 a.download = codexFilename();196 if (isNativePlatform()) {197 198 199 200 201 let bin = "";202 const chunk = 0x8000;203 for (let i = 0; i < bytes.length; i += chunk) {204 const slice = bytes.subarray(i, Math.min(i + chunk, bytes.length));205 bin += String.fromCharCode(...slice);206 }207 a.href = `data:application/pdf;base64,${btoa(bin)}`;208 } else {209 210 211 const owned = new Uint8Array(bytes);212 const blob = new Blob([owned.buffer], { type: "application/pdf" });213 a.href = URL.createObjectURL(blob);214 setTimeout(() => URL.revokeObjectURL(a.href), 4000);215 }216 document.body.appendChild(a);217 a.click();218 a.remove();219 }220221 function explainCodexError(code: string, message: string): string {222 switch (code) {223 case "VAULT_LOCKED":224 return "your vault is locked. unlock and try again.";225 case "VAULT_EMPTY":226 return "your vault is empty — nothing to print yet.";227 case "PRO_REQUIRED":228 return "Codex generation is a Pro feature.";229 case "PDF_GENERATION_FAILED":230 case "AZTEC_GENERATION_FAILED":231 return "could not render the Codex. please try again.";232 case "COMPRESSION_ROUNDTRIP_FAILED":233 case "ERASURE_CODING_VERIFICATION_FAILED":234 case "ERASURE_CODING_CONTENT_MISMATCH":235 case "AZTEC_ROUNDTRIP_FAILED":236 case "AZTEC_CONTENT_MISMATCH":237 case "RECONSTRUCTION_SIMULATION_FAILED":238 case "RECONSTRUCTION_VERIFY_CODE_MISMATCH":239 case "RECONSTRUCTION_FINGERPRINT_MISMATCH":240 case "RECONSTRUCTION_COUNT_MISMATCH":241 return `a self-check failed while building the Codex (${code}). nothing was saved. please try again.`;242 default:243 return message || "Codex generation failed.";244 }245 }246247 const onProgress: CodexProgress = (s, current, total) => {248 setProgressStep(s);249 setProgressCurrent(current ?? 0);250 setProgressTotal(total ?? 0);251 };252253 async function handleGenerate() {254 const snap = snapshot();255 if (!snap || !allCorrect() || step() === "generating") return;256257 setGenError(null);258 setStep("generating");259 setProgressStep("compress");260261 try {262 const generateModule = await warmCodexLibrary(snap);263 if (!generateModule) {264 setStep("words");265 return;266 }267 const result = await generateModule.generateCodex(buildDeps(snap), onProgress);268 downloadPdf(result.pdfBytes);269 setCompleted(result.manifest);270 setStep("done");271272 try {273 await chrome.storage.local?.set({274 [STORAGE_KEY_LAST_GENERATED]: Date.now(),275 });276 } catch {277 278 }279 } catch (err) {280 const codexErr = err as CodexError;281 const code = (codexErr && (codexErr as { code?: string }).code) ?? "";282 const message =283 err instanceof Error ? err.message : "Codex generation failed.";284 console.error("[Codex] generation failed:", code, message);285 setGenError(explainCodexError(code, message));286 setStep("words");287 }288 }289290 function progressLabel(): string {291 switch (progressStep()) {292 case "compress":293 return "compressing vault";294 case "erasure":295 return "building chunks";296 case "aztec": {297 const total = progressTotal();298 const current = progressCurrent();299 if (total > 0) return `generating codes (${current} of ${total})`;300 return "generating codes";301 }302 case "verify":303 return "self-checking";304 case "pdf":305 return "creating PDF";306 default:307 return "preparing";308 }309 }310311 function progressPercent(): number {312 const total = progressTotal();313 const current = progressCurrent();314 315 316 switch (progressStep()) {317 case "compress":318 return 8;319 case "erasure":320 return 18;321 case "aztec":322 if (total <= 0) return 30;323 return 30 + Math.floor((current / total) * 55); 324 case "verify":325 return 90;326 case "pdf":327 return 96;328 default:329 return 4;330 }331 }332333 return (334 <div class="peach-overlay-view ov-enter-right" style="display:flex">335 <div class="peach-overlay-header">336 <button337 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"338 onClick={props.onBack}339 aria-label="Back"340 disabled={step() === "generating"}341 >342 <svg width="7" height="12" viewBox="0 0 7 12" fill="none"343 stroke="currentColor" stroke-width="1.1"344 stroke-linecap="round" stroke-linejoin="round">345 <path d="M6 1L1 6L6 11" />346 </svg>347 </button>348 <span class="peach-overlay-title">peach codex</span>349 <div class="peach-overlay-spacer"></div>350 </div>351352 <div class="peach-settings-body peach-codex-body peach-codex-body--runes">353 <RecoveryRuneBackground />354355 <Show when={!props.isPro}>356 <p class="peach-hint">redirecting…</p>357 </Show>358359 <Show when={props.isPro && loading()}>360 <p class="peach-hint">loading vault…</p>361 </Show>362363 <Show when={props.isPro && !loading() && loadError()}>364 <p class="peach-hint peach-hint-error">{loadError()}</p>365 </Show>366367 <Show when={props.isPro && !loading() && !loadError() && missingRecoveryWords()}>368 <div class="peach-settings-group peach-codex-intro">369 <p class="peach-codex-blurb">370 the Codex prints your encrypted vault as Aztec codes. before371 you can generate one, set up your 12 recovery words in372 Settings → Security.373 </p>374 </div>375 <button376 class="peach-btn peach-btn--accent peach-btn--md peach-btn--full"377 onClick={props.onOpenSecurity}378 >379 open Security settings380 </button>381 </Show>382383 <Show384 when={385 props.isPro &&386 !loading() &&387 !loadError() &&388 !missingRecoveryWords() &&389 snapshot()390 }391 >392 <Show when={step() === "words"}>393 <div class="peach-codex-recovery-headline">394 <h2 class="peach-codex-recovery-title">your recovery words.</h2>395 <p class="peach-codex-recovery-body">396 write these down — store separately from your Codex.397 </p>398 </div>399400 <ol class="peach-codex-words-grid" start={1}>401 <For each={snapshot()!.recoveryWords ?? []}>402 {(word, i) => (403 <li class="peach-codex-word-row">404 <span class="peach-codex-word-numeral">{i() + 1}</span>405 <span class="peach-codex-word-text">{word}</span>406 </li>407 )}408 </For>409 </ol>410411 <Show when={estimatedPages() !== null}>412 <p class="peach-codex-meta">413 <strong>{estimatedPages()}</strong>{" "}414 {estimatedPages() === 1 ? "page" : "pages"} ·{" "}415 {snapshot()!.entryCount}{" "}416 {snapshot()!.entryCount === 1 ? "entry" : "entries"}417 <Show when={estimatedParity() !== null}>418 {" "}· {estimatedParity()} redundancy codes419 </Show>420 </p>421 </Show>422423 <div class="peach-codex-confirm-section">424 <p class="peach-codex-confirm-label">425 to continue, enter words {confirmPositions().join(", ")}:426 </p>427 <For each={confirmPositions()}>428 {(pos, i) => (429 <div class="peach-codex-confirm-row">430 <span class="peach-codex-confirm-pos">word {pos}</span>431 <input432 class="peach-codex-confirm-input"433 type="text"434 autocomplete="off"435 autocorrect="off"436 autocapitalize="none"437 spellcheck={false}438 value={confirmInputs()[i()]}439 onInput={(e) => {440 const next = [...confirmInputs()] as [441 string,442 string,443 string,444 ];445 next[i()] = (e.target as HTMLInputElement).value;446 setConfirmInputs(next);447 }}448 onKeyDown={(e) => {449 if (e.key === "Enter" && allCorrect())450 void handleGenerate();451 }}452 />453 </div>454 )}455 </For>456 </div>457458 <Show when={genError()}>459 <p class="peach-hint peach-hint-error peach-codex-error">460 {genError()}461 </p>462 </Show>463 <Show when={libraryError()}>464 <p class="peach-hint peach-hint-error peach-codex-error">465 {libraryError()}466 </p>467 </Show>468 <Show when={libraryLoading() && !libraryReady()}>469 <p class="peach-hint peach-codex-meta">470 preparing Codex library…471 </p>472 </Show>473474 <button475 class="peach-btn peach-btn--accent peach-btn--md peach-btn--full peach-codex-generate-btn"476 disabled={!allCorrect() || libraryLoading()}477 onClick={() => void handleGenerate()}478 >479 {libraryLoading() && !libraryReady() ? "preparing…" : "generate my Codex"}480 </button>481 </Show>482483 <Show when={step() === "generating"}>484 <div class="peach-codex-recovery-headline">485 <h2 class="peach-codex-recovery-title">486 generating your codex…487 </h2>488 </div>489490 <div class="peach-codex-progress-group">491 <p class="peach-codex-progress-label">492 <span>{progressLabel()}</span>493 <span class="peach-codex-verify-tag">494 {snapshot()?.verifyCode}495 </span>496 </p>497 <div class="peach-codex-progress-track">498 <div499 class="peach-codex-progress-fill"500 style={`width:${progressPercent()}%`}501 />502 </div>503 </div>504 </Show>505506 <Show when={step() === "done"}>507 <div class="peach-codex-recovery-headline">508 <h2 class="peach-codex-recovery-title">509 your codex is downloading.510 </h2>511 </div>512513 <Show when={completed()}>514 <p class="peach-codex-meta">515 {completed()!.pageCount}{" "}516 {completed()!.pageCount === 1 ? "page" : "pages"} ·{" "}517 {completed()!.totalChunks} codes · verify code{" "}518 <span class="peach-codex-verify">519 {completed()!.verifyCode}520 </span>521 </p>522 </Show>523524 <div class="peach-codex-done-guidance">525 <div class="peach-codex-done-item">526 <span class="peach-codex-done-item-icon">🖨</span>527 <div>528 <p class="peach-codex-done-item-title">529 print on a laser printer530 </p>531 <p class="peach-codex-done-item-body">532 laser prints last longer and scan more reliably than533 inkjet. print at 100% scale — do not fit to page.534 </p>535 </div>536 </div>537538 <div class="peach-codex-done-item">539 <span class="peach-codex-done-item-icon">📦</span>540 <div>541 <p class="peach-codex-done-item-title">make two copies</p>542 <p class="peach-codex-done-item-body">543 store one at home (fireproof box or safe) and one544 somewhere else (trusted person, safety deposit box).545 fire and flood can destroy a single copy.546 </p>547 </div>548 </div>549550 <div class="peach-codex-done-item">551 <span class="peach-codex-done-item-icon">🔑</span>552 <div>553 <p class="peach-codex-done-item-title">554 store separately from recovery words555 </p>556 <p class="peach-codex-done-item-body">557 your 12 recovery words go somewhere different. together558 they restore your vault without your passphrase. keep559 them apart.560 </p>561 </div>562 </div>563564 <div class="peach-codex-done-item">565 <span class="peach-codex-done-item-icon">💾</span>566 <div>567 <p class="peach-codex-done-item-title">also save the PDF</p>568 <p class="peach-codex-done-item-body">569 copy the PDF to a USB drive. Peach can import it570 directly without needing to scan printed codes.571 </p>572 </div>573 </div>574 </div>575576 <p class="peach-codex-done-regenerate">577 regenerate your codex whenever you add many new entries.578 </p>579580 <button581 class="peach-btn peach-btn--subtle peach-btn--sm peach-btn--full"582 onClick={() => {583 setCompleted(null);584 setGenError(null);585 setLibraryError(null);586 setConfirmInputs(["", "", ""]);587 setConfirmPositions(pickConfirmationPositions());588 setStep("words");589 }}590 >591 generate another592 </button>593 </Show>594 </Show>595 </div>596 </div>597 );598}