952ee75b2c6e

show fresh recovery words in Settings after password change

time
2026-05-05 15:53 UTC · 5 months ago
machine
koh2
parent
62761aef1e41
2 files src/popup/views/Settings.tsx+35 −26src/popup/views/contracts.test.tsx+48 −19
src/popup/views/Settings.tsx+35 −26
@@ -2,6 +2,7 @@22 import { sendMessage } from "../App";33 import { PeachMark } from "../components/PeachMark";44 import { LockRuneCanvas } from "../components/LockRuneCanvas";5+import { RecoveryWordDisplayView } from "./RecoveryWordDisplay";56 import type { Canary } from "../../shared/types";67 import { ACCENT_COLORS } from "../../shared/types";78 import type {@@ -201,6 +202,7 @@201202   const [changePwSaving, setChangePwSaving] = createSignal(false);202203   const [changePwError, setChangePwError] = createSignal<string | null>(null);203204   const [changePwSuccess, setChangePwSuccess] = createSignal(false);205+  const [showNewRecoveryWords, setShowNewRecoveryWords] = createSignal(false);204206 205207   const [duressEnabled, setDuressEnabled] = createSignal(false);206208   const [showDuressSetup, setShowDuressSetup] = createSignal(false);@@ -308,9 +310,9 @@308310       payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() },309311     }) as { ok: boolean; error?: string };310312     if (res.ok) {311-      setChangePwSuccess(true);312313       setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");313-      setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200);314+      setShowChangePw(false);315+      setShowNewRecoveryWords(true);314316     } else {315317       setChangePwError(res.error ?? "failed to change password");316318     }@@ -1905,30 +1907,37 @@19051907   }19061908 19071909   return (1908-    <div class="peach-overlay-view ov-enter-right" style="display:flex">1909-      <div class="peach-overlay-header">1910-        <button1911-          class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1912-          onClick={handleHeaderBack}1913-          aria-label="Back"1914-        >1915-          <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1916-            <path d="M6 1L1 6L6 11" />1917-          </svg>1918-        </button>1919-        <span class="peach-overlay-title">{currentTitle()}</span>1920-        <div class="peach-overlay-spacer"></div>1921-      </div>1910+    <Show1911+      when={showNewRecoveryWords()}1912+      fallback={1913+        <div class="peach-overlay-view ov-enter-right" style="display:flex">1914+          <div class="peach-overlay-header">1915+            <button1916+              class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"1917+              onClick={handleHeaderBack}1918+              aria-label="Back"1919+            >1920+              <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">1921+                <path d="M6 1L1 6L6 11" />1922+              </svg>1923+            </button>1924+            <span class="peach-overlay-title">{currentTitle()}</span>1925+            <div class="peach-overlay-spacer"></div>1926+          </div>19221927 1923-      <Switch fallback={renderHome()}>1924-        <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1925-        <Match when={activeSection() === "vault"}>{renderVault()}</Match>1926-        <Match when={activeSection() === "sync"}>{renderSync()}</Match>1927-        <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1928-        <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1929-        <Match when={activeSection() === "license"}>{renderLicense()}</Match>1930-        <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1931-      </Switch>1932-    </div>1928+          <Switch fallback={renderHome()}>1929+            <Match when={activeSection() === "security"}>{renderSecurity()}</Match>1930+            <Match when={activeSection() === "vault"}>{renderVault()}</Match>1931+            <Match when={activeSection() === "sync"}>{renderSync()}</Match>1932+            <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>1933+            <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>1934+            <Match when={activeSection() === "license"}>{renderLicense()}</Match>1935+            <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1936+          </Switch>1937+        </div>1938+      }1939+    >1940+      <RecoveryWordDisplayView onDone={() => setShowNewRecoveryWords(false)} />1941+    </Show>19331942   );19341943 }

view file

src/popup/views/contracts.test.tsx+48 −19
@@ -320,7 +320,19 @@320320   });321321 322322   it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {323-    sendMessageMock.mockResolvedValue({ ok: true, data: null });323+    sendMessageMock.mockImplementation(async (msg: unknown) => {324+      const m = msg as { type: string };325+      if (m.type === "GET_RECOVERY_WORDS") {326+        return {327+          ok: true,328+          data: [329+            "abandon", "ability", "able", "about", "above", "absent",330+            "absorb", "abstract", "absurd", "abuse", "access", "accident",331+          ],332+        };333+      }334+      return { ok: true, data: null };335+    });324336     const onAutoLockMinutesChange = vi.fn();325337     const onThemeChange = vi.fn();326338     const container = mount(@@ -355,18 +367,44 @@355367     await flushView();356368     sendMessageMock.mockClear();357369 358-    const headerBack = container.querySelector(".peach-overlay-header button") as HTMLButtonElement;370+    function headerBack() {371+      return container.querySelector(".peach-overlay-header button") as HTMLButtonElement;372+    }359373 360-    const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(361-      (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"362-    ) as HTMLButtonElement;363-    securityRow.click();374+    // ── Auto-lock + theme changes (before password change) ─────────────375+    const securityRow = () =>376+      Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(377+        (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"378+      ) as HTMLButtonElement;379+380+    securityRow().click();364381     await flushView();365382 366383     const selects = container.querySelectorAll("select");367384     changeSelect(selects[0] as HTMLSelectElement, "15");368385     expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);369386 387+    headerBack().click();388+    await flushView();389+390+    const appearanceRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(391+      (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Appearance"392+    ) as HTMLButtonElement;393+    appearanceRow.click();394+    await flushView();395+    const themeButton = Array.from(container.querySelectorAll("button")).find(396+      (candidate) => candidate.textContent?.trim() === "Switch to light"397+    ) as HTMLButtonElement;398+    themeButton.click();399+    expect(onThemeChange).toHaveBeenCalledWith("light");400+401+    // ── Password change → recovery words display ────────────────────────402+    headerBack().click();403+    await flushView();404+405+    securityRow().click();406+    await flushView();407+370408     const changeButton = Array.from(container.querySelectorAll("button")).find(371409       (candidate) => candidate.textContent?.trim() === "change"372410     ) as HTMLButtonElement;@@ -385,19 +423,6 @@385423     saveButton.click();386424     await flushView();387425 388-    headerBack.click();389-    await flushView();390-    const appearanceRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(391-      (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Appearance"392-    ) as HTMLButtonElement;393-    appearanceRow.click();394-    await flushView();395-    const themeButton = Array.from(container.querySelectorAll("button")).find(396-      (candidate) => candidate.textContent?.trim() === "Switch to light"397-    ) as HTMLButtonElement;398-    themeButton.click();399-    expect(onThemeChange).toHaveBeenCalledWith("light");400-401426     expect(sendMessageMock).toHaveBeenCalledWith({402427       type: "CHANGE_PASSWORD",403428       payload: {@@ -405,6 +430,10 @@405430         newPassword: "new-password-123",406431       },407432     });433+434+    // After a successful password change the recovery word display replaces settings.435+    expect(container.textContent).toContain("save your recovery words");436+    expect(sendMessageMock).toHaveBeenCalledWith({ type: "GET_RECOVERY_WORDS" });408437   });409438 410439   it("Entry copy actions send COPY and canary ping messages for password and username", async () => {

view file