800fef2eb834

add BIOMETRICS section in Settings security panel: enable/disable toggle wired to GET_DERIVED_KEY -> KeystorePlugin.storeMasterKey/clearMasterKey + biometricsEnabled UPDATE_SETTINGS; only renders on native + supported devices

time
2026-05-07 02:22 UTC · 5 months ago
machine
koh2
parent
a1d75b982320
1 file src/popup/views/Settings.tsx+85 −0
src/popup/views/Settings.tsx+85 −0
@@ -21,6 +21,7 @@2121   type Keybinding,2222   type ShortcutAction,2323 } from "../../shared/shortcuts";24+import { isNativePlatform, nativePlugin } from "../../shared/native";2425 2526 interface LicenseInfo {2627   valid: boolean;@@ -123,6 +124,10 @@123124   const [syncProStatus, setSyncProStatus] = createSignal(false);124125   const isPro = () => props.isPro ?? syncProStatus();125126   const [syncingNow, setSyncingNow] = createSignal(false);127+  const [biometricSupported, setBiometricSupported] = createSignal(false);128+  const [biometricStored, setBiometricStored] = createSignal(false);129+  const [biometricBusy, setBiometricBusy] = createSignal(false);130+  const [biometricError, setBiometricError] = createSignal<string | null>(null);126131   const [syncFeedback, setSyncFeedback] = createSignal<127132     { kind: "ok" | "err"; text: string } | null128133   >(null);@@ -296,8 +301,68 @@296301         setSyncProStatus(msg.isPro ?? false);297302       }298303     });304+305+    if (isNativePlatform()) {306+      const biometric = nativePlugin<{ isAvailable: () => Promise<{ available: boolean }> }>("BiometricPlugin");307+      const keystore = nativePlugin<{ hasMasterKey: () => Promise<{ exists: boolean }> }>("KeystorePlugin");308+      try {309+        if (biometric) {310+          const a = await biometric.isAvailable();311+          setBiometricSupported(a.available);312+        }313+        if (keystore) {314+          const k = await keystore.hasMasterKey();315+          setBiometricStored(k.exists);316+        }317+      } catch { /* missing plugins — leave defaults */ }318+    }299319   });300320 321+  async function handleEnableBiometrics() {322+    setBiometricBusy(true);323+    setBiometricError(null);324+    try {325+      const keyRes = (await sendMessage({ type: "GET_DERIVED_KEY" })) as {326+        ok: boolean;327+        data?: { masterKeyHex: string };328+        error?: string;329+      };330+      if (!keyRes.ok || !keyRes.data?.masterKeyHex) {331+        setBiometricError(keyRes.error === "vault_locked"332+          ? "unlock the vault first"333+          : "couldn't read derived key");334+        return;335+      }336+      const keystore = nativePlugin<{ storeMasterKey: (p: { masterKeyHex: string }) => Promise<void> }>("KeystorePlugin");337+      if (!keystore) {338+        setBiometricError("biometric plugin unavailable");339+        return;340+      }341+      await keystore.storeMasterKey({ masterKeyHex: keyRes.data.masterKeyHex });342+      await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: true } });343+      setBiometricStored(true);344+    } catch (e) {345+      setBiometricError(e instanceof Error ? e.message : "failed to enable biometrics");346+    } finally {347+      setBiometricBusy(false);348+    }349+  }350+351+  async function handleDisableBiometrics() {352+    setBiometricBusy(true);353+    setBiometricError(null);354+    try {355+      const keystore = nativePlugin<{ clearMasterKey: () => Promise<void> }>("KeystorePlugin");356+      if (keystore) await keystore.clearMasterKey();357+      await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: false } });358+      setBiometricStored(false);359+    } catch (e) {360+      setBiometricError(e instanceof Error ? e.message : "failed to disable biometrics");361+    } finally {362+      setBiometricBusy(false);363+    }364+  }365+301366   async function handleChangePassword() {302367     setChangePwError(null);303368     if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; }@@ -751,6 +816,26 @@751816               <option value="0">Never</option>752817             </select>753818           </div>819+820+          <Show when={isNativePlatform() && biometricSupported()}>821+            <p class="peach-settings-sub-label">BIOMETRICS</p>822+            <div class="peach-settings-row">823+              <span>Unlock with biometrics</span>824+              <button825+                class={`peach-toggle ${biometricStored() ? "is-on" : ""}`}826+                onClick={() => void (biometricStored() ? handleDisableBiometrics() : handleEnableBiometrics())}827+                disabled={biometricBusy()}828+                aria-label="Toggle biometric unlock"829+              >830+                <span class="peach-toggle-knob"></span>831+              </button>832+            </div>833+            <Show when={biometricError()}>834+              <div class="peach-settings-row peach-settings-row--meta">835+                <span class="peach-hint peach-hint-error">{biometricError()}</span>836+              </div>837+            </Show>838+          </Show>754839 755840           <p class="peach-settings-sub-label">BREACH PROTECTION</p>756841           <div class="peach-settings-row">

view file