2adce7cc303e
feat: contextual signup wizard — step through email → username → password on multi-field signup forms (v3.6.157)
- time
- 2026-06-04 15:59 UTC · 4 months ago
- machine
- koh2
- agent
- claude-sonnet-4-6
- session
- signup form username+email+name detection
- parent
- 3ae3a65f503f
↓ peach-browser-2adce7cc.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/index.ts+1 −0src/content/inject.ts+155 −14manifest.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.6.156",5+ "version": "3.6.157",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.6.156",4+ "version": "3.6.157",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.6.156",3+ "version": "3.6.157",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/content/index.ts+1 −0
@@ -497,6 +497,7 @@497497 searchableEntries,498498 isSignup,499499 identityKind,500+ hasEmailField: !!emailEl,500501 savedIdentities,501502 onFillFields: (username, password, email) => {502503 const form = (passwordEl ?? usernameEl)?.closest("form") as HTMLFormElement | null ?? null;src/content/inject.ts+155 −14
@@ -72,6 +72,13 @@7272 */7373 onFillFields?: (username: string, password: string, email?: string) => void;7474 /**75+ * True when the form has both a distinct username field AND a distinct email76+ * field (e.g. Name / Username / Email / Password signup forms). Enables the77+ * two-step identity wizard: first pick the field type the user clicked on,78+ * then pick the other, then generate a password.79+ */80+ hasEmailField?: boolean;81+ /**7582 * When true, show an inline unlock form instead of the entries list.7683 * Used when the vault is locked so the user can unlock without opening7784 * the popup.@@ -1470,8 +1477,18 @@14701477 const customKind = resolveCustomIdentityKind(typedQuery, identityMode, fallbackKind);14711478 14721479 function applyIdentity(identity: string) {1473- options.onFillFields?.(identity, "");1474- renderGenerator(identity);1480+ if (options.hasEmailField) {1481+ if (options.identityKind === "email") {1482+ options.onFillFields?.("", "", identity); // fill email field now1483+ renderSecondaryIdentityStep("username", identity);1484+ } else {1485+ options.onFillFields?.(identity, "", ""); // fill username field now1486+ renderSecondaryIdentityStep("email", identity);1487+ }1488+ } else {1489+ options.onFillFields?.(identity, "");1490+ renderGenerator(identity);1491+ }14751492 }14761493 14771494 function applyCustomIdentity(identity: string) {@@ -1658,14 +1675,19 @@16581675 });16591676 }16601677 1661- function renderGenerator(filledEmail: string) {1678+ /**1679+ * Step 2 of the contextual signup wizard.1680+ * `stepKind` is the identity type to collect in this step ("email" or "username").1681+ * `firstValue` is what was picked in step 1 (the other type).1682+ */1683+ function renderSecondaryIdentityStep(stepKind: "email" | "username", firstValue: string) {1684+ currentMode = "identity-helper";16621685 container.innerHTML = "";1663- const sitePolicy = detectPasswordPolicy();16641686 16651687 const wrap = container.appendChild(document.createElement("div"));16661688 wrap.className = "gen-view";16671689 1668- // Header: domain + NEW ACCOUNT1690+ // Header16691691 const hdrRow = wrap.appendChild(document.createElement("div"));16701692 hdrRow.className = "domain-header";16711693 const eyebrow2 = hdrRow.appendChild(document.createElement("div"));@@ -1675,7 +1697,7 @@16751697 domName2.className = "domain-name";16761698 domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");16771699 1678- // Confirmed email row1700+ // Confirmed first step16791701 const conf = wrap.appendChild(document.createElement("div"));16801702 conf.className = "gen-confirmed";16811703 const confCheck = conf.appendChild(document.createElement("span"));@@ -1683,8 +1705,117 @@16831705 confCheck.textContent = "✓";16841706 const confVal = conf.appendChild(document.createElement("div"));16851707 confVal.className = "gen-confirmed-val";1686- confVal.textContent = filledEmail;1708+ confVal.textContent = firstValue;16871709 1710+ const stepLbl = wrap.appendChild(document.createElement("div"));1711+ stepLbl.className = "gen-label";1712+ stepLbl.textContent = stepKind === "email" ? "Now choose your email" : "Now choose your username";1713+1714+ // Filter saved identities to the correct type for this step1715+ const identities = (options.savedIdentities ?? [])1716+ .filter(s => s.type === stepKind)1717+ .sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0))1718+ .slice(0, 6);1719+1720+ const list = wrap.appendChild(document.createElement("div"));1721+ list.className = "identity-list";1722+ for (const id of identities) {1723+ const row = list.appendChild(document.createElement("div"));1724+ row.className = "peach-identity-row";1725+ row.setAttribute("tabindex", "-1");1726+ const val = row.appendChild(document.createElement("div"));1727+ val.className = "peach-identity-value";1728+ val.textContent = id.value;1729+ const meta = row.appendChild(document.createElement("div"));1730+ meta.className = "peach-identity-meta";1731+ meta.textContent = `used ${id.useCount}×`;1732+ row.addEventListener("click", () => applySecondIdentity(id.value));1733+ }1734+1735+ // Text input for a value not in the list1736+ const altInputWrap = wrap.appendChild(document.createElement("div"));1737+ altInputWrap.className = "identity-alt-input-wrap open";1738+ const altInput = altInputWrap.appendChild(document.createElement("input"));1739+ altInput.className = "identity-alt-input";1740+ altInput.type = stepKind === "email" ? "email" : "text";1741+ altInput.placeholder = stepKind === "email" ? "[email protected]" : "your username";1742+ altInput.setAttribute("autocomplete", "off");1743+ const altConfirm = altInputWrap.appendChild(document.createElement("button"));1744+ altConfirm.className = "identity-alt-confirm";1745+ altConfirm.type = "button";1746+ altConfirm.textContent = "→";1747+1748+ function applySecondIdentity(value: string) {1749+ const trimmed = value.trim();1750+ if (!trimmed) return;1751+ // Determine final email + username order before passing to the generator1752+ const [email, username] = stepKind === "email"1753+ ? [trimmed, firstValue]1754+ : [firstValue, trimmed];1755+ if (stepKind === "email") {1756+ options.onFillFields?.("", "", trimmed); // fill email field1757+ } else {1758+ options.onFillFields?.(trimmed, "", ""); // fill username field1759+ }1760+ renderGenerator(email, username);1761+ }1762+1763+ altConfirm.addEventListener("click", () => applySecondIdentity(altInput.value));1764+ altInput.addEventListener("keydown", (e) => {1765+ if (e.key === "Enter") { e.preventDefault(); applySecondIdentity(altInput.value); }1766+ });1767+1768+ // Skip: proceed without this field1769+ const skipRow = wrap.appendChild(document.createElement("div"));1770+ skipRow.className = "identity-escape-row";1771+ skipRow.textContent = `Skip ${stepKind} →`;1772+ skipRow.setAttribute("tabindex", "0");1773+ skipRow.addEventListener("click", () => {1774+ const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1775+ renderGenerator(email, username);1776+ });1777+ skipRow.addEventListener("keydown", (e) => {1778+ if (e.key === "Enter" || e.key === " ") {1779+ e.preventDefault();1780+ const [email, username] = stepKind === "email" ? ["", firstValue] : [firstValue, ""];1781+ renderGenerator(email, username);1782+ }1783+ });1784+1785+ requestAnimationFrame(() => { altInput.focus(); });1786+ }1787+1788+ function renderGenerator(filledEmail: string, filledUsername?: string) {1789+ container.innerHTML = "";1790+ const sitePolicy = detectPasswordPolicy();1791+1792+ const wrap = container.appendChild(document.createElement("div"));1793+ wrap.className = "gen-view";1794+1795+ // Header: domain + NEW ACCOUNT1796+ const hdrRow = wrap.appendChild(document.createElement("div"));1797+ hdrRow.className = "domain-header";1798+ const eyebrow2 = hdrRow.appendChild(document.createElement("div"));1799+ eyebrow2.className = "domain-eyebrow";1800+ eyebrow2.textContent = "NEW ACCOUNT";1801+ const domName2 = hdrRow.appendChild(document.createElement("div"));1802+ domName2.className = "domain-name";1803+ domName2.textContent = options.domain ?? location.hostname.replace(/^www\./, "");1804+1805+ // Confirmed identity rows — show both username and email when both were collected1806+ function confirmedRow(value: string) {1807+ const conf = wrap.appendChild(document.createElement("div"));1808+ conf.className = "gen-confirmed";1809+ const confCheck = conf.appendChild(document.createElement("span"));1810+ confCheck.className = "gen-confirmed-check";1811+ confCheck.textContent = "✓";1812+ const confVal = conf.appendChild(document.createElement("div"));1813+ confVal.className = "gen-confirmed-val";1814+ confVal.textContent = value;1815+ }1816+ if (filledEmail) confirmedRow(filledEmail);1817+ if (filledUsername && filledUsername !== filledEmail) confirmedRow(filledUsername);1818+16881819 const lbl = wrap.appendChild(document.createElement("div"));16891820 lbl.className = "gen-label";16901821 lbl.textContent = "Generate a password";@@ -1702,12 +1833,12 @@17021833 fillBtn.textContent = "Copy & fill";17031834 fillBtn.addEventListener("click", () => {17041835 if (!currentPw) return;1705- options.onFillFields?.("", currentPw);1706- renderSaveOffer(filledEmail, currentPw);1836+ options.onFillFields?.("", currentPw, "");1837+ renderSaveOffer(filledEmail, currentPw, filledUsername);17071838 });17081839 }17091840 1710- function renderSaveOffer(filledEmail: string, filledPassword: string) {1841+ function renderSaveOffer(filledEmail: string, filledPassword: string, filledUsername?: string) {17111842 container.innerHTML = "";17121843 const domain = options.domain ?? location.hostname.replace(/^www\./, "");17131844 @@ -1730,7 +1861,13 @@17301861 }17311862 17321863 detailRow("site", domain);1733- detailRow("email", filledEmail);1864+ // When both username and email were collected, show both; otherwise show the identity as "email"1865+ if (filledUsername) {1866+ detailRow("username", filledUsername);1867+ if (filledEmail) detailRow("email", filledEmail);1868+ } else {1869+ if (filledEmail) detailRow("email", filledEmail);1870+ }17341871 detailRow("password", "••••••••••••••••");17351872 17361873 const btns = wrap.appendChild(document.createElement("div"));@@ -1744,9 +1881,13 @@17441881 saveBtn.disabled = true;17451882 saveBtn.textContent = "Saving…";17461883 try {1884+ // When both username and email were set, save them in the correct fields.1885+ // Otherwise fall back to storing the identity as the username (current behaviour).1886+ const saveUsername = filledUsername || filledEmail;1887+ const saveEmail = filledUsername ? (filledEmail || undefined) : undefined;17471888 const res = await chrome.runtime.sendMessage({17481889 type: "ADD_ENTRY",1749- payload: { title: sTitle, url: sOrigin, username: filledEmail, password: filledPassword, type: "login" },1890+ payload: { title: sTitle, url: sOrigin, username: saveUsername, email: saveEmail, password: filledPassword, type: "login" },17501891 });17511892 if ((res as { ok?: boolean }).ok) {17521893 showSuccess(sTitle);@@ -1878,8 +2019,8 @@18782019 18792020 entryEl.addEventListener("click", () => {18802021 if (options.isSignup && options.onFillFields) {1881- // Signup mode: clicking a saved identity fills the username field1882- options.onFillFields(e.username ?? "", "");2022+ // Fill username, email, and leave password for the user to confirm2023+ options.onFillFields(e.username ?? "", "", e.email);18832024 dismiss();18842025 } else {18852026 onSelect(e.id);