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 { useAccounts } from "@/contexts/AccountsContext"; 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 { activeAccountId } = useAccounts(); 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(() => { if (!activeAccountId) { setEnabled(false); setHasPin(false); setBiometricEnabledState(false); setIsLocked(false); hydrated.current = false; return; } let cancelled = false; hydrated.current = false; const accountId = activeAccountId; async function hydrate() { const [lockEnabled, pin, bioEnabled, hasHardware, isEnrolled, authTypes] = await Promise.all([ getAppLockEnabled(accountId), getStoredPin(accountId), getBiometricEnabled(accountId), 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; }; }, [activeAccountId]); useEffect(() => { const subscription = AppState.addEventListener("change", (nextState: AppStateStatus) => { if (!hydrated.current || !enabled || !activeAccountId) return; if (nextState === "background" || nextState === "inactive") { wasBackgrounded.current = true; } if (nextState === "active" && wasBackgrounded.current) { wasBackgrounded.current = false; setIsLocked(true); } }); return () => subscription.remove(); }, [enabled, activeAccountId]); const unlockWithPin = useCallback( async (pin: string) => { if (!activeAccountId) return false; const stored = await getStoredPin(activeAccountId); if (!stored || stored !== pin) { return false; } setIsLocked(false); return true; }, [activeAccountId], ); const unlockWithBiometric = useCallback(async () => { if (!biometricAvailable || !activeAccountId) { return false; } const result = await LocalAuthentication.authenticateAsync({ promptMessage: "Unlock beenvoice", cancelLabel: "Use PIN", disableDeviceFallback: false, biometricsSecurityLevel: "weak", }); if (!result.success) { return false; } if (!biometricEnabled) { await setBiometricEnabled(activeAccountId, true); setBiometricEnabledState(true); } setIsLocked(false); return true; }, [biometricAvailable, biometricEnabled, activeAccountId]); const enableLock = useCallback( async (pin: string) => { if (!activeAccountId) { throw new Error("No active account"); } if (!isValidPin(pin)) { throw new Error("PIN must be 4–6 digits"); } const [hasHardware, isEnrolled] = await Promise.all([ LocalAuthentication.hasHardwareAsync(), LocalAuthentication.isEnrolledAsync(), ]); const bioAvailable = hasHardware && isEnrolled; await setStoredPin(activeAccountId, pin); await setAppLockEnabled(activeAccountId, true); setHasPin(true); setEnabled(true); setIsLocked(false); if (bioAvailable) { await setBiometricEnabled(activeAccountId, true); setBiometricEnabledState(true); } }, [activeAccountId], ); const disableLock = useCallback( async (pin: string) => { if (!activeAccountId) return false; const stored = await getStoredPin(activeAccountId); if (!stored || stored !== pin) { return false; } await setAppLockEnabled(activeAccountId, false); await clearStoredPin(activeAccountId); await setBiometricEnabled(activeAccountId, false); setEnabled(false); setHasPin(false); setBiometricEnabledState(false); setIsLocked(false); return true; }, [activeAccountId], ); const changePin = useCallback( async (currentPin: string, nextPin: string) => { if (!activeAccountId) return false; const stored = await getStoredPin(activeAccountId); if (!stored || stored !== currentPin || !isValidPin(nextPin)) { return false; } await setStoredPin(activeAccountId, nextPin); return true; }, [activeAccountId], ); const setUseBiometric = useCallback( async (next: boolean) => { if (!activeAccountId) return; if (next) { const result = await LocalAuthentication.authenticateAsync({ promptMessage: `Enable ${biometricLabel}`, cancelLabel: "Cancel", disableDeviceFallback: true, }); if (!result.success) return; } await setBiometricEnabled(activeAccountId, next); setBiometricEnabledState(next); }, [biometricLabel, activeAccountId], ); 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; }