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
5 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 −10
src/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>

view file

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 

view file

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 

view file

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

view file

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

view file