324ba36bbfbe

collapse 3-way routing into one surface: entries → autofill, no entries → signup mode; add 'I already have an account' escape hatch

time
2026-05-12 17:45 UTC · 5 months ago
machine
koh2
parent
f7ce7c14626d
5 files manifest.firefox.json+1 −1manifest.json+1 −1src/content/fill.ts+1 −1src/content/index.ts+24 −63src/content/inject.ts+23 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.38.20",4+  "version": "2.38.21",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.38.20",4+  "version": "2.38.21",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

src/content/fill.ts+1 −1
@@ -43,7 +43,7 @@4343 // value setter (pulled off the prototype) and then dispatch the real input4444 // and change events so React re-reads the new value. Without this, the4545 // field reads as empty to the form's controlled-input state.46-function setNativeValue(el: HTMLInputElement, value: string): void {46+export function setNativeValue(el: HTMLInputElement, value: string): void {4747   const nativeInputValueSetter = Object.getOwnPropertyDescriptor(4848     window.HTMLInputElement.prototype,4949     "value"

view file

src/content/index.ts+24 −63
@@ -21,7 +21,7 @@2121 } from "./inject";2222 import { detectBrowser } from "../shared/browser";2323 import { FIREFOX_USE_CONTENT_SCRIPT_PROXY } from "../shared/passkey-flags";24-import { fillCardForm, fillFields } from "./fill";24+import { fillCardForm, fillFields, setNativeValue } from "./fill";2525 import { setupSaveDetection } from "./save";2626 import { contentMLDetector } from "./content-ml-detector";2727 import { detectPasswordPolicy } from "./policy";@@ -407,6 +407,26 @@407407   searchableEntries: SelectorEntry[],408408   domain: string409409 ): Promise<void> {410+  // No saved entries for this site → signup/new-account mode.411+  // Fetch vault identities for the identity-first surface and locate412+  // the confirm-password field so "Copy & fill" can populate it too.413+  const isSignup = domainEntries.length === 0;414+  let savedIdentities: SavedIdentity[] | undefined;415+  let confirmEl: HTMLInputElement | null = null;416+  if (isSignup) {417+    try {418+      const idResp = await chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null) as419+        { ok: boolean; data?: SavedIdentity[] } | null;420+      if (idResp?.ok && Array.isArray(idResp.data)) savedIdentities = idResp.data;421+    } catch { /* silent fallback */ }422+423+    const form = passwordEl.closest("form") ?? document.body;424+    const allPwInputs = Array.from(form.querySelectorAll<HTMLInputElement>("input[type='password']"));425+    confirmEl = allPwInputs.find(426+      (el) => el !== passwordEl && /confirm|repeat|verify/i.test(el.name + el.id + el.placeholder)427+    ) ?? (allPwInputs.length >= 2 ? allPwInputs.find((el) => el !== passwordEl) ?? null : null);428+  }429+410430   currentDismiss?.();411431   currentAnchorField = anchorField;412432   currentDismiss = showAutofillSelector(@@ -441,14 +461,12 @@441461     {442462       domain,443463       searchableEntries,444-      // After State 5 (new entry) save: fill the detected fields with the445-      // freshly-created credentials so the user can submit immediately.446-      // On a login form the user created the entry to use here; on a signup447-      // form the password was generated for this site. Same outcome either448-      // way — the credentials land in the form and the popup exits.464+      isSignup,465+      savedIdentities,449466       onFillFields: (username, password) => {450467         const form = passwordEl.closest("form") as HTMLFormElement | null;451468         fillFields(usernameEl, passwordEl, username, password, form, getPageDomain());469+        if (password && confirmEl) setNativeValue(confirmEl, password);452470         recordFillSuccess(anchorField, usernameEl, passwordEl);453471       },454472       onDismiss: () => { lastDismissedField = anchorField; startDismissLock(); },@@ -456,56 +474,7 @@456474   );457475 }458476 459-async function openSignupSelector(460-  usernameEl: HTMLInputElement,461-  passwordEl: HTMLInputElement | null,462-  domainEntries: SelectorEntry[],463-  allEntries: SelectorEntry[],464-  domain: string465-): Promise<void> {466-  // Fetch derived identity list (emails then usernames from vault).467-  // Falls back to empty list silently so the popup still opens when the468-  // vault is temporarily unreachable or returns no login entries.469-  let savedIdentities: SavedIdentity[] = [];470-  try {471-    const idResp = await chrome.runtime.sendMessage({ type: "GET_IDENTITIES" }).catch(() => null) as472-      { ok: boolean; data?: SavedIdentity[] } | null;473-    if (idResp?.ok && Array.isArray(idResp.data)) savedIdentities = idResp.data;474-  } catch { /* silent fallback */ }475477 476-  // Find the confirm-password field (second password input, or one labelled477-  // confirm/repeat/verify) so "Copy & fill" from the generator can fill it too.478-  const form = passwordEl?.closest("form") ?? document.body;479-  const allPwInputs = Array.from(form.querySelectorAll<HTMLInputElement>("input[type='password']"));480-  const confirmEl = allPwInputs.find(481-    (el) => el !== passwordEl && /confirm|repeat|verify/i.test(el.name + el.id + el.placeholder)482-  ) ?? (allPwInputs.length >= 2 ? allPwInputs.find((el) => el !== passwordEl) ?? null : null);483-484-  currentDismiss?.();485-  currentAnchorField = usernameEl;486-  currentDismiss = showAutofillSelector(487-    usernameEl,488-    domainEntries,489-    () => {},490-    {491-      domain,492-      searchableEntries: allEntries,493-      isSignup: true,494-      savedIdentities,495-      onFillFields: (username, password) => {496-        if (username) setNativeValue(usernameEl, username);497-        if (password && passwordEl) {498-          setNativeValue(passwordEl, password);499-          if (confirmEl) setNativeValue(confirmEl, password);500-        }501-        recordFillSuccess(usernameEl, passwordEl);502-      },503-      onDismiss: () => { lastDismissedField = usernameEl; startDismissLock(); },504-    }505-  );506-}507-508-509478 async function handleFieldInteraction(anchorField?: HTMLInputElement | null): Promise<void> {510479   const field = anchorField ?? getDeepActiveInput();511480   if (!field) return;@@ -596,14 +565,6 @@596565     const pwField = fields.find((f) => f.type === "password");597566     if (!pwField) return;598567     const passwordEl = pwField.el;599-600-    // Username/email icon click on a signup form → mini identity-first flow601-    // (pick identity → generator → save). Password icon click goes straight602-    // to the full create form so the generator is front-and-centre.603-    if (signupForm && usernameEl && field === usernameEl) {604-      await openSignupSelector(usernameEl, passwordEl, entries, allEntries, domain);605-      return;606-    }607568 608569     await openAutofillSelector(field, usernameEl, passwordEl, entries, allEntries, domain);609570   } finally {

view file

src/content/inject.ts+23 −0
@@ -982,6 +982,13 @@982982     color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;983983   }984984   .identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }985+  .identity-escape-row {986+    display: flex; align-items: center; padding: 8px 12px; cursor: pointer;987+    border-radius: 8px; transition: background 100ms ease; outline: none;988+    color: var(--peach-accent); font-size: 11px; font-family: Inter, sans-serif;989+    margin-top: 2px;990+  }991+  .identity-escape-row:hover, .identity-escape-row:focus { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }985992   .identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }986993   .identity-alt-input-wrap.open { display: flex; }987994   .identity-alt-input {@@ -1397,6 +1404,22 @@13971404     altInput.addEventListener("keydown", (e) => {13981405       if (e.key === "Enter") { e.preventDefault(); fillCustomEmail(); }13991406     });1407+1408+    // Escape hatch: user already has a vault entry for this site but it1409+    // wasn't detected (different domain, old entry, etc.). Tapping this1410+    // switches to the full searchable list so they can find and fill it;1411+    // selecting an entry there will trigger the remember-site toast if1412+    // the domain isn't already associated.1413+    if ((options.searchableEntries?.length ?? 0) > 0) {1414+      const escapeRow = container.appendChild(document.createElement("div"));1415+      escapeRow.className = "identity-escape-row";1416+      escapeRow.textContent = "I already have an account →";1417+      escapeRow.setAttribute("tabindex", "0");1418+      escapeRow.addEventListener("click", () => renderList());1419+      escapeRow.addEventListener("keydown", (e) => {1420+        if (e.key === "Enter" || e.key === " ") { e.preventDefault(); renderList(); }1421+      });1422+    }14001423   }14011424 14021425   function renderGenerator(filledEmail: string) {

view file