4d2b19516c66

fix: Update onboarding features and Codex restore (v3.6.64)

- Refresh onboarding free and Pro feature copy to match current product surfaces - Add a Codex tab to the onboarding restore options and route it to Codex restore - Harden RuneBackground for runtimes without document.fonts - Cover Codex restore availability in popup contracts

time
2026-05-20 12:02 UTC · 5 months ago
machine
koh2
parent
d99308b4fb2a
9 files manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+2 −2package.json+1 −1src/popup/components/RuneBackground.tsx+6 −1src/popup/styles/popup.css+27 −1src/popup/views/Lock.tsx+29 −3src/popup/views/Onboarding.tsx+2 −3src/popup/views/contracts.test.tsx+38 −0
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach",5-  "version": "3.6.63",5+  "version": "3.6.64",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "3.6.63",4+  "version": "3.6.64",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package-lock.json+2 −2
@@ -1,12 +1,12 @@11 {22   "name": "peach",3-  "version": "3.6.63",3+  "version": "3.6.64",44   "lockfileVersion": 3,55   "requires": true,66   "packages": {77     "": {88       "name": "peach",9-      "version": "3.6.63",9+      "version": "3.6.64",1010       "dependencies": {1111         "@bokuweb/zstd-wasm": "^0.0.27",1212         "@stablelib/blake2s": "^2.0.1",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "3.6.63",3+  "version": "3.6.64",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/components/RuneBackground.tsx+6 −1
@@ -96,7 +96,12 @@9696     };9797     document.addEventListener("visibilitychange", onVisibility);9898 99-    document.fonts.load(`${FONT_SIZE}px PeachScript`).finally(start);99+    const fonts = document.fonts;100+    if (fonts) {101+      fonts.load(`${FONT_SIZE}px PeachScript`).finally(start);102+    } else {103+      start();104+    }100105 101106     onCleanup(() => {102107       stop();

view file

src/popup/styles/popup.css+27 −1
@@ -3777,11 +3777,37 @@37773777 37783778 .peach-rt-symbols,37793779 .peach-rt-csv,3780-.peach-rt-phone {3780+.peach-rt-phone,3781+.peach-restore-codex {37813782   display: flex;37823783   flex-direction: column;37833784   gap: 8px;37843785   overflow: hidden;3786+}3787+3788+.peach-restore-codex {3789+  min-height: 0;3790+  justify-content: center;3791+}3792+3793+.peach-restore-codex-title {3794+  margin: 0;3795+  font-family: var(--font-display);3796+  font-size: 1rem;3797+  font-style: italic;3798+  color: var(--peach-t0);3799+}3800+3801+.peach-restore-codex-copy {3802+  margin: 0;3803+  font-size: 0.75rem;3804+  line-height: 1.5;3805+  color: var(--peach-t1);3806+}3807+3808+.peach-restore-codex-button {3809+  width: 100%;3810+  justify-content: center;37853811 }37863812 37873813 .peach-rt-submit {

view file

src/popup/views/Lock.tsx+29 −3
@@ -256,6 +256,7 @@256256       <Match when={restoreSurface() === "restore"}>257257         <RestoreTabView258258           hasVault={props.hasVault}259+          onOpenCodexRestore={props.onOpenCodexRestore}259260           onBack={() => setRestoreSurface(null)}260261           onUnlock={async () => {261262             setRestoreSurface(null);@@ -534,16 +535,17 @@534535 535536 // ── Restore tab view ───────────────────────────────────────────────────────536537 537-type RestoreTab = "symbols" | "csv";538+type RestoreTab = "symbols" | "codex" | "csv";538539 539540 function tabLabel(tab: RestoreTab): string {540541   return (541-    { symbols: "PeachScript", csv: "CSV" }[tab]542+    { symbols: "PeachScript", codex: "Codex", csv: "CSV" }[tab]542543   );543544 }544545 545546 function RestoreTabView(props: {546547   hasVault: boolean;548+  onOpenCodexRestore?: () => void;547549   onBack: () => void;548550   onUnlock: () => void | Promise<void>;549551 }) {@@ -582,7 +584,7 @@582584 583585       <div class="peach-newvault-card peach-restore-tabcard">584586         <div class="peach-restore-tabs" role="tablist">585-          <For each={["symbols", "csv"] as RestoreTab[]}>587+          <For each={["symbols", "codex", "csv"] as RestoreTab[]}>586588             {(tab) => (587589               <button588590                 class="peach-restore-tab"@@ -600,6 +602,9 @@600602           <Show when={activeTab() === "symbols"}>601603             <RestoreSymbols hasVault={props.hasVault} onSuccess={handleRestoreSuccess} />602604           </Show>605+          <Show when={activeTab() === "codex"}>606+            <RestoreCodex onOpen={() => props.onOpenCodexRestore?.()} />607+          </Show>603608           <Show when={activeTab() === "csv"}>604609             <RestoreCSV hasVault={props.hasVault} onSuccess={handleRestoreSuccess} />605610           </Show>@@ -627,6 +632,27 @@627632         </div>628633       </Show>629634     </section>635+  );636+}637+638+// ── Peach Codex tab ────────────────────────────────────────────────────────639+640+function RestoreCodex(props: { onOpen: () => void }) {641+  return (642+    <div class="peach-restore-codex">643+      <p class="peach-restore-codex-title">Restore from Peach Codex</p>644+      <p class="peach-restore-codex-copy">645+        Scan printed Codex pages or import the Codex PDF. Codex restore is646+        always available, even before you create a vault.647+      </p>648+      <button649+        type="button"650+        class="peach-btn peach-btn--accent peach-btn--md peach-restore-codex-button"651+        onClick={props.onOpen}652+      >653+        open Codex restore →654+      </button>655+    </div>630656   );631657 }632658 

view file

src/popup/views/Onboarding.tsx+2 −3
@@ -28,9 +28,8 @@2828   {2929     headline: "Everything\nyou need.",3030     body: [31-      "Passwords, passkeys, cards, secure notes, two-factor codes.",32-      "Autofills on every site. Generates strong passwords. Detects breaches.",33-      "Free forever. Pro adds sync, passkeys, and advanced vault health.",31+      "Free includes passwords, cards, secure notes, two-factor codes, autofill, strong passwords, and breach checks.",32+      "Pro adds encrypted sync, Peach Codex paper restore, passkeys, mobile apps, advanced vault health, canary entries, and duress tools.",3433     ],3534     warning: null,3635   },

view file

src/popup/views/contracts.test.tsx+38 −0
@@ -257,6 +257,44 @@257257     expect(container.textContent).toContain("CSV");258258   });259259 260+  it("Lock onboarding restore options include Peach Codex", async () => {261+    const onOpenCodexRestore = vi.fn();262+    const container = mount(263+      <LockView264+        hasVault={false}265+        ready={true}266+        onUnlock={vi.fn()}267+        onOpenCodexRestore={onOpenCodexRestore}268+      />269+    );270+271+    const restoreButton = Array.from(container.querySelectorAll("button")).find(272+      (candidate) => candidate.textContent?.includes("restore from backup")273+    ) as HTMLButtonElement;274+    restoreButton.click();275+    await flushView();276+277+    expect(container.textContent).toContain("PeachScript");278+    expect(container.textContent).toContain("Codex");279+    expect(container.textContent).toContain("CSV");280+281+    const codexTab = Array.from(container.querySelectorAll("button")).find(282+      (candidate) => candidate.textContent?.trim() === "Codex"283+    ) as HTMLButtonElement;284+    codexTab.click();285+    await flushView();286+287+    expect(container.textContent).toContain("Restore from Peach Codex");288+    expect(container.textContent).toContain("Scan printed Codex pages");289+290+    const openCodexButton = Array.from(container.querySelectorAll("button")).find(291+      (candidate) => candidate.textContent?.trim() === "open Codex restore →"292+    ) as HTMLButtonElement;293+    openCodexButton.click();294+295+    expect(onOpenCodexRestore).toHaveBeenCalledTimes(1);296+  });297+260298   it("Settings requires current, new, and confirm passwords before enabling save", async () => {261299     sendMessageMock.mockResolvedValue({ ok: true, data: null });262300     const container = mount(

view file