4d43c82d69fd
fix: Add onboarding password reveal controls (v3.9.2)
- time
- 2026-06-11 16:30 UTC · 4 months ago
- machine
- koh2
- parent
- f17d41070dea
↓ peach-browser-4d43c82d.tar.gzatlas at this save
files changed6 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/views/Import.tsx+88 −50src/popup/views/Lock.tsx+5 −3src/popup/views/contracts.test.tsx+10 −0manifest.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 Password Manager",5- "version": "3.9.1",5+ "version": "3.9.2",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "3.9.1",4+ "version": "3.9.2",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "3.9.1",3+ "version": "3.9.2",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/popup/views/Import.tsx+88 −50
@@ -28,6 +28,76 @@2828 onImport: () => void | Promise<void>;2929 }3030 31+function OnboardingMasterPasswordFields(props: {32+ idPrefix: string;33+ copy: string;34+ masterPlaceholder: string;35+ password: string;36+ confirm: string;37+ onPasswordInput: (value: string) => void;38+ onConfirmInput: (value: string) => void;39+}) {40+ const [showPassword, setShowPassword] = createSignal(false);41+42+ return (43+ <>44+ <div class="peach-newvault-label-row">45+ <label class="peach-label" for={`${props.idPrefix}-master-password`}>46+ new master password47+ </label>48+ <button49+ class="peach-lock-reveal-btn"50+ type="button"51+ onClick={() => setShowPassword((value) => !value)}52+ aria-label={showPassword() ? "Hide passwords" : "Show passwords"}53+ >54+ <Show55+ when={showPassword()}56+ fallback={57+ <svg width="15" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">58+ <path d="M1 5C1 5 2.8 1 7 1C11.2 1 13 5 13 5C13 5 11.2 9 7 9C2.8 9 1 5 1 5Z" />59+ <circle cx="7" cy="5" r="1.75" />60+ </svg>61+ }62+ >63+ <svg width="15" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">64+ <path d="M1 2C3 4.5 5 6 7 6C9 6 11 4.5 13 2" />65+ <path d="M3.5 5.5L2.5 7.5" />66+ <path d="M7 6.5V8.5" />67+ <path d="M10.5 5.5L11.5 7.5" />68+ </svg>69+ </Show>70+ {showPassword() ? "hide" : "show"}71+ </button>72+ </div>73+ <p class="peach-export-card-copy">{props.copy}</p>74+ <div class="peach-settings-stack peach-export-password-stack">75+ <input76+ id={`${props.idPrefix}-master-password`}77+ class="peach-settings-input"78+ type={showPassword() ? "text" : "password"}79+ placeholder={props.masterPlaceholder}80+ value={props.password}81+ onInput={(event) => props.onPasswordInput((event.target as HTMLInputElement).value)}82+ />83+ <input84+ id={`${props.idPrefix}-confirm-password`}85+ class="peach-settings-input"86+ type={showPassword() ? "text" : "password"}87+ placeholder="confirm master password"88+ value={props.confirm}89+ onInput={(event) => props.onConfirmInput((event.target as HTMLInputElement).value)}90+ />91+ <Show when={props.confirm.length > 0 && props.password !== props.confirm}>92+ <p class="peach-export-note" style="color:var(--peach-t2)">93+ Passwords don't match94+ </p>95+ </Show>96+ </div>97+ </>98+ );99+}100+31101 export function ImportView(props: ImportProps) {32102 const [view, setView] = createSignal<ImportSubview>(props.initialView ?? "menu");33103 // When FileImport detects a .peach file, it stashes the contents here and@@ -286,31 +356,15 @@286356 />287357 </div>288358 <Show when={isOnboarding}>289- <p class="peach-settings-label" style="margin-top:4px">new master password</p>290- <p class="peach-export-card-copy">291- This password unlocks Peach on this device after the restore.292- </p>293- <div class="peach-settings-stack peach-export-password-stack">294- <input295- class="peach-settings-input"296- type="password"297- placeholder={`master password (min 8 chars)`}298- value={masterPassword()}299- onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}300- />301- <input302- class="peach-settings-input"303- type="password"304- placeholder="confirm master password"305- value={masterConfirm()}306- onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}307- />308- <Show when={masterConfirm().length > 0 && masterPassword() !== masterConfirm()}>309- <p class="peach-export-note" style="color:var(--peach-t2)">310- Passwords don't match311- </p>312- </Show>313- </div>359+ <OnboardingMasterPasswordFields360+ idPrefix="peachscript"361+ copy="Use at least 8 characters. This password unlocks Peach on this device after the restore, and both fields must match before Peach can create it."362+ masterPlaceholder="new master password"363+ password={masterPassword()}364+ confirm={masterConfirm()}365+ onPasswordInput={setMasterPassword}366+ onConfirmInput={setMasterConfirm}367+ />314368 </Show>315369 <button316370 class="peach-btn peach-btn--subtle peach-btn--sm"@@ -707,31 +761,15 @@707761 708762 <Show when={needsMasterPassword() && preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>709763 <div class="peach-settings-group">710- <p class="peach-settings-label">new master password</p>711- <p class="peach-export-card-copy">712- This password unlocks the Peach vault created on this device after713- import.714- </p>715- <div class="peach-auth-grid">716- <input717- class="peach-restore-input"718- type="password"719- placeholder="new master password"720- value={masterPassword()}721- onInput={(event) =>722- setMasterPassword((event.target as HTMLInputElement).value)723- }724- />725- <input726- class="peach-restore-input"727- type="password"728- placeholder="confirm master password"729- value={masterConfirm()}730- onInput={(event) =>731- setMasterConfirm((event.target as HTMLInputElement).value)732- }733- />734- </div>764+ <OnboardingMasterPasswordFields765+ idPrefix="backup"766+ copy="Use at least 8 characters. This password unlocks the Peach vault created on this device after import, and both fields must match before Peach can create it."767+ masterPlaceholder="new master password"768+ password={masterPassword()}769+ confirm={masterConfirm()}770+ onPasswordInput={setMasterPassword}771+ onConfirmInput={setMasterConfirm}772+ />735773 </div>736774 </Show>737775 src/popup/views/Lock.tsx+5 −3
@@ -106,7 +106,7 @@106106 if (!confirmPassword()) return "confirm your password";107107 if (password() !== confirmPassword()) return "passwords don't match";108108 if (strengthScore() === null) return "checking strength…";109- if (strengthScore()! < MIN_ZXCVBN_SCORE) return "password too weak — add words or length";109+ if (strengthScore()! < MIN_ZXCVBN_SCORE) return "password too weak — use a longer passphrase";110110 return null;111111 });112112 @@ -225,7 +225,7 @@225225 }226226 const score = strengthScore();227227 if (score === null || score < MIN_ZXCVBN_SCORE) {228- setError("password is too weak — try a longer or more varied passphrase");228+ setError("password is too weak — use a longer passphrase");229229 return;230230 }231231 setLoading(true);@@ -464,7 +464,9 @@464464 <Show465465 when={password().length > 0 && passwordStrength() !== null}466466 fallback={467- <p class="peach-newvault-hint">at least 12 characters</p>467+ <p class="peach-newvault-hint">468+ Use at least 12 characters. Peach will only create the vault when the passwords match and the strength check passes.469+ </p>468470 }469471 >470472 <div class="peach-password-strength">src/popup/views/contracts.test.tsx+10 −0
@@ -959,6 +959,7 @@959959 const createButton = findButton(container, "Create vault + import 1");960960 expect(createButton.disabled).toBe(true);961961 expect(container.textContent).toContain("Restoring now replaces the locked local vault");962+ expect(container.textContent).toContain("Use at least 8 characters");962963 963964 const masterInput = container.querySelector(964965 'input[placeholder="new master password"]'@@ -966,6 +967,15 @@966967 const confirmInput = container.querySelector(967968 'input[placeholder="confirm master password"]'968969 ) as HTMLInputElement;970+ const revealButton = container.querySelector(971+ 'button[aria-label="Show passwords"]'972+ ) as HTMLButtonElement;973+ expect(revealButton).toBeTruthy();974+ revealButton.click();975+ await flushView();976+ expect(masterInput.type).toBe("text");977+ expect(confirmInput.type).toBe("text");978+969979 inputText(masterInput, "fresh-master-password");970980 inputText(confirmInput, "fresh-master-password");971981 await flushView();