taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }