import * as LocalAuthentication from "expo-local-authentication"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { AppState, type AppStateStatus } from "react-native"; import { clearStoredPin, getAppLockEnabled, getBiometricEnabled, getStoredPin, isValidPin, setAppLockEnabled, setBiometricEnabled, setStoredPin, } from "@/lib/app-lock"; type AppLockContextValue = { enabled: boolean; biometricEnabled: boolean; hasPin: boolean; isLocked: boolean; biometricAvailable: boolean; biometricLabel: string; unlockWithPin: (pin: string) => Promise; unlockWithBiometric: () => Promise; enableLock: (pin: string) => Promise; disableLock: (pin: string) => Promise; changePin: (currentPin: string, nextPin: string) => Promise; setUseBiometric: (enabled: boolean) => Promise; lock: () => void; }; const AppLockContext = createContext(null); export function AppLockProvider({ children }: { children: ReactNode }) { const [enabled, setEnabled] = useState(false); const [biometricEnabled, setBiometricEnabledState] = useState(false); const [hasPin, setHasPin] = useState(false); const [isLocked, setIsLocked] = useState(false); const [biometricAvailable, setBiometricAvailable] = useState(false); const [biometricLabel, setBiometricLabel] = useState("Biometrics"); const wasBackgrounded = useRef(false); const hydrated = useRef(false); useEffect(() => { let cancelled = false; async function hydrate() { const [lockEnabled, pin, bioEnabled, hasHardware, isEnrolled, authTypes] = await Promise.all([ getAppLockEnabled(), getStoredPin(), getBiometricEnabled(), LocalAuthentication.hasHardwareAsync(), LocalAuthentication.isEnrolledAsync(), LocalAuthentication.supportedAuthenticationTypesAsync(), ]); if (cancelled) return; const bioAvailable = hasHardware && isEnrolled; setEnabled(lockEnabled); setHasPin(Boolean(pin)); setBiometricEnabledState(bioEnabled && bioAvailable); setBiometricAvailable(bioAvailable); setBiometricLabel( authTypes.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION) ? "Face ID" : authTypes.includes(LocalAuthentication.AuthenticationType.FINGERPRINT) ? "Touch ID" : "Biometrics", ); setIsLocked(lockEnabled); hydrated.current = true; } void hydrate(); return () => { cancelled = true; }; }, []); useEffect(() => { const subscription = AppState.addEventListener("change", (nextState: AppStateStatus) => { if (!hydrated.current || !enabled) return; if (nextState === "background" || nextState === "inactive") { wasBackgrounded.current = true; } if (nextState === "active" && wasBackgrounded.current) { wasBackgrounded.current = false; setIsLocked(true); } }); return () => subscription.remove(); }, [enabled]); const unlockWithPin = useCallback(async (pin: string) => { const stored = await getStoredPin(); if (!stored || stored !== pin) { return false; } setIsLocked(false); return true; }, []); const unlockWithBiometric = useCallback(async () => { if (!biometricEnabled || !biometricAvailable) { return false; } const result = await LocalAuthentication.authenticateAsync({ promptMessage: "Unlock beenvoice", cancelLabel: "Use PIN", disableDeviceFallback: true, }); if (!result.success) { return false; } setIsLocked(false); return true; }, [biometricAvailable, biometricEnabled]); const enableLock = useCallback(async (pin: string) => { if (!isValidPin(pin)) { throw new Error("PIN must be 4–6 digits"); } await setStoredPin(pin); await setAppLockEnabled(true); setHasPin(true); setEnabled(true); setIsLocked(false); }, []); const disableLock = useCallback(async (pin: string) => { const stored = await getStoredPin(); if (!stored || stored !== pin) { return false; } await setAppLockEnabled(false); await clearStoredPin(); await setBiometricEnabled(false); setEnabled(false); setHasPin(false); setBiometricEnabledState(false); setIsLocked(false); return true; }, []); const changePin = useCallback(async (currentPin: string, nextPin: string) => { const stored = await getStoredPin(); if (!stored || stored !== currentPin || !isValidPin(nextPin)) { return false; } await setStoredPin(nextPin); return true; }, []); const setUseBiometric = useCallback(async (next: boolean) => { if (next) { const result = await LocalAuthentication.authenticateAsync({ promptMessage: `Enable ${biometricLabel}`, cancelLabel: "Cancel", disableDeviceFallback: true, }); if (!result.success) return; } await setBiometricEnabled(next); setBiometricEnabledState(next); }, [biometricLabel]); const lock = useCallback(() => { if (enabled) { setIsLocked(true); } }, [enabled]); const value = useMemo( () => ({ enabled, biometricEnabled, hasPin, isLocked, biometricAvailable, biometricLabel, unlockWithPin, unlockWithBiometric, enableLock, disableLock, changePin, setUseBiometric, lock, }), [ enabled, biometricEnabled, hasPin, isLocked, biometricAvailable, biometricLabel, unlockWithPin, unlockWithBiometric, enableLock, disableLock, changePin, setUseBiometric, lock, ], ); return {children}; } export function useAppLock() { const context = useContext(AppLockContext); if (!context) { throw new Error("useAppLock must be used within AppLockProvider"); } return context; }