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