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
↓ peach-browser-800fef2e.tar.gzatlas at this save
files changed1 file
src/popup/views/Settings.tsx+85 −0src/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">