1 import ABI48_0_0ExpoModulesCore
2 import LocalAuthentication
3 
4 public class LocalAuthenticationModule: Module {
definitionnull5   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 
isFaceIdDevicenull119 func isFaceIdDevice() -> Bool {
120   let context = LAContext()
121   context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil)
122 
123   return context.biometryType == LABiometryType.faceID
124 }
125 
isTouchIdDevicenull126 func isTouchIdDevice() -> Bool {
127   let context = LAContext()
128   context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil)
129 
130   return context.biometryType == LABiometryType.touchID
131 }
132 
convertErrorCodenull133 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