1 import ABI49_0_0ExpoModulesCore 2 import LocalAuthentication 3 4 public class LocalAuthenticationModule: Module { 5 public func definition() -> ModuleDefinition { 6 Name("ExpoLocalAuthentication") 7 8 AsyncFunction("hasHardwareAsync") { () -> Bool in 9 let context = LAContext() 10 var error: NSError? 11 let isSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) 12 let isAvailable: Bool = isSupported || error?.code != LAError.biometryNotAvailable.rawValue 13 14 return isAvailable 15 } 16 17 AsyncFunction("isEnrolledAsync") { () -> Bool in 18 let context = LAContext() 19 var error: NSError? 20 let isSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) 21 let isEnrolled: Bool = isSupported && error == nil 22 23 return isEnrolled 24 } 25 26 AsyncFunction("supportedAuthenticationTypesAsync") { () -> [Int] in 27 var supportedAuthenticationTypes: [Int] = [] 28 29 if isTouchIdDevice() { 30 supportedAuthenticationTypes.append(AuthenticationType.fingerprint.rawValue) 31 } 32 33 if isFaceIdDevice() { 34 supportedAuthenticationTypes.append(AuthenticationType.facialRecognition.rawValue) 35 } 36 37 return supportedAuthenticationTypes 38 } 39 40 AsyncFunction("getEnrolledLevelAsync") { () -> Int in 41 let context = LAContext() 42 var error: NSError? 43 44 var level: Int = SecurityLevel.none.rawValue 45 46 let isAuthenticationSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: &error) 47 if isAuthenticationSupported && error == nil { 48 level = SecurityLevel.secret.rawValue 49 } 50 51 let isBiometricsSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) 52 53 if isBiometricsSupported && error == nil { 54 level = SecurityLevel.biometric.rawValue 55 } 56 57 return level 58 } 59 60 AsyncFunction("authenticateAsync") { (options: LocalAuthenticationOptions, promise: Promise) -> Void in 61 var warningMessage: String? 62 var reason = options.promptMessage 63 var cancelLabel = options.cancelLabel 64 var fallbackLabel = options.fallbackLabel 65 var disableDeviceFallback = options.disableDeviceFallback 66 67 if isFaceIdDevice() { 68 let usageDescription = Bundle.main.object(forInfoDictionaryKey: "NSFaceIDUsageDescription") 69 70 if usageDescription == nil { 71 warningMessage = "FaceID is available but has not been configured. To enable FaceID, provide `NSFaceIDUsageDescription`." 72 } 73 } 74 75 let context = LAContext() 76 77 if fallbackLabel != nil { 78 context.localizedFallbackTitle = fallbackLabel 79 } 80 81 if cancelLabel != nil { 82 context.localizedCancelTitle = cancelLabel 83 } 84 85 context.interactionNotAllowed = false 86 87 let policyForAuth = disableDeviceFallback ? LAPolicy.deviceOwnerAuthenticationWithBiometrics : LAPolicy.deviceOwnerAuthentication 88 89 if disableDeviceFallback { 90 if warningMessage != nil { 91 // If the warning message is set (NSFaceIDUsageDescription is not configured) then we can't use 92 // authentication with biometrics — it would crash, so let's just resolve with no success. 93 // We could reject, but we already resolve even if there are any errors, so sadly we would need to introduce a breaking change. 94 return promise.resolve([ 95 "success": false, 96 "error": "missing_usage_description", 97 "warning": warningMessage 98 ]) 99 } 100 } 101 102 context.evaluatePolicy(policyForAuth, localizedReason: reason ?? "") { success, error in 103 var err: String? 104 105 if let error = error as? NSError { 106 err = convertErrorCode(error: error) 107 } 108 109 return promise.resolve([ 110 "success": success, 111 "error": err, 112 "warning": warningMessage 113 ]) 114 } 115 } 116 } 117 } 118 119 func isFaceIdDevice() -> Bool { 120 let context = LAContext() 121 context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil) 122 123 return context.biometryType == LABiometryType.faceID 124 } 125 126 func isTouchIdDevice() -> Bool { 127 let context = LAContext() 128 context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil) 129 130 return context.biometryType == LABiometryType.touchID 131 } 132 133 func convertErrorCode(error: NSError) -> String { 134 switch error.code { 135 case LAError.systemCancel.rawValue: 136 return "system_cancel" 137 case LAError.appCancel.rawValue: 138 return "app_cancel" 139 case LAError.biometryLockout.rawValue: 140 return "lockout" 141 case LAError.userFallback.rawValue: 142 return "user_fallback" 143 case LAError.userCancel.rawValue: 144 return "user_cancel" 145 case LAError.biometryNotAvailable.rawValue: 146 return "not_available" 147 case LAError.invalidContext.rawValue: 148 return "invalid_context" 149 case LAError.biometryNotEnrolled.rawValue: 150 return "not_enrolled" 151 case LAError.passcodeNotSet.rawValue: 152 return "passcode_not_set" 153 case LAError.authenticationFailed.rawValue: 154 return "authentication_failed" 155 default: 156 return "unknown: \(error.code), \(error.localizedDescription)" 157 } 158 } 159 160 enum AuthenticationType: Int { 161 case fingerprint = 1 162 case facialRecognition = 2 163 } 164 165 enum SecurityLevel: Int { 166 case none = 0 167 case secret = 1 168 case biometric = 2 169 } 170