chahinebrini aa9466aa92 feat(rebreak-native): Face ID app lock (opt-in)
Privacy/stigma layer on top of the authenticated Supabase session — re-auth on
open so nobody but the user can open Rebreak. Not a login replacement.

- expo-local-authentication; NSFaceIDUsageDescription in app.config
- stores/appLock.ts: persisted `enabled` pref, in-memory `locked`, device-
  capability check (`available`), device-passcode fallback on biometric failure
- AppLockGate wraps the root layout: locks immediately on `background` (not
  `inactive` → app-switcher peek doesn't lock), renders LockScreen while
  `enabled && locked && session`
- LockScreen: dark brand screen, auto-prompts on mount + on return from
  background, "Abmelden" escape hatch (clears session → fresh login next launch)
- Settings: new "Sicherheit" section, native UISwitch; enabling requires a
  successful biometric prompt first; row disabled + explained when device has no
  biometrics/passcode
- de/en strings

Per product call: the lock gates the whole app incl. SOS (SOS already requires
an authenticated user, so there's no unauthenticated path to carve out).

Cold-start: appLock init blocks the splash → `locked` is set before first paint,
no flash of unlocked content. ios/ is gitignored so EAS prebuilds the new module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:41:56 +02:00

36 lines
1.3 KiB
TypeScript

import { useEffect } from 'react';
import { AppState } from 'react-native';
import { useAppLockStore } from '../stores/appLock';
import { useAuthStore } from '../stores/auth';
import { LockScreen } from './LockScreen';
/**
* Hängt die App-Sperre vor den App-Inhalt:
* - sperrt sofort wenn die App in den Hintergrund geht (`background`-State —
* NICHT `inactive`, sonst würde der App-Switcher-Peek schon sperren)
* - rendert den LockScreen solange `enabled && locked && session` gilt
*
* `init()` der appLock-Store wird im RootLayout zusammen mit den anderen Stores
* aufgerufen; der Splash wartet auf `ready`, daher gibt es hier kein Flash-of-
* unlocked-content beim Kaltstart (init setzt `locked = enabled`).
*/
export function AppLockGate({ children }: { children: React.ReactNode }) {
const enabled = useAppLockStore((s) => s.enabled);
const locked = useAppLockStore((s) => s.locked);
const lock = useAppLockStore((s) => s.lock);
const session = useAuthStore((s) => s.session);
useEffect(() => {
if (!enabled) return;
const sub = AppState.addEventListener('change', (state) => {
if (state === 'background') lock();
});
return () => sub.remove();
}, [enabled, lock]);
if (enabled && locked && session) {
return <LockScreen />;
}
return <>{children}</>;
}