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
↓ peach-browser-324ba36b.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1src/content/fill.ts+1 −1src/content/index.ts+24 −63src/content/inject.ts+23 −0manifest.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": {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",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"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 {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) {