cba62ca5b081
remove all Android/Capacitor code from popup views — App, Settings, Lock, SyncPairing, Onboarding
- time
- 2026-05-04 22:18 UTC · 5 months ago
- machine
- koh2
- parent
- 31184beaf504
↓ peach-browser-cba62ca5.tar.gzatlas at this save
files changed5 files
src/popup/App.tsx+49 −450src/popup/views/Lock.tsx+1 −69src/popup/views/Onboarding.tsx+1 −74src/popup/views/Settings.tsx+2 −71src/popup/views/SyncPairing.tsx+2 −10src/popup/App.tsx+49 −450
@@ -1,12 +1,6 @@11 import { createEffect, createMemo, createSignal, lazy, onCleanup, onMount, Show, Suspense } from "solid-js";2-import { App as CapacitorApp } from "@capacitor/app";3-import { Capacitor, registerPlugin } from "@capacitor/core";4-import { Keyboard } from "@capacitor/keyboard";5-import { StatusBar, Style } from "@capacitor/status-bar";62 import { storageGet, storageSet } from "../shared/storage";73 import { sendMessage } from "../shared/bridge";8-import { clearInMemoryVault, publishInMemoryVault } from "../shared/in-memory-vault";9-import { startFoldMonitoring, type PostureChangedEvent } from "../shared/fold";104 import { ACCENT_COLORS } from "../shared/types";115 import type { AccentColorName, PendingPasskeyRequest, ProFeature, VaultEntry } from "../shared/types";126 import type { IncomingPairingRequest } from "../shared/sync-types";@@ -42,16 +36,6 @@4236 const PanicView = lazy(() => import("./views/Panic").then(m => ({ default: m.PanicView })));4337 const SyncPairingView = lazy(() => import("./views/SyncPairing").then(m => ({ default: m.SyncPairingView })));4438 45-const Autofill = registerPlugin<{46- checkPendingSave(): Promise<{ pending: string | null }>;47- openAutofillSettings(): Promise<void>;48- isEnabledAsAutofillService(): Promise<{ enabled: boolean }>;49- consumePendingAutofillRequest(): Promise<{50- pending: { domain: string; package: string } | null;51- }>;52- completeAutofillAuthentication(options: { domain: string }): Promise<void>;53-}>("Autofill");54-5539 type View =5640 | "lock"5741 | "pro-celebration"@@ -134,8 +118,6 @@134118 "enter"135119 );136120 const [contentTransitioning, setContentTransitioning] = createSignal(false);137- const [layoutClass, setLayoutClass] = createSignal<"compact" | "medium" | "expanded">("compact");138- const isTwoPaneLayout = () => layoutClass() === "medium" || layoutClass() === "expanded";139121 const [hasVault, setHasVault] = createSignal(false);140122 const [onboarded, setOnboarded] = createSignal(true);141123 const [entries, setEntries] = createSignal<VaultEntry[]>([]);@@ -168,16 +150,6 @@168150 const [listSearchSeedNonce, setListSearchSeedNonce] = createSignal(0);169151 const [pendingCelebration, setPendingCelebration] = createSignal(false);170152 const [pendingProNotice, setPendingProNotice] = createSignal<{ feature: string } | null>(null);171- const [pendingAutofillSave, setPendingAutofillSave] = createSignal<{172- domain: string;173- username: string;174- password: string;175- } | null>(null);176- const [pendingAutofillRequest, setPendingAutofillRequest] = createSignal<{177- domain: string;178- pkg: string;179- } | null>(null);180- const [backExitNotice, setBackExitNotice] = createSignal(false);181153 const [incomingPair, setIncomingPair] = createSignal<IncomingPairingRequest | null>(null);182154 const [recentPairToast, setRecentPairToast] = createSignal<string | null>(null);183155 const [incomingPairResponding, setIncomingPairResponding] = createSignal(false);@@ -192,10 +164,8 @@192164 // onBack fires we return to Panic instead of Settings so the user keeps the193165 // destruction context they came from.194166 const [returnToPanic, setReturnToPanic] = createSignal(false);195- const [showGestureHints, setShowGestureHints] = createSignal(false);196167 197168 let contentPhaseTimer: number | undefined;198- let lastBackExitAt = 0;199169 200170 function applyAccent(name: AccentColorName) {201171 const color = ACCENT_COLORS.find(c => c.name === name) ?? ACCENT_COLORS[0]!;@@ -215,9 +185,6 @@215185 createEffect(() => {216186 document.documentElement.classList.toggle("light", theme() === "light");217187 document.body.classList.toggle("light", theme() === "light");218- if (Capacitor.isNativePlatform()) {219- void applyMobileStatusBar(theme());220- }221188 });222189 223190 createEffect(() => {@@ -311,211 +278,7 @@311278 });312279 }313280 314- async function applyMobileStatusBar(nextTheme: "dark" | "light") {315- try {316- await StatusBar.setStyle({ style: nextTheme === "light" ? Style.Light : Style.Dark });317- await StatusBar.setBackgroundColor({318- color: nextTheme === "light" ? "#EEE3CC" : "#0C0A06",319- });320- } catch {321- // Status bar APIs are available only in the native shell.322- }323- }324-325- function handleBackNavigation() {326- if (showShortcutsHelp()) {327- setShowShortcutsHelp(false);328- return;329- }330- if (restoreView() !== null) {331- setRestoreView(null);332- return;333- }334-335- switch (view()) {336- case "list":337- setView("entry");338- return;339- case "settings":340- case "add":341- case "import":342- handleBack();343- return;344- case "export":345- if (returnToPanic()) {346- setReturnToPanic(false);347- setView("panic");348- } else {349- handleBack();350- }351- return;352- case "health":353- case "panic":354- case "sync-pairing":355- setView("settings");356- return;357- case "trash":358- setView("list");359- return;360- case "entry":361- case "lock": {362- const now = Date.now();363- if (now - lastBackExitAt < 1600) {364- void CapacitorApp.exitApp();365- return;366- }367- lastBackExitAt = now;368- setBackExitNotice(true);369- window.setTimeout(() => setBackExitNotice(false), 1400);370- return;371- }372- default:373- void CapacitorApp.exitApp();374- }375- }376-377281 onMount(async () => {378- if (Capacitor.isNativePlatform()) {379- document.body.classList.add("peach-mobile");380- document.documentElement.classList.add("peach-mobile-root");381- void applyMobileStatusBar(theme());382-383- // Window Size Class detection — compact/medium/expanded matches Android's384- // breakpoints. Drives two-pane layout on Z Fold inner display + tablets.385- const updateLayoutClass = () => {386- const w = window.innerWidth;387- const cls = document.body.classList;388- cls.remove("layout-compact", "layout-medium", "layout-expanded");389- const next: "compact" | "medium" | "expanded" =390- w >= 840 ? "expanded" : w >= 600 ? "medium" : "compact";391- cls.add(`layout-${next}`);392- setLayoutClass(next);393- };394- updateLayoutClass();395- window.addEventListener("resize", updateLayoutClass);396-397- // launchMode=singleTask: a re-fired autofill auth intent triggers398- // onNewIntent in MainActivity, which dispatches this window event.399- // Re-consume so the lock screen surfaces the pending request.400- const onAutofillRequested = () => {401- void (async () => {402- try {403- const { pending } = await Autofill.consumePendingAutofillRequest();404- if (pending && (pending.domain || pending.package)) {405- setPendingAutofillRequest({406- domain: pending.domain || pending.package,407- pkg: pending.package || "",408- });409- }410- } catch { /* best effort */ }411- })();412- };413- window.addEventListener("peach:autofill-requested", onAutofillRequested);414-415- // When Peach foregrounds, re-check for a pending autofill save (the user416- // may have submitted a form in another app while we were backgrounded)417- // and re-consume any new autofill request extras.418- const appState = await CapacitorApp.addListener("appStateChange", (state) => {419- if (!state.isActive) return;420- void (async () => {421- try {422- const { pending } = await Autofill.checkPendingSave();423- if (pending) {424- const parsed = JSON.parse(pending) as {425- domain?: string;426- username?: string;427- password?: string;428- };429- if (parsed.username && parsed.password) {430- setPendingAutofillSave({431- domain: parsed.domain ?? "",432- username: parsed.username,433- password: parsed.password,434- });435- }436- }437- } catch { /* best effort */ }438- onAutofillRequested();439- })();440- });441-442- // Foldable posture (Z Fold tabletop / book) — set data-posture and a443- // CSS variable for hinge height so layouts can pad away from the crease.444- const stopFold = await startFoldMonitoring((e: PostureChangedEvent) => {445- document.body.setAttribute("data-posture", e.posture);446- document.documentElement.style.setProperty(447- "--peach-hinge-height",448- `${e.hingeHeight}px`,449- );450- });451-452- const keyboardShow = await Keyboard.addListener("keyboardWillShow", (info) => {453- document.body.style.paddingBottom = `${info.keyboardHeight}px`;454- });455- const keyboardHide = await Keyboard.addListener("keyboardWillHide", () => {456- document.body.style.paddingBottom = "0";457- });458- const backButton = await CapacitorApp.addListener("backButton", () => {459- handleBackNavigation();460- });461-462- let touchStartX = 0;463- let touchStartY = 0;464- let touchStartTime = 0;465- const handleTouchStart = (event: TouchEvent) => {466- const touch = event.touches[0];467- if (!touch) return;468- touchStartX = touch.clientX;469- touchStartY = touch.clientY;470- touchStartTime = Date.now();471- };472- const handleTouchEnd = (event: TouchEvent) => {473- const touch = event.changedTouches[0];474- if (!touch) return;475- const dx = touch.clientX - touchStartX;476- const rawDy = touch.clientY - touchStartY;477- const absDx = Math.abs(dx);478- const absDy = Math.abs(rawDy);479- const dt = Date.now() - touchStartTime;480-481- // Left-edge swipe right → back482- if (touchStartX < 30 && dx > 60 && absDy < 40) {483- handleBackNavigation();484- return;485- }486-487- // Swipe gestures only in the main entry view with no overlay488- if (view() === "entry" && !isShellOverlayOpen() && !showGestureHints()) {489- // Swipe up from the lower portion of the screen → open vault list490- if (rawDy < -80 && absDx < 60 && dt < 500491- && touchStartY > window.innerHeight * 0.4) {492- openList();493- return;494- }495- // Horizontal swipe → navigate entries (not starting from left edge)496- if (absDx > 60 && absDy < 40 && dt < 400 && touchStartX >= 30) {497- navigate(dx < 0 ? 1 : -1);498- return;499- }500- }501- };502- document.addEventListener("touchstart", handleTouchStart, { passive: true });503- document.addEventListener("touchend", handleTouchEnd, { passive: true });504-505- onCleanup(() => {506- void keyboardShow.remove();507- void keyboardHide.remove();508- void backButton.remove();509- void appState.remove();510- stopFold();511- window.removeEventListener("peach:autofill-requested", onAutofillRequested);512- window.removeEventListener("resize", updateLayoutClass);513- document.removeEventListener("touchstart", handleTouchStart);514- document.removeEventListener("touchend", handleTouchEnd);515- document.body.style.paddingBottom = "0";516- });517- }518-519282 // Resolve the active tab in parallel with the unlock/has-vault queries —520283 // openVault()'s smart selection picks the entry matching currentTabDomain,521284 // so it must be populated before openVault runs. Previously refreshCurrentTab@@ -575,28 +338,26 @@575338 await consumeRecentPairing();576339 577340 // Track tab changes so smart sort stays current578- if (!Capacitor.isNativePlatform()) {579- const handleTabActivated = () => { void refreshCurrentTab(); };580- const handleTabUpdated = (_tabId: number, changeInfo: { url?: string; status?: string }) => {581- if (changeInfo.url || changeInfo.status === "complete") void refreshCurrentTab();582- };583- try {584- chrome.tabs.onActivated.addListener(handleTabActivated);585- chrome.tabs.onUpdated.addListener(handleTabUpdated);586- onCleanup(() => {587- chrome.tabs.onActivated.removeListener(handleTabActivated);588- chrome.tabs.onUpdated.removeListener(handleTabUpdated);589- });590- } catch {591- // tabs permission may not be available in all contexts592- }341+ const handleTabActivated = () => { void refreshCurrentTab(); };342+ const handleTabUpdated = (_tabId: number, changeInfo: { url?: string; status?: string }) => {343+ if (changeInfo.url || changeInfo.status === "complete") void refreshCurrentTab();344+ };345+ try {346+ chrome.tabs.onActivated.addListener(handleTabActivated);347+ chrome.tabs.onUpdated.addListener(handleTabUpdated);348+ onCleanup(() => {349+ chrome.tabs.onActivated.removeListener(handleTabActivated);350+ chrome.tabs.onUpdated.removeListener(handleTabUpdated);351+ });352+ } catch {353+ // tabs permission may not be available in all contexts593354 }594355 595356 setBootState("ready");596357 document.getElementById("pre-lock")?.setAttribute("hidden", "true");597358 598359 // Check for pending session-storage notices from the background.599- if (!Capacitor.isNativePlatform() && chrome.storage.session) {360+ if (chrome.storage.session) {600361 const session = await chrome.storage.session.get(["pendingProGate", "pendingProNotice"]) as601362 Record<string, { feature?: string } | undefined>;602363 if (session.pendingProGate?.feature) {@@ -610,41 +371,6 @@610371 }611372 }612373 613- if (Capacitor.getPlatform() === "android") {614- try {615- const { pending } = await Autofill.checkPendingSave();616- if (pending) {617- const parsed = JSON.parse(pending) as {618- domain?: string;619- username?: string;620- password?: string;621- };622- if (parsed.username && parsed.password) {623- setPendingAutofillSave({624- domain: parsed.domain ?? "",625- username: parsed.username,626- password: parsed.password,627- });628- }629- }630- } catch {631- // Pending autofill saves are opportunistic; boot must continue.632- }633-634- // Locked-vault autofill flow: was the activity opened via the autofill635- // auth intent? If so, surface "Unlock to fill X" and complete the auth636- // dataset once the vault unlocks.637- try {638- const { pending } = await Autofill.consumePendingAutofillRequest();639- if (pending && (pending.domain || pending.package)) {640- setPendingAutofillRequest({641- domain: pending.domain || pending.package,642- pkg: pending.package || "",643- });644- }645- } catch { /* best effort */ }646- }647-648374 // Warm the lazy-module cache so the Suspense fallback never shows for649375 // locally-bundled extension views. Without this, the first navigation to650376 // Settings/Import/Export/etc can briefly mount `.peach-loading`, which@@ -660,29 +386,27 @@660386 // Lock-on-close via a persistent port. sendMessage in pagehide is661387 // unreliable — the popup context tears down before the message lands.662388 // The SW's onConnect listener locks when this port disconnects.663- if (!Capacitor.isNativePlatform()) {664- let popupPort: chrome.runtime.Port | null = null;665- try {666- popupPort = chrome.runtime.connect({ name: "popup" });667- } catch { /* extension context invalidated — ignore */ }668- onCleanup(() => { try { popupPort?.disconnect(); } catch {} });389+ let popupPort: chrome.runtime.Port | null = null;390+ try {391+ popupPort = chrome.runtime.connect({ name: "popup" });392+ } catch { /* extension context invalidated — ignore */ }393+ onCleanup(() => { try { popupPort?.disconnect(); } catch {} });669394 670- // Background auto-lock broadcasts VAULT_LOCKED so the popup drops stale671- // entries instead of waiting for the next GET_ENTRIES round-trip.672- const handleRuntimeMessage = (msg: { type?: string; payload?: { feature?: ProFeature } }) => {673- if (msg?.type === "VAULT_LOCKED" && view() !== "lock") handleLock();674- if (msg?.type === "PRO_GATE_TRIGGERED") {675- setUpgradeHighlight(msg.payload?.feature ?? null);676- setSettingsInitialSection("license");677- setView("settings");678- }679- if (msg?.type === "INCOMING_PAIRING_REQUEST") {680- void refreshIncomingPairing();681- }682- };683- chrome.runtime.onMessage.addListener(handleRuntimeMessage);684- onCleanup(() => chrome.runtime.onMessage.removeListener(handleRuntimeMessage));685- }395+ // Background auto-lock broadcasts VAULT_LOCKED so the popup drops stale396+ // entries instead of waiting for the next GET_ENTRIES round-trip.397+ const handleRuntimeMessage = (msg: { type?: string; payload?: { feature?: ProFeature } }) => {398+ if (msg?.type === "VAULT_LOCKED" && view() !== "lock") handleLock();399+ if (msg?.type === "PRO_GATE_TRIGGERED") {400+ setUpgradeHighlight(msg.payload?.feature ?? null);401+ setSettingsInitialSection("license");402+ setView("settings");403+ }404+ if (msg?.type === "INCOMING_PAIRING_REQUEST") {405+ void refreshIncomingPairing();406+ }407+ };408+ chrome.runtime.onMessage.addListener(handleRuntimeMessage);409+ onCleanup(() => chrome.runtime.onMessage.removeListener(handleRuntimeMessage));686410 687411 // Action handlers — keyed by ShortcutAction. The dispatcher below tests688412 // each binding against the event and runs the matching handler. New@@ -791,24 +515,21 @@791515 window.addEventListener("keydown", handleGlobalKeydown);792516 onCleanup(() => window.removeEventListener("keydown", handleGlobalKeydown));793517 794- if (!Capacitor.isNativePlatform()) {795- const handleStorageChanged = (796- changes: Record<string, chrome.storage.StorageChange>,797- areaName: string798- ) => {799- if (areaName !== "session") return;800- if (!Object.keys(changes).some((key) => key.startsWith("passkey_pending_"))) {801- return;802- }803- void refreshPendingPasskey();804- };805- chrome.storage.onChanged.addListener(handleStorageChanged);806- onCleanup(() => chrome.storage.onChanged.removeListener(handleStorageChanged));807- }518+ const handleStorageChanged = (519+ changes: Record<string, chrome.storage.StorageChange>,520+ areaName: string521+ ) => {522+ if (areaName !== "session") return;523+ if (!Object.keys(changes).some((key) => key.startsWith("passkey_pending_"))) {524+ return;525+ }526+ void refreshPendingPasskey();527+ };528+ chrome.storage.onChanged.addListener(handleStorageChanged);529+ onCleanup(() => chrome.storage.onChanged.removeListener(handleStorageChanged));808530 });809531 810532 async function refreshCurrentTab() {811- if (Capacitor.isNativePlatform()) return;812533 try {813534 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });814535 const url = tab?.url ?? tab?.pendingUrl ?? "";@@ -937,16 +658,6 @@937658 938659 const loadedEntries = res.data ?? [];939660 setEntries(loadedEntries);940- void publishInMemoryVault(loadedEntries).then(() => {941- // If we were opened by Android's autofill auth intent (vault was locked),942- // entries are now published to the native InMemoryVault — finish the943- // auth dataset so the framework fills the originating app's fields.944- const req = pendingAutofillRequest();945- if (req) {946- setPendingAutofillRequest(null);947- void Autofill.completeAutofillAuthentication({ domain: req.domain });948- }949- });950661 951662 const currentId = selectedEntry()?.id;952663 if (currentId) {@@ -1002,7 +713,7 @@1002713 setContentTransitioning(false);1003714 setContentPhase("enter");1004715 queueContentIdle();1005- if (!skipViewSet) setView(isTwoPaneLayout() ? "list" : "entry");716+ if (!skipViewSet) setView("entry");1006717 }1007718 1008719 async function handleUnlock() {@@ -1019,13 +730,6 @@1019730 setView("pro-celebration");1020731 window.setTimeout(() => setView("entry"), 2500);1021732 }1022- if (Capacitor.isNativePlatform()) {1023- const shown = await storageGet("peach_mobile_gestures_shown");1024- if (!shown) {1025- await storageSet("peach_mobile_gestures_shown", "1");1026- setShowGestureHints(true);1027- }1028- }1029733 }1030734 1031735 function handleSelectEntry(entry: VaultEntry) {@@ -1033,9 +737,7 @@1033737 setContentTransitioning(false);1034738 setContentPhase("enter");1035739 queueContentIdle();1036- // Two-pane layouts (Z Fold inner display, tablets, DeX) keep the list1037- // mounted on the left and only update the detail pane on selection.1038- if (!isTwoPaneLayout()) setView("entry");740+ setView("entry");1039741 }1040742 1041743 function handleAdd() {@@ -1066,26 +768,6 @@1066768 await openVault(savedEntry?.id);1067769 }1068770 1069- async function savePendingAutofill() {1070- const pending = pendingAutofillSave();1071- if (!pending) return;1072- const domain = pending.domain.trim();1073- const response = await sendMessage({1074- type: "ADD_ENTRY",1075- payload: {1076- type: "login",1077- title: domain || "Saved login",1078- url: domain ? `https://${domain}` : "",1079- username: pending.username,1080- password: pending.password,1081- },1082- });1083- if ((response as { ok: boolean }).ok) {1084- setPendingAutofillSave(null);1085- await openVault((response as { data?: VaultEntry }).data?.id);1086- }1087- }1088-1089771 function handleBack() {1090772 setEditEntry(undefined);1091773 setView("entry");@@ -1098,7 +780,6 @@1098780 setEditEntry(undefined);1099781 setContentPhase("idle");1100782 setView("lock");1101- void clearInMemoryVault();1102783 }1103784 1104785 function handleHeaderLock() {@@ -1238,18 +919,7 @@1238919 data-surface="extension"1239920 >1240921 <header class="peach-chrome" classList={{ "is-hidden-for-overlay": isShellOverlayOpen() }}>1241- <Show1242- when={Capacitor.isNativePlatform()}1243- fallback={<PeachWordmark unlocked />}1244- >1245- <button1246- class="peach-wordmark-btn"1247- onClick={() => setView("settings")}1248- aria-label="Settings"1249- >1250- <PeachWordmark unlocked />1251- </button>1252- </Show>922+ <PeachWordmark unlocked />1253923 <div class="peach-chrome-actions">1254924 <button1255925 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"@@ -1346,76 +1016,6 @@13461016 </div>13471017 </Show>13481018 1349- <Show when={pendingAutofillSave() !== null && !isLockedView()}>1350- <div class="peach-pro-notice-banner peach-autofill-save-banner">1351- <span>1352- Save login{pendingAutofillSave()?.domain ? ` for ${pendingAutofillSave()!.domain}` : ""}?1353- </span>1354- <button1355- class="peach-pro-notice-link"1356- onClick={() => void savePendingAutofill()}1357- >1358- Save1359- </button>1360- <button1361- class="peach-pro-notice-close"1362- onClick={() => setPendingAutofillSave(null)}1363- aria-label="Dismiss"1364- >1365- x1366- </button>1367- </div>1368- </Show>1369-1370- <Show when={pendingAutofillRequest() !== null}>1371- <div class="peach-pro-notice-banner peach-autofill-request-banner">1372- <span>1373- Unlock to autofill{pendingAutofillRequest()?.domain ? ` ${pendingAutofillRequest()!.domain}` : ""}1374- </span>1375- <button1376- class="peach-pro-notice-close"1377- onClick={() => setPendingAutofillRequest(null)}1378- aria-label="Dismiss"1379- >1380- x1381- </button>1382- </div>1383- </Show>1384-1385- <Show when={backExitNotice()}>1386- <div class="peach-pro-notice-banner peach-back-exit-banner">1387- <span>Tap back again to exit Peach</span>1388- </div>1389- </Show>1390-1391- <Show when={showGestureHints()}>1392- <div class="peach-gesture-hints-overlay" onClick={() => setShowGestureHints(false)}>1393- <div class="peach-gesture-hints-card" onClick={(e: MouseEvent) => e.stopPropagation()}>1394- <p class="peach-gesture-hints-title">welcome to peach</p>1395- <ul class="peach-gesture-hints-list">1396- <li>1397- <span class="peach-gesture-glyph">↑</span>1398- <span>swipe up — open vault list</span>1399- </li>1400- <li>1401- <span class="peach-gesture-glyph">← →</span>1402- <span>swipe — switch entries</span>1403- </li>1404- <li>1405- <span class="peach-gesture-glyph">⬡</span>1406- <span>tap peach mark — settings</span>1407- </li>1408- </ul>1409- <button1410- class="peach-btn peach-btn--accent peach-btn--sm"1411- onClick={() => setShowGestureHints(false)}1412- >1413- got it1414- </button>1415- </div>1416- </div>1417- </Show>1418-14191019 <div class="peach-shell-body">14201020 <div class="peach-shell-veil"></div>14211021 <div class="peach-content-frame" data-content-phase={contentPhase()} classList={{ navigating: contentTransitioning() }}>@@ -1605,7 +1205,6 @@16051205 setReturnToPanic(false);16061206 setContentPhase("idle");16071207 setView("lock");1608- void clearInMemoryVault();16091208 }}16101209 />16111210 </Suspense>src/popup/views/Lock.tsx+1 −69
@@ -1,12 +1,9 @@1-import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js";2-import { Capacitor } from "@capacitor/core";3-1+import { For, Match, Show, Switch, createMemo, createSignal, onCleanup, onMount } from "solid-js";42 import { sendMessage } from "../App";53 import { LockRuneCanvas } from "../components/LockRuneCanvas";64 import { PeachMark, PeachWordmark } from "../components/PeachMark";75 import { RuneBackground } from "../components/RuneBackground";86 import { ImportView } from "./Import";9-import { biometricUnlock, biometricAvailable } from "../../shared/biometric";107 import { storageGet, storageSet } from "../../shared/storage";118 import { isPeachScript, decodePeachScript, normalisePeachScript } from "../../crypto/peachScript";129 import { parsePayload } from "../../crypto/peachPayload";@@ -251,10 +248,6 @@251248 >252249 {loading() ? "unlocking..." : "unlock"}253250 </button>254-255- <Show when={Capacitor.isNativePlatform()}>256- <BiometricUnlockButton ready={props.ready} loading={loading} onUnlock={props.onUnlock} />257- </Show>258251 </section>259252 </section>260253 </Match>@@ -841,10 +834,6 @@841834 const [restoring, setRestoring] = createSignal(false);842835 const [error, setError] = createSignal<string | null>(null);843836 844- const isAndroid = createMemo(() =>845- Capacitor.isNativePlatform() && Capacitor.getPlatform() === "android"846- );847-848837 async function processFile(file: File) {849838 setDecoding(true);850839 setDecoded(null);@@ -872,24 +861,6 @@872861 }873862 }874863 875- async function handleCameraScan() {876- setError(null);877- try {878- const { Camera, CameraResultType, CameraSource } = await import("@capacitor/camera");879- const image = await Camera.getPhoto({ quality: 90, resultType: CameraResultType.Base64, source: CameraSource.Camera, allowEditing: false });880- if (!image.base64String) return;881- setDecoding(true);882- const result = await handleMayanDecode(image.base64String);883- setDecoding(false);884- if (!result.ok) { setError(result.reason); return; }885- setDecoded(result.payload);886- } catch (err) {887- setDecoding(false);888- const msg = err instanceof Error ? err.message : "";889- if (msg && !/cancel/i.test(msg)) setError(msg);890- }891- }892-893864 async function handleRestore() {894865 const bytes = decoded();895866 if (!bytes || !backupPass().trim() || restoring()) return;@@ -949,11 +920,6 @@949920 disabled={decoding()}950921 />951922 </label>952- <Show when={isAndroid()}>953- <button class="peach-btn peach-btn--ghost peach-btn--sm" onClick={() => void handleCameraScan()} disabled={decoding()}>954- scan with camera955- </button>956- </Show>957923 <Show when={error()}>958924 <p class="peach-rt-error">{error()}</p>959925 </Show>@@ -1230,40 +1196,6 @@12301196 </button>12311197 </Show>12321198 </div>1233- );1234-}1235-1236-// ── Biometric unlock ────────────────────────────────────────────────────────1237-1238-async function handleBiometricUnlock(props: { onUnlock: () => void | Promise<void> }) {1239- const unlocked = await biometricUnlock();1240- if (unlocked) {1241- await props.onUnlock();1242- }1243-}1244-1245-function BiometricUnlockButton(props: { ready: boolean; loading: () => boolean; onUnlock: () => void | Promise<void> }) {1246- const [bioAvailable, setBioAvailable] = createSignal(false);1247- onMount(async () => {1248- setBioAvailable(await biometricAvailable());1249- });1250- return (1251- <Show when={bioAvailable()}>1252- <button1253- class="peach-btn peach-btn--subtle peach-btn--md peach-biometric-btn"1254- onClick={() => void handleBiometricUnlock(props)}1255- disabled={!props.ready || props.loading()}1256- type="button"1257- >1258- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">1259- <path d="M8 1a3 3 0 0 0-3 3v4a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />1260- <path d="M5 8v1a3 3 0 0 0 6 0V8" />1261- <line x1="8" y1="12" x2="8" y2="15" />1262- <line x1="5" y1="15" x2="11" y2="15" />1263- </svg>1264- use biometrics1265- </button>1266- </Show>12671199 );12681200 }12691201 src/popup/views/Onboarding.tsx+1 −74
@@ -1,16 +1,12 @@11 import { For, Show, createSignal } from "solid-js";2-import { Capacitor } from "@capacitor/core";3-42 import { PeachWordmark } from "../components/PeachMark";53 import { RuneBackground } from "../components/RuneBackground";6-import { registerPlugin } from "@capacitor/core";74 import { storageSet } from "../../shared/storage";85 96 interface OnboardingCard {107 headline: string;118 body: string[];129 warning: string | null;13- mobileCard?: boolean;1410 }1511 1612 const BASE_CARDS: OnboardingCard[] = [@@ -41,31 +37,8 @@4137 },4238 ];4339 44-function getAutofillPlugin() {45- try {46- return registerPlugin<{47- openAutofillSettings(): Promise<void>;48- }>("Autofill");49- } catch {50- return null;51- }52-}40+const CARDS: OnboardingCard[] = BASE_CARDS;5341 54-const AutofillPlugin = getAutofillPlugin();55-56-const MOBILE_CARD: OnboardingCard = {57- headline: "Enable autofill.",58- body: [59- "Peach fills passwords in Chrome, Safari, and every app on your phone.",60- ],61- warning: null,62- mobileCard: true,63-};64-65-const CARDS: OnboardingCard[] = Capacitor.isNativePlatform()66- ? [...BASE_CARDS, MOBILE_CARD]67- : BASE_CARDS;68-6942 const CARD_EXIT_MS = 120;7043 const CARD_ENTER_GAP_MS = 60;7144 @@ -80,8 +53,6 @@8053 8154 async function advance() {8255 if (exiting() || completing()) return;83- // Mobile card has its own buttons — don't advance on click84- if (CARDS[card()]?.mobileCard) return;8556 if (card() < CARDS.length - 1) {8657 setExiting(true);8758 window.setTimeout(() => {@@ -101,33 +72,6 @@10172 }10273 }10374 104- async function handleEnableAutofill() {105- if (AutofillPlugin) {106- await AutofillPlugin.openAutofillSettings();107- }108- setCompleting(true);109- try {110- await storageSet("peach_onboarded", "true");111- } catch {112- // storage unavailable — still complete the flow113- }114- window.setTimeout(() => {115- void props.onComplete();116- }, 200);117- }118-119- async function handleSkipAutofill() {120- setCompleting(true);121- try {122- await storageSet("peach_onboarded", "true");123- } catch {124- // storage unavailable — still complete the flow125- }126- window.setTimeout(() => {127- void props.onComplete();128- }, 200);129- }130-13175 return (13276 <section13377 class="peach-screen peach-theme peach-onboarding"@@ -164,23 +108,6 @@164108 <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>165109 <span>{CARDS[card()]!.warning}</span>166110 </p>167- </Show>168-169- <Show when={CARDS[card()]?.mobileCard}>170- <div class="peach-onboarding-mobile-card-buttons">171- <button172- class="peach-btn peach-btn--accent peach-btn--md"173- onClick={(e) => { e.stopPropagation(); handleEnableAutofill(); }}174- >175- Enable Peach autofill176- </button>177- <button178- class="peach-btn peach-btn--subtle peach-btn--md"179- onClick={(e) => { e.stopPropagation(); handleSkipAutofill(); }}180- >181- Skip for now182- </button>183- </div>184111 </Show>185112 </div>186113 src/popup/views/Settings.tsx+2 −71
@@ -1,9 +1,7 @@11 import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";2-import { Capacitor, registerPlugin } from "@capacitor/core";32 import { sendMessage } from "../App";43 import { PeachMark } from "../components/PeachMark";54 import { LockRuneCanvas } from "../components/LockRuneCanvas";6-import { biometricAvailable } from "../../shared/biometric";75 import type { Canary } from "../../shared/types";86 import { ACCENT_COLORS } from "../../shared/types";97 import type {@@ -72,8 +70,7 @@7270 | "appearance"7371 | "shortcuts"7472 | "license"75- | "feedback"76- | "mobile";73+ | "feedback";7774 7875 const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];7976 @@ -96,8 +93,7 @@9693 value === "appearance" ||9794 value === "shortcuts" ||9895 value === "license" ||99- value === "feedback" ||100- value === "mobile"96+ value === "feedback"10197 );10298 }10399 @@ -217,22 +213,6 @@217213 218214 const [canaryEntries, setCanaryEntries] = createSignal<Array<{id: string; title: string; canary: Canary}>>([]);219215 220- // Mobile-only state221- function getAutofillPlugin() {222- try {223- return registerPlugin<{224- isEnabledAsAutofillService(): Promise<{ enabled: boolean }>;225- openAutofillSettings(): Promise<void>;226- }>("Autofill");227- } catch {228- return null;229- }230- }231- const AutofillPlugin = getAutofillPlugin();232- const [autofillEnabled, setAutofillEnabled] = createSignal(false);233- const [biometricEnabled, setBiometricEnabled] = createSignal(false);234- const isAndroid = Capacitor.getPlatform() === "android";235- const isMobile = Capacitor.isNativePlatform();236216 const [canaryTestingId, setCanaryTestingId] = createSignal<string | null>(null);237217 const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null);238218 @@ -305,16 +285,6 @@305285 );306286 }307287 308- // Android-only: check autofill and biometric status309- if (isAndroid) {310- if (AutofillPlugin) {311- const { enabled } = await AutofillPlugin.isEnabledAsAutofillService();312- setAutofillEnabled(enabled);313- }314- const bio = await biometricAvailable();315- setBiometricEnabled(bio);316- }317-318288 chrome.runtime.onMessage.addListener((msg) => {319289 if (msg.type === "SYNC_STATE_UPDATE") {320290 setPairedDevices(msg.pairedDevices ?? []);@@ -478,10 +448,6 @@478448 479449 async function handleResetSyncIdentity() {480450 setResetIdentityRunning(true);481- if (Capacitor.isNativePlatform()) {482- const { clearAndroidSyncIdentity } = await import("../../shared/android-sync-identity");483- await clearAndroidSyncIdentity();484- }485451 const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });486452 setResetIdentityRunning(false);487453 if ((res as { ok: boolean }).ok) {@@ -741,10 +707,6 @@741707 <p class="peach-settings-nav-label">PREFERENCES</p>742708 {renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })}743709 {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}744- <Show when={isAndroid}>745- <p class="peach-settings-nav-label">MOBILE</p>746- {renderCatRow({ section: "mobile", name: "Mobile", desc: "Autofill & biometric settings", icon: iconSync })}747- </Show>748710 749711 <p class="peach-settings-nav-label">SUPPORT</p>750712 {renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}@@ -1942,36 +1904,6 @@19421904 );19431905 }19441906 1945- function renderMobile() {1946- return (1947- <div class="peach-settings-body">1948- <div class="peach-settings-group">1949- <p class="peach-settings-label">Autofill service</p>1950- <div class="peach-mobile-row">1951- <span class="peach-mobile-status">1952- {autofillEnabled() ? "Enabled" : "Disabled"}1953- </span>1954- <button1955- class="peach-btn peach-btn--subtle peach-btn--sm"1956- onClick={() => void AutofillPlugin?.openAutofillSettings()}1957- >1958- {autofillEnabled() ? "Change" : "Enable"}1959- </button>1960- </div>1961- </div>1962-1963- <div class="peach-settings-group">1964- <p class="peach-settings-label">Biometric unlock</p>1965- <div class="peach-mobile-row">1966- <span class="peach-mobile-status">1967- {biometricEnabled() ? "Available" : "Not available"}1968- </span>1969- </div>1970- </div>1971- </div>1972- );1973- }1974-19751907 return (19761908 <div class="peach-overlay-view ov-enter-right" style="display:flex">19771909 <div class="peach-overlay-header">@@ -1996,7 +1928,6 @@19961928 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>19971929 <Match when={activeSection() === "license"}>{renderLicense()}</Match>19981930 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>1999- <Match when={activeSection() === "mobile"}>{renderMobile()}</Match>20001931 </Switch>20011932 </div>20021933 );src/popup/views/SyncPairing.tsx+2 −10
@@ -1,11 +1,7 @@11 import { createSignal, onCleanup, onMount, Show } from "solid-js";2-import { Capacitor } from "@capacitor/core";32 import { sendMessage } from "../App";43 import type { PairedDevice } from "../../shared/sync-types";54 6-// Both desktop and Android can now host or enter a 6-digit code session.7-// Android's host path uses a Kotlin TCP server + UDP beacon plus a JS8-// Noise NK responder (`capacitor-pairing-server.ts`).95 const SHOW_MODE_AVAILABLE = true;106 117 interface SyncPairingProps {@@ -165,16 +161,12 @@165161 }166162 167163 onMount(() => {168- if (!Capacitor.isNativePlatform()) {169- chrome.runtime.onMessage.addListener(handleRuntimeMessage);170- }164+ chrome.runtime.onMessage.addListener(handleRuntimeMessage);171165 void startShow();172166 });173167 174168 onCleanup(() => {175- if (!Capacitor.isNativePlatform()) {176- chrome.runtime.onMessage.removeListener(handleRuntimeMessage);177- }169+ chrome.runtime.onMessage.removeListener(handleRuntimeMessage);178170 clearExpiryTimer();179171 // Pairing server keeps running in the background per spec — the user180172 // showed the code, so a peer can still complete pairing after the popup