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
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/index.ts+1 −0src/content/inject.ts+155 −14
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.6.156",5+  "version": "3.6.157",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.6.156",4+  "version": "3.6.157",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.6.156",3+  "version": "3.6.157",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

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;

view file

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

view file