8ce923a07412

fix mic permission: request audioCapture optional perm before getUserMedia; redesign pro upgrade screen with editorial headline, numbered grid, inline key entry; bump 3.6.13

time
2026-05-18 22:30 UTC · 5 months ago
machine
koh2
parent
20c36630394e
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/styles/popup.css+119 −86src/popup/views/ChirpSync.tsx+14 −0src/popup/views/Settings.tsx+53 −58
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",5-  "version": "3.6.12",5+  "version": "3.6.13",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",4-  "version": "3.6.12",4+  "version": "3.6.13",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.12",3+  "version": "3.6.13",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/styles/popup.css+119 −86
@@ -5946,144 +5946,149 @@59465946 }59475947 59485948 /* ── Pro tier — upgrade page ─────────────────────────────────── */5949+/* ── Pro upgrade — redesigned editorial layout ─────────────────── */5950+59495951 .peach-pro-upgrade {59505952   display: flex;59515953   flex-direction: column;59525954   align-items: stretch;5953-  gap: 12px;5954-  padding: 0 4px 12px;5955+  gap: 14px;5956+  padding: 4px 2px 16px;59555957 }59565958 5957-.peach-pro-upgrade-hero {5958-  display: flex;5959-  flex-direction: column;5960-  align-items: center;5961-  gap: 6px;5962-  padding: 18px 0 10px;5963-  position: relative;5959+.peach-pro-eyebrow {5960+  margin: 0;5961+  font-family: var(--font-ui);5962+  font-size: 0.625rem;5963+  font-weight: 500;5964+  letter-spacing: 0.12em;5965+  color: var(--peach-accent);59645966 }5965-.peach-pro-upgrade-hero::before {5966-  content: "";5967-  position: absolute;5968-  inset: 0;5969-  background: radial-gradient(ellipse 80% 60% at 50% 40%, color-mix(in srgb, var(--peach-accent) 14%, transparent), transparent 70%);5970-  pointer-events: none;5971-}59725967 5973-.peach-pro-upgrade-mark-wrap {5974-  display: flex;5975-  align-items: center;5976-  justify-content: center;5977-  margin-bottom: 4px;5978-  filter: drop-shadow(0 0 10px color-mix(in srgb, var(--peach-accent) 40%, transparent));5979-}5980-5981-.peach-pro-upgrade-mark {5982-  width: 46px;5983-  height: 46px;5984-  opacity: 1;5985-}5986-5987-.peach-pro-upgrade-title {5988-  font-family: var(--font-display, var(--font-ui));5989-  font-size: 24px;5968+.peach-pro-headline {59905969   margin: 0;5970+  font-family: var(--font-display, "Cormorant Garamond", serif);5971+  font-size: 36px;5972+  font-weight: 700;5973+  font-style: normal;5974+  line-height: 1.05;59915975   color: var(--peach-t0);5992-  letter-spacing: 0.01em;5976+  letter-spacing: -0.01em;59935977 }5978+.peach-pro-headline em {5979+  font-style: italic;5980+  font-weight: 400;5981+  color: var(--peach-accent);5982+}59945983 5995-.peach-pro-upgrade-tagline {5984+.peach-pro-subtitle {59965985   margin: 0;5997-  font-style: italic;5998-  color: var(--peach-t1);59995986   font-size: 0.75rem;5987+  line-height: 1.55;5988+  color: var(--peach-t1);60005989 }60015990 6002-.peach-pro-features {5991+/* 2-column numbered feature grid */5992+.peach-pro-grid {5993+  display: grid;5994+  grid-template-columns: 1fr 1fr;5995+  border-top: 1px solid var(--peach-border);5996+  border-left: 1px solid var(--peach-border);5997+}5998+5999+.peach-pro-grid-item {60036000   display: flex;60046001   flex-direction: column;6005-  gap: 9px;6006-  border-top: 1px solid var(--peach-border);6002+  gap: 3px;6003+  padding: 10px 10px 10px 10px;6004+  border-right: 1px solid var(--peach-border);60076005   border-bottom: 1px solid var(--peach-border);6008-  padding: 12px 6px;60096006 }60106007 6011-.peach-pro-feature {6012-  display: flex;6013-  gap: 10px;6014-  align-items: baseline;6008+.peach-pro-grid-item--wide {6009+  grid-column: 1 / -1;60156010 }60166011 6017-.peach-pro-feature-mark {6012+.peach-pro-grid-num {6013+  font-family: var(--font-mono);6014+  font-size: 0.5625rem;60186015   color: var(--peach-accent);6019-  font-size: 0.6875rem;6020-  flex-shrink: 0;6021-  display: inline-block;6022-  transform-origin: center;6023-  opacity: 0.9;6016+  letter-spacing: 0.06em;6017+  opacity: 0.85;60246018 }60256019 6026-.peach-pro-feature-text {6027-  display: flex;6028-  flex-direction: column;6029-  gap: 2px;6030-}6031-6032-.peach-pro-feature-name {6020+.peach-pro-grid-name {60336021   font-size: 0.8125rem;6022+  font-weight: 500;60346023   color: var(--peach-t0);6024+  line-height: 1.2;60356025 }60366026 6037-.peach-pro-feature-desc {6038-  font-size: 0.6875rem;6027+.peach-pro-grid-desc {6028+  font-size: 0.625rem;60396029   color: var(--peach-t1);6030+  line-height: 1.4;60406031 }60416032 6042-.peach-pro-feature.is-highlighted .peach-pro-feature-mark {6033+.peach-pro-grid-item.is-highlighted .peach-pro-grid-num {60436034   animation: peach-pulse-accent 600ms ease-out forwards;60446035 }60456036 60466037 @keyframes peach-pulse-accent {6047-  0%   { opacity: 1; transform: scale(1.4); }6048-  100% { opacity: 0.9; transform: scale(1); }6038+  0%   { opacity: 1; transform: scale(1.2); }6039+  100% { opacity: 0.85; transform: scale(1); }60496040 }60506041 6042+/* Actions */60516043 .peach-pro-upgrade-actions {60526044   display: flex;60536045   flex-direction: column;6054-  align-items: center;6046+  align-items: stretch;60556047   gap: 10px;6056-  padding-top: 2px;60576048 }60586049 60596050 .peach-pro-pricing {60606051   margin: 0;6061-  text-align: center;6062-  font-size: 0.75rem;6052+  font-size: 1rem;6053+  font-weight: 600;60636054   color: var(--peach-t0);6064-  letter-spacing: 0.05em;6055+  letter-spacing: 0.01em;60656056 }6057+.peach-pro-pricing-sub {6058+  font-size: 0.75rem;6059+  font-weight: 400;6060+  color: var(--peach-t1);6061+  letter-spacing: 0.02em;6062+}60666063 60676064 .peach-pro-upgrade-cta {6068-  align-self: center;6069-  text-align: center;6065+  display: flex;6066+  align-items: center;6067+  justify-content: center;60706068   text-decoration: none;6071-  display: block;6072-  padding: 10px 36px;6069+  background: color-mix(in srgb, var(--peach-accent) 18%, transparent);6070+  border: 1px solid var(--peach-accent);6071+  border-radius: 10px;6072+  color: var(--peach-accent);6073+  font-family: var(--font-ui);60736074   font-size: 0.8125rem;6074-  min-width: 190px;6075+  font-weight: 600;6076+  letter-spacing: 0.06em;6077+  padding: 11px 16px;6078+  transition: background 0.15s ease, color 0.15s ease;60756079 }6080+.peach-pro-upgrade-cta:hover {6081+  background: color-mix(in srgb, var(--peach-accent) 28%, transparent);6082+}60766083 60776084 .peach-pro-key-divider {60786085   display: flex;60796086   align-items: center;60806087   gap: 8px;6081-  width: 100%;60826088   color: var(--peach-t2);6083-  font-size: 0.625rem;6084-  letter-spacing: 0.08em;6089+  font-size: 0.5625rem;6090+  letter-spacing: 0.1em;60856091 }6086-60876092 .peach-pro-key-divider::before,60886093 .peach-pro-key-divider::after {60896094   content: "";@@ -6092,21 +6097,49 @@60926097   background: var(--peach-border);60936098 }60946099 6095-.peach-pro-key-link {6100+/* Inline key entry row */6101+.peach-pro-key-inline {6102+  display: flex;6103+  align-items: center;6104+  background: color-mix(in srgb, var(--peach-t0) 4%, transparent);6105+  border: 1px solid var(--peach-border);6106+  border-radius: 8px;6107+  overflow: hidden;6108+}6109+.peach-pro-key-inline-input {6110+  flex: 1;60966111   background: transparent;60976112   border: none;6098-  border-radius: 8px;6113+  outline: none;6114+  color: var(--peach-t1);6115+  font-family: var(--font-mono);6116+  font-size: 0.625rem;6117+  letter-spacing: 0.06em;6118+  padding: 9px 10px;6119+}6120+.peach-pro-key-inline-input::placeholder {60996121   color: var(--peach-t2);6100-  font-size: 0.75rem;6122+}6123+.peach-pro-key-apply {6124+  background: transparent;6125+  border: none;6126+  border-left: 1px solid var(--peach-border);6127+  color: var(--peach-t2);6128+  font-family: var(--font-ui);6129+  font-size: 0.5625rem;6130+  font-weight: 600;6131+  letter-spacing: 0.08em;6132+  padding: 9px 10px;61016133   cursor: pointer;6102-  align-self: stretch;6103-  padding: 6px 12px;6104-  text-align: center;6105-  transition: color 0.2s;6134+  transition: color 0.15s ease;6135+  flex-shrink: 0;61066136 }6107-6108-.peach-pro-key-link:hover {6137+.peach-pro-key-apply:not(:disabled):hover {61096138   color: var(--peach-accent);6139+}6140+.peach-pro-key-apply:disabled {6141+  opacity: 0.35;6142+  cursor: not-allowed;61106143 }61116144 61126145 /* ── Pro tier — key ceremony ─────────────────────────────────── */

view file

src/popup/views/ChirpSync.tsx+14 −0
@@ -70,6 +70,20 @@7070 7171   async function handleStartPairing(): Promise<void> {7272     setError(null);73+74+    // audioCapture is declared as optional_permissions in the manifest.75+    // Chrome will silently deny getUserMedia unless we request it here,76+    // during a user gesture. Firefox ignores this API but getUserMedia works77+    // under its existing host permission grant.78+    if (typeof chrome !== "undefined" && chrome.permissions?.request) {79+      const granted = await chrome.permissions.request({ permissions: ["audioCapture"] });80+      if (!granted) {81+        setPairing({ phase: "mic_denied" });82+        setMode("pairing");83+        return;84+      }85+    }86+7387     setPairing({ phase: "idle" });7488     setMode("pairing");7589 

view file

src/popup/views/Settings.tsx+53 −58
@@ -1440,84 +1440,79 @@14401440         <Show14411441           when={isPro()}14421442           fallback={1443-            <Show1444-              when={showKeyEntry()}1445-              fallback={14461443                 <div class="peach-pro-upgrade">1447-                  <div class="peach-pro-upgrade-hero">1448-                    <div class="peach-pro-upgrade-mark-wrap">1449-                      <PeachMark class="peach-pro-upgrade-mark" />1450-                    </div>1451-                    <h2 class="peach-pro-upgrade-title">Peach Pro</h2>1452-                    <p class="peach-pro-upgrade-tagline">Everything on every device.</p>1453-                  </div>1444+                  <p class="peach-pro-eyebrow">PEACH PRO · ONE-TIME</p>14541445 1455-                  <div class="peach-pro-features">1456-                    <For each={PRO_FEATURES}>1457-                      {(item) => (1446+                  <h2 class="peach-pro-headline">1447+                    Every<em>thing,</em><br />every<em>where.</em>1448+                  </h2>1449+1450+                  <p class="peach-pro-subtitle">1451+                    A single key unlocks sync, mobile, passkeys and our most thorough vault health tooling — for the lifetime of the app.1452+                  </p>1453+1454+                  <div class="peach-pro-grid">1455+                    <For each={PRO_FEATURES.slice(0, 4)}>1456+                      {(item, i) => (14581457                         <div1459-                          class="peach-pro-feature"1458+                          class="peach-pro-grid-item"14601459                           classList={{ "is-highlighted": props.upgradeHighlight === item.feature }}14611460                         >1462-                          <span class="peach-pro-feature-mark">✦</span>1463-                          <div class="peach-pro-feature-text">1464-                            <span class="peach-pro-feature-name">{item.name}</span>1465-                            <span class="peach-pro-feature-desc">{item.desc}</span>1466-                          </div>1461+                          <span class="peach-pro-grid-num">{String(i() + 1).padStart(2, "0")}</span>1462+                          <span class="peach-pro-grid-name">{item.name}</span>1463+                          <span class="peach-pro-grid-desc">{item.desc}</span>14671464                         </div>14681465                       )}14691466                     </For>1467+                    <Show when={PRO_FEATURES[4]}>1468+                      <div1469+                        class="peach-pro-grid-item peach-pro-grid-item--wide"1470+                        classList={{ "is-highlighted": props.upgradeHighlight === PRO_FEATURES[4].feature }}1471+                      >1472+                        <span class="peach-pro-grid-num">05</span>1473+                        <span class="peach-pro-grid-name">{PRO_FEATURES[4].name}</span>1474+                        <span class="peach-pro-grid-desc">{PRO_FEATURES[4].desc}</span>1475+                      </div>1476+                    </Show>14701477                   </div>14711478 14721479                   <div class="peach-pro-upgrade-actions">1473-                    <p class="peach-pro-pricing">$39 · once · yours forever</p>1480+                    <p class="peach-pro-pricing">1481+                      $39 <span class="peach-pro-pricing-sub">once · yours forever</span>1482+                    </p>14741483                     <a14751484                       href="https://peachpasswords.com/#pricing"14761485                       target="_blank"14771486                       rel="noopener noreferrer"1478-                      class="peach-btn peach-btn--accent peach-pro-upgrade-cta"1487+                      class="peach-pro-upgrade-cta"14791488                     >14801489                       Get Peach Pro14811490                     </a>1482-                    <div class="peach-pro-key-divider"><span>or</span></div>1483-                    <button class="peach-pro-key-link" onClick={() => setShowKeyEntry(true)}>1484-                      Activate a license key1485-                    </button>1491+                    <div class="peach-pro-key-divider"><span>ALREADY HAVE A KEY</span></div>1492+                    <div class="peach-pro-key-inline">1493+                      <input1494+                        class="peach-pro-key-inline-input"1495+                        type="text"1496+                        placeholder="PEACH-XXXX-XXXX-XXXX"1497+                        value={licenseKeyInput()}1498+                        onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1499+                        onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1500+                        autocomplete="off"1501+                        spellcheck={false}1502+                      />1503+                      <button1504+                        class="peach-pro-key-apply"1505+                        onClick={() => void onActivate()}1506+                        disabled={activatingLicense() || !licenseKeyInput().trim()}1507+                      >1508+                        {activatingLicense() ? "…" : "APPLY"}1509+                      </button>1510+                    </div>1511+                    <Show when={licenseError()}>1512+                      <p class="peach-pro-key-error">{licenseError()}</p>1513+                    </Show>14861514                   </div>14871515                 </div>1488-              }1489-            >1490-              <div class="peach-pro-key-ceremony">1491-                <LockRuneCanvas />1492-                <p class="peach-pro-key-ceremony-label">Enter your license key</p>1493-                <input1494-                  class="peach-underline-input peach-pro-key-input"1495-                  type="text"1496-                  placeholder="PPK-XXXX-XXXX-XXXX-XXXX-XXXX"1497-                  value={licenseKeyInput()}1498-                  onInput={(e) => setLicenseKeyInput(e.currentTarget.value)}1499-                  onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }}1500-                  autocomplete="off"1501-                  spellcheck={false}1502-                />1503-                <Show when={licenseError()}>1504-                  <p class="peach-pro-key-error">That key doesn't look right.</p>1505-                </Show>1506-                <button1507-                  class="peach-btn peach-btn--accent peach-pro-key-activate"1508-                  onClick={() => void onActivate()}1509-                  disabled={activatingLicense() || !licenseKeyInput().trim()}1510-                >1511-                  {activatingLicense() ? "activating…" : "Activate"}1512-                </button>1513-                <button1514-                  class="peach-pro-key-cancel"1515-                  onClick={() => { setShowKeyEntry(false); setLicenseError(null); setLicenseKeyInput(""); }}1516-                >1517-                  cancel1518-                </button>1519-              </div>1520-            </Show>15211516           }15221517         >15231518           <div class="peach-pro-active">

view file