BiometricService.swift (3860B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * 7 * @author Marc Stibane 8 */ 9 import SwiftUI 10 import LocalAuthentication 11 12 @MainActor 13 class BiometricService: ObservableObject { 14 static let shared = BiometricService() 15 @Published var canAuthenticate: Bool = true 16 @Published var isAuthenticated = false 17 @Published var authenticationError: String? 18 19 @AppStorage("useAuthentication") var useAuthentication: Bool = false 20 21 private var context: LAContext? 22 23 func biometryType() -> LABiometryType { 24 if context == nil { 25 resetContext() 26 } 27 return context?.biometryType ?? .none 28 } 29 30 func authenticateUser() { 31 let reason = resetContext() 32 33 if useAuthentication { 34 guard let context = context else { 35 authenticationError = String(localized: "Failed to initialize authentication context.", comment: "FaceID") 36 canAuthenticate = false 37 return 38 } 39 guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) else { 40 authenticationError = String(localized: "Authentication is not available on this device.", comment: "FaceID") 41 canAuthenticate = false 42 return 43 } 44 canAuthenticate = true 45 context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { [weak self] success, authenticationError in 46 Task { @MainActor in 47 if success { 48 self?.isAuthenticated = true 49 self?.authenticationError = nil 50 } else if let error = authenticationError as? LAError { 51 self?.authenticationError = self?.errorMessage(for: error) 52 } else { 53 self?.authenticationError = String(localized: "Unknown authentication error occurred.", comment: "FaceID") 54 } 55 } 56 } 57 } 58 } 59 60 @discardableResult 61 private func resetContext() -> String { 62 let reason = String(localized: "Authenticate to access your money", comment: "FaceID") 63 context = LAContext() 64 context?.localizedReason = reason 65 context?.localizedCancelTitle = String(localized: "Cancel", comment: "FaceID") 66 context?.localizedFallbackTitle = String(localized: "Use Passcode", comment: "FaceID") 67 return reason 68 } 69 70 private func errorMessage(for error: LAError) -> String { 71 switch error.code { 72 case .authenticationFailed: 73 return String(localized: "Authentication failed. Please try again.", comment: "FaceID") 74 case .userCancel: 75 return String(localized: "You canceled the authentication.", comment: "FaceID") 76 case .userFallback: 77 return String(localized: "You chose to use the fallback option.", comment: "FaceID") 78 case .systemCancel: 79 return String(localized: "The system canceled the authentication.", comment: "FaceID") 80 case .passcodeNotSet: 81 return String(localized: "Passcode is not set on this device.", comment: "FaceID") 82 case .biometryNotAvailable: 83 return String(localized: "Biometric authentication is not available on this device.", comment: "FaceID") 84 case .biometryNotEnrolled: 85 return String(localized: "No biometrics are enrolled on this device.", comment: "FaceID") 86 case .biometryLockout: 87 return String(localized: "Biometric authentication is locked. Enter your passcode to unlock.", comment: "FaceID") 88 default: 89 return String(localized: "An unknown error occurred.", comment: "FaceID") 90 } 91 } 92 }