3872f604f17d

add shared/native.ts (isNativePlatform, nativePlugin); App.tsx subscribes to Capacitor App appStateChange and locks vault when background elapsed >= autoLockMinutes

time
2026-05-07 02:19 UTC · 5 months ago
machine
koh2
parent
16db45928fa3
2 files src/popup/App.tsx+34 −0src/shared/native.ts+23 −0
src/popup/App.tsx+34 −0
@@ -25,6 +25,7 @@2525   type ShortcutAction,2626 } from "../shared/shortcuts";2727 import { generateTOTP } from "../crypto/totp";28+import { isNativePlatform, nativePlugin } from "../shared/native";2829 2930 export { sendMessage } from "../shared/bridge";3031 @@ -413,6 +414,39 @@413414       popupPort = chrome.runtime.connect({ name: "popup" });414415     } catch { /* extension context invalidated — ignore */ }415416     onCleanup(() => { try { popupPort?.disconnect(); } catch {} });417+418+    // Android autolock. chrome.alarms is no-op'd on Capacitor, so the only419+    // reliable signal is appStateChange — setTimeout is killed when the OS420+    // backgrounds the app. We measure elapsed background time on resume.421+    if (isNativePlatform()) {422+      let backgroundedAt: number | null = null;423+      const CapApp = nativePlugin<{424+        addListener: (425+          ev: string,426+          cb: (s: { isActive: boolean }) => void,427+        ) => Promise<{ remove: () => Promise<void> }> | { remove: () => Promise<void> };428+      }>("App");429+      if (CapApp?.addListener) {430+        const sub = CapApp.addListener(431+          "appStateChange",432+          ({ isActive }: { isActive: boolean }) => {433+            if (!isActive) {434+              backgroundedAt = Date.now();435+              return;436+            }437+            if (backgroundedAt !== null && autoLockMinutes() > 0) {438+              const elapsed = Date.now() - backgroundedAt;439+              if (elapsed >= autoLockMinutes() * 60_000) handleHeaderLock();440+            }441+            backgroundedAt = null;442+          }443+        );444+        // Capacitor 6 listeners return a Promise<{ remove: () => Promise<void> }>.445+        onCleanup(() => {446+          Promise.resolve(sub).then((s) => s?.remove?.()).catch(() => {});447+        });448+      }449+    }416450 417451     // Background auto-lock broadcasts VAULT_LOCKED so the popup drops stale418452     // entries instead of waiting for the next GET_ENTRIES round-trip.

view file

src/shared/native.ts+23 −0
@@ -0,0 +1,23 @@1+// Mobile platform detection helpers used by popup views to gate Capacitor-only paths.2+// Kept tiny and runtime-only — never import @capacitor/core at module scope, since3+// desktop builds must not pull it.4+5+// eslint-disable-next-line @typescript-eslint/no-explicit-any6+type Cap = { isNativePlatform?: () => boolean; Plugins?: Record<string, any> };7+8+function cap(): Cap | null {9+  const g = globalThis as unknown as { Capacitor?: Cap };10+  return g.Capacitor ?? null;11+}12+13+export function isNativePlatform(): boolean {14+  const c = cap();15+  if (!c) return false;16+  if (typeof c.isNativePlatform === "function") return c.isNativePlatform();17+  return false;18+}19+20+// eslint-disable-next-line @typescript-eslint/no-explicit-any21+export function nativePlugin<T = any>(name: string): T | null {22+  return (cap()?.Plugins?.[name] as T | undefined) ?? null;23+}

view file