import Foundation import LocalAuthentication /// Biometric hardware available on this device. Never assume Face ID: iPad and /// iPhone Duo can ship with Touch ID, Vision Pro with Optic ID. enum BiometryKind { case faceID case touchID case opticID case none /// Marketing name shown in UI ("Face ID", "Touch ID", "Optic ID"). Falls back /// to a generic label when no biometry is available (e.g. not enrolled). var displayName: String { switch self { case .faceID: return "Face ID" case .touchID: return "Touch ID" case .opticID: return "Optic ID" case .none: return String(localized: "biometric_generic_name") } } /// SF Symbol matching the hardware. var systemImage: String { switch self { case .faceID: return "faceid" case .touchID: return "touchid" case .opticID: return "opticid" case .none: return "lock.shield" } } } enum AppLockService { static func canUseBiometrics() -> Bool { let context = LAContext() var error: NSError? return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) } /// The biometry type reported by the system. `biometryType` is only populated /// after `canEvaluatePolicy` runs, and stays set even when not enrolled. static var biometryKind: BiometryKind { let context = LAContext() var error: NSError? _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) switch context.biometryType { case .faceID: return .faceID case .touchID: return .touchID case .opticID: return .opticID case .none: return .none @unknown default: return .none } } /// Localized biometry name for user-facing text. static var biometryName: String { biometryKind.displayName } static func authenticate(reason: String, completion: @escaping (Bool) -> Void) { let context = LAContext() context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in DispatchQueue.main.async { completion(success) } } } }