// // BiometricAuthManager.swift // Reflect // // Manages Face ID / Touch ID authentication for app privacy lock. // import Foundation import LocalAuthentication import SwiftUI @MainActor class BiometricAuthManager: ObservableObject { // MARK: - Published State @Published var isUnlocked: Bool = true @Published var isAuthenticating: Bool = false // MARK: - App Storage @AppStorage(UserDefaultsStore.Keys.privacyLockEnabled.rawValue, store: GroupUserDefaults.groupDefaults) var isLockEnabled: Bool = false // MARK: - Biometric Capabilities var canUseBiometrics: Bool { let context = LAContext() var error: NSError? return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) } var canUseDevicePasscode: Bool { let context = LAContext() var error: NSError? return context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) } var biometricType: LABiometryType { let context = LAContext() var error: NSError? _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) return context.biometryType } var biometricName: String { switch biometricType { case .none: return "Passcode" case .faceID: return "Face ID" case .touchID: return "Touch ID" case .opticID: return "Optic ID" @unknown default: return "Biometrics" } } var biometricIcon: String { switch biometricType { case .none: return "lock.fill" case .faceID: return "faceid" case .touchID: return "touchid" case .opticID: return "opticid" @unknown default: return "lock.fill" } } // MARK: - Authentication func authenticate() async -> Bool { guard isLockEnabled else { isUnlocked = true return true } let context = LAContext() context.localizedCancelTitle = "Cancel" // Try biometrics first, fall back to device passcode let policy: LAPolicy = canUseBiometrics ? .deviceOwnerAuthenticationWithBiometrics : .deviceOwnerAuthentication isAuthenticating = true defer { isAuthenticating = false } do { let success = try await context.evaluatePolicy( policy, localizedReason: "Unlock Reflect to access your mood data" ) isUnlocked = success if success { AnalyticsManager.shared.track(.biometricUnlockSuccess) } return success } catch { #if DEBUG print("Authentication failed: \(error.localizedDescription)") #endif AnalyticsManager.shared.track(.biometricUnlockFailed(error: error.localizedDescription)) // If biometrics failed, try device passcode as fallback if canUseDevicePasscode && policy == .deviceOwnerAuthenticationWithBiometrics { return await authenticateWithPasscode() } return false } } private func authenticateWithPasscode() async -> Bool { let context = LAContext() do { let success = try await context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: "Unlock Reflect to access your mood data" ) isUnlocked = success return success } catch { #if DEBUG print("Passcode authentication failed: \(error.localizedDescription)") #endif return false } } // MARK: - Lock Management func lock() { guard isLockEnabled else { return } isUnlocked = false AnalyticsManager.shared.track(.appLocked) } func enableLock() async -> Bool { // Authenticate first to enable lock - require biometrics let context = LAContext() var error: NSError? // Only allow enabling if biometrics are available guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { #if DEBUG print("Biometric authentication not available: \(error?.localizedDescription ?? "Unknown")") #endif return false } do { let success = try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "Verify your identity to enable app lock" ) if success { isLockEnabled = true isUnlocked = true AnalyticsManager.shared.track(.privacyLockEnabled) } return success } catch { #if DEBUG print("Failed to enable lock: \(error.localizedDescription)") #endif return false } } func disableLock() { isLockEnabled = false isUnlocked = true AnalyticsManager.shared.track(.privacyLockDisabled) } }