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
↓ peach-browser-bdf81779.tar.gzatlas at this save
files changed2 files
src/popup/styles/popup.css+17 −0src/popup/views/Settings.tsx+108 −0src/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;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