bdf8177948cc

feat: UI-5 view-recovery-words requires master password re-entry (GET_RECOVERY_WORDS{password}); maps wrong_password/recovery_not_setup inline, reveals 12-word grid on success

time
2026-06-10 03:45 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
audit fix integration
parent
5d22740c27d6
2 files src/popup/styles/popup.css+17 −0src/popup/views/Settings.tsx+108 −0
src/popup/styles/popup.css+17 −0
@@ -648,6 +648,23 @@648648   color: var(--peach-t2);649649   margin-bottom: 2px;650650 }651+/* UI-5 / LV-4: recovery words grid in the view-words reveal. */652+.peach-recovery-words-grid {653+  display: grid;654+  grid-template-columns: 1fr 1fr;655+  gap: 4px 10px;656+}657+.peach-recovery-word {658+  font-family: "IBM Plex Mono", "SFMono-Regular", monospace;659+  font-size: 11px;660+  color: var(--peach-t0);661+}662+.peach-recovery-word b {663+  color: var(--peach-t2);664+  font-weight: 400;665+  margin-right: 4px;666+}667+651668 /* UI-1 / HV-1b: dismissible sync alert banners at the top of the vault view. */652669 .peach-sync-alerts {653670   display: flex;

view file

src/popup/views/Settings.tsx+108 −0
@@ -390,6 +390,54 @@390390   const [changePwSuccess] = createSignal(false);391391   const [showNewRecoveryWords, setShowNewRecoveryWords] = createSignal(false);392392 393+  // UI-5 / LV-4: view existing recovery words behind a fresh password proof.394+  const [showViewWords, setShowViewWords] = createSignal(false);395+  const [viewWordsPassword, setViewWordsPassword] = createSignal("");396+  const [viewWordsError, setViewWordsError] = createSignal<string | null>(null);397+  const [viewedWords, setViewedWords] = createSignal<string[] | null>(null);398+  const [viewWordsBusy, setViewWordsBusy] = createSignal(false);399+400+  async function handleViewRecoveryWords() {401+    setViewWordsError(null);402+    if (!viewWordsPassword()) {403+      setViewWordsError("Enter your master password.");404+      return;405+    }406+    setViewWordsBusy(true);407+    try {408+      const r = (await sendMessage({409+        type: "GET_RECOVERY_WORDS",410+        payload: { password: viewWordsPassword() },411+      })) as { ok: boolean; data?: string[]; error?: string };412+      if (r.ok) {413+        setViewedWords(r.data ?? []);414+        setViewWordsPassword("");415+        return;416+      }417+      switch (r.error) {418+        case "wrong_password":419+          setViewWordsError("Incorrect password");420+          break;421+        case "password_required":422+          setViewWordsError("Enter your master password.");423+          break;424+        case "recovery_not_setup":425+          setViewWordsError("Recovery words aren't set up for this vault yet.");426+          break;427+        default:428+          setViewWordsError("Couldn't retrieve recovery words.");429+      }430+    } finally {431+      setViewWordsBusy(false);432+    }433+  }434+  function closeViewWords() {435+    setShowViewWords(false);436+    setViewWordsPassword("");437+    setViewWordsError(null);438+    setViewedWords(null);439+  }440+393441   const [duressEnabled, setDuressEnabled] = createSignal(false);394442   const [showDuressSetup, setShowDuressSetup] = createSignal(false);395443   const [duressPassword, setDuressPassword] = createSignal("");@@ -886,6 +934,66 @@886934               >887935                 {changePwSaving() ? "saving…" : "save new password"}888936               </button>937+            </div>938+          </Show>939+940+          {/* UI-5 / LV-4: view existing recovery words behind a password proof. */}941+          <div class="peach-settings-row peach-settings-row--meta">942+            <span>Recovery words</span>943+            <button944+              class="peach-btn peach-btn--subtle peach-btn--sm"945+              onClick={() => {946+                if (showViewWords()) { closeViewWords(); } else { setShowViewWords(true); }947+              }}948+            >949+              {showViewWords() ? "cancel" : "view"}950+            </button>951+          </div>952+          <Show when={showViewWords()}>953+            <div class="peach-settings-stack" style="padding: 4px 0 2px; gap: 6px;">954+              <Show955+                when={viewedWords()}956+                fallback={957+                  <>958+                    <p class="peach-hint" style="margin:0;color:var(--peach-t2)">959+                      Enter your master password to reveal your recovery words.960+                    </p>961+                    <input962+                      class="peach-settings-input"963+                      type="password"964+                      placeholder="master password"965+                      value={viewWordsPassword()}966+                      onInput={(e) => setViewWordsPassword((e.target as HTMLInputElement).value)}967+                      autocomplete="current-password"968+                      onKeyDown={(e) => { if (e.key === "Enter") void handleViewRecoveryWords(); }}969+                    />970+                    <Show when={viewWordsError()}>971+                      <p class="peach-hint peach-hint-error" style="margin:0">{viewWordsError()}</p>972+                    </Show>973+                    <button974+                      class="peach-btn peach-btn--accent peach-btn--sm"975+                      style="align-self:flex-start"976+                      onClick={() => void handleViewRecoveryWords()}977+                      disabled={viewWordsBusy() || !viewWordsPassword()}978+                    >979+                      {viewWordsBusy() ? "checking…" : "reveal recovery words"}980+                    </button>981+                  </>982+                }983+              >984+                <div class="peach-recovery-words-grid">985+                  <For each={viewedWords()!}>986+                    {(word, i) => (987+                      <span class="peach-recovery-word"><b>{i() + 1}</b> {word}</span>988+                    )}989+                  </For>990+                </div>991+                <button992+                  class="peach-btn peach-btn--subtle peach-btn--sm"993+                  style="align-self:flex-start"994+                  onClick={closeViewWords}995+                >done</button>996+              </Show>889997             </div>890998           </Show>891999 

view file