4d43c82d69fd

fix: Add onboarding password reveal controls (v3.9.2)

time
2026-06-11 16:30 UTC · 4 months ago
machine
koh2
parent
f17d41070dea
6 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 −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 Password Manager",5-  "version": "3.9.1",5+  "version": "3.9.2",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 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",

view file

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",

view file

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 

view file

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">

view file

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();

view file