import { create } from 'zustand'; import AsyncStorage from '@react-native-async-storage/async-storage'; // expo-local-authentication ist ein NATIVE-Modul. In einem Dev-Client, der noch // VOR dem Hinzufügen dieser Dependency gebaut wurde, fehlt der native Teil → // `require()` wirft "Cannot find native module 'ExpoLocalAuthentication'" und // würde die ganze App beim Start crashen (appLock wird im RootLayout importiert). // Daher: defensiv laden. Fehlt das Modul → App-Sperre einfach „nicht verfügbar", // der Rest der App läuft normal weiter. In jedem echten Build (EAS / frischer // `expo prebuild`) ist das Modul drin und alles funktioniert. type LocalAuthModule = typeof import('expo-local-authentication'); let LocalAuthentication: LocalAuthModule | null = null; try { // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require LocalAuthentication = require('expo-local-authentication') as LocalAuthModule; } catch { LocalAuthentication = null; } /** * App-Sperre (Face ID / Touch ID). * * Liegt OBEN auf der bereits authentifizierten Supabase-Session — re-auth beim * Öffnen, kein Login-Ersatz. Sucht-/Stigma-Schutz: wer dein entsperrtes iPhone * nimmt, kann Rebreak nicht öffnen. * * - `enabled` → User-Präferenz, persistiert (AsyncStorage) * - `locked` → in-memory: ist die App gerade hinter der Sperre? * - `available`→ Gerät kann biometrisch ODER per Geräte-Passcode auth'en * * Lock-Timing: sofort beim Backgrounden (siehe AppLockGate / AppState-Listener). */ const STORAGE_KEY = '@rebreak/app-lock-enabled'; /** Reason-String im System-Prompt von Face ID / Touch ID. Wird i18n überschrieben * wo wir `t()` haben — dieser Default greift nur falls der Aufrufer keinen liefert. */ const DEFAULT_PROMPT = 'Rebreak entsperren'; type AppLockState = { /** persistierte User-Präferenz */ enabled: boolean; /** in-memory: App ist gerade gesperrt */ locked: boolean; /** Gerät unterstützt Biometrie/Passcode-Auth (sonst Toggle ausgrauen) */ available: boolean; /** init() durch */ ready: boolean; init: () => Promise; setEnabled: (enabled: boolean) => Promise; /** sperren (App geht in den Hintergrund) */ lock: () => void; /** Face-ID/Touch-ID/Passcode-Prompt — bei Erfolg entsperrt. Gibt success zurück. */ authenticate: (promptMessage?: string) => Promise; }; export const useAppLockStore = create((set, get) => ({ enabled: true, locked: false, available: false, ready: false, init: async () => { // Idempotenz: nur beim allerersten init() den locked-Default setzen. Spätere // init()-Calls (z.B. wenn RootLayoutInner durch router.replace re-mountet) // dürfen den aktuellen locked-Zustand NICHT zurücksetzen — sonst entsteht // eine Endlosschleife: unlock → re-mount → init() → locked=true wieder. const alreadyReady = get().ready; if (!LocalAuthentication) { // Native-Modul fehlt (alter Dev-Client) → Sperre nicht verfügbar, App läuft weiter. set({ enabled: false, available: false, locked: false, ready: true }); return; } const [storedRaw, hasHardware, isEnrolled] = await Promise.all([ AsyncStorage.getItem(STORAGE_KEY), LocalAuthentication.hasHardwareAsync(), LocalAuthentication.isEnrolledAsync(), ]); // available = Gerät hat Biometrie-Hardware UND mind. eine Methode eingerichtet // (Face/Touch ODER Geräte-Passcode — isEnrolledAsync deckt beides ab). const available = hasHardware && isEnrolled; const enabled = storedRaw === 'true' && available; set({ enabled, available, // Cold-Start: locked=enabled (kein Flash von App-Inhalt vor LockScreen). // Re-Init: aktuellen locked-Stand erhalten — sonst Loop. locked: alreadyReady ? get().locked : enabled, ready: true, }); }, setEnabled: async (enabled) => { await AsyncStorage.setItem(STORAGE_KEY, enabled ? 'true' : 'false'); set({ enabled, locked: false }); }, lock: () => { if (get().enabled) set({ locked: true }); }, authenticate: async (promptMessage) => { if (!LocalAuthentication) return false; const result = await LocalAuthentication.authenticateAsync({ promptMessage: promptMessage ?? DEFAULT_PROMPT, // Geräte-Passcode als Fallback erlauben (Face ID schlägt 3x fehl → Passcode). // Wichtig, damit man sich nicht aus der App aussperrt. disableDeviceFallback: false, cancelLabel: undefined, }); if (result.success) { set({ locked: false }); return true; } return false; }, }));