import ABI49_0_0ExpoModulesCore import LocalAuthentication public class LocalAuthenticationModule: Module { public func definition() -> ModuleDefinition { Name("ExpoLocalAuthentication") AsyncFunction("hasHardwareAsync") { () -> Bool in let context = LAContext() var error: NSError? let isSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) let isAvailable: Bool = isSupported || error?.code != LAError.biometryNotAvailable.rawValue return isAvailable } AsyncFunction("isEnrolledAsync") { () -> Bool in let context = LAContext() var error: NSError? let isSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) let isEnrolled: Bool = isSupported && error == nil return isEnrolled } AsyncFunction("supportedAuthenticationTypesAsync") { () -> [Int] in var supportedAuthenticationTypes: [Int] = [] if isTouchIdDevice() { supportedAuthenticationTypes.append(AuthenticationType.fingerprint.rawValue) } if isFaceIdDevice() { supportedAuthenticationTypes.append(AuthenticationType.facialRecognition.rawValue) } return supportedAuthenticationTypes } AsyncFunction("getEnrolledLevelAsync") { () -> Int in let context = LAContext() var error: NSError? var level: Int = SecurityLevel.none.rawValue let isAuthenticationSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: &error) if isAuthenticationSupported && error == nil { level = SecurityLevel.secret.rawValue } let isBiometricsSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) if isBiometricsSupported && error == nil { level = SecurityLevel.biometric.rawValue } return level } AsyncFunction("authenticateAsync") { (options: LocalAuthenticationOptions, promise: Promise) -> Void in var warningMessage: String? var reason = options.promptMessage var cancelLabel = options.cancelLabel var fallbackLabel = options.fallbackLabel var disableDeviceFallback = options.disableDeviceFallback if isFaceIdDevice() { let usageDescription = Bundle.main.object(forInfoDictionaryKey: "NSFaceIDUsageDescription") if usageDescription == nil { warningMessage = "FaceID is available but has not been configured. To enable FaceID, provide `NSFaceIDUsageDescription`." } } let context = LAContext() if fallbackLabel != nil { context.localizedFallbackTitle = fallbackLabel } if cancelLabel != nil { context.localizedCancelTitle = cancelLabel } context.interactionNotAllowed = false let policyForAuth = disableDeviceFallback ? LAPolicy.deviceOwnerAuthenticationWithBiometrics : LAPolicy.deviceOwnerAuthentication if disableDeviceFallback { if warningMessage != nil { // If the warning message is set (NSFaceIDUsageDescription is not configured) then we can't use // authentication with biometrics — it would crash, so let's just resolve with no success. // We could reject, but we already resolve even if there are any errors, so sadly we would need to introduce a breaking change. return promise.resolve([ "success": false, "error": "missing_usage_description", "warning": warningMessage ]) } } context.evaluatePolicy(policyForAuth, localizedReason: reason ?? "") { success, error in var err: String? if let error = error as? NSError { err = convertErrorCode(error: error) } return promise.resolve([ "success": success, "error": err, "warning": warningMessage ]) } } } } func isFaceIdDevice() -> Bool { let context = LAContext() context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil) return context.biometryType == LABiometryType.faceID } func isTouchIdDevice() -> Bool { let context = LAContext() context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil) return context.biometryType == LABiometryType.touchID } func convertErrorCode(error: NSError) -> String { switch error.code { case LAError.systemCancel.rawValue: return "system_cancel" case LAError.appCancel.rawValue: return "app_cancel" case LAError.biometryLockout.rawValue: return "lockout" case LAError.userFallback.rawValue: return "user_fallback" case LAError.userCancel.rawValue: return "user_cancel" case LAError.biometryNotAvailable.rawValue: return "not_available" case LAError.invalidContext.rawValue: return "invalid_context" case LAError.biometryNotEnrolled.rawValue: return "not_enrolled" case LAError.passcodeNotSet.rawValue: return "passcode_not_set" case LAError.authenticationFailed.rawValue: return "authentication_failed" default: return "unknown: \(error.code), \(error.localizedDescription)" } } enum AuthenticationType: Int { case fingerprint = 1 case facialRecognition = 2 } enum SecurityLevel: Int { case none = 0 case secret = 1 case biometric = 2 }