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
↓ peach-browser-8ce923a0.tar.gzatlas at this save
files changed6 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 −58manifest.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": {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",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": {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 ─────────────────────────────────── */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 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">