expo-local-authentication 13.5.0 → 13.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 13.6.0 — 2023-09-04
14
+
15
+ ### 🎉 New features
16
+
17
+ - Added support for React Native 0.73. ([#24018](https://github.com/expo/expo/pull/24018) by [@kudo](https://github.com/kudo))
18
+
19
+ ### 💡 Others
20
+
21
+ - [Android] Migrate to use Expo Modules API. ([#24083](https://github.com/expo/expo/pull/24083) by [@lukmccall](https://github.com/lukmccall))
22
+
13
23
  ## 13.5.0 — 2023-08-02
14
24
 
15
25
  _This version does not introduce any user-facing changes._
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
3
3
  apply plugin: 'maven-publish'
4
4
 
5
5
  group = 'host.exp.exponent'
6
- version = '13.5.0'
6
+ version = '13.6.0'
7
7
 
8
8
  buildscript {
9
9
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
@@ -53,13 +53,16 @@ afterEvaluate {
53
53
  android {
54
54
  compileSdkVersion safeExtGet("compileSdkVersion", 33)
55
55
 
56
- compileOptions {
57
- sourceCompatibility JavaVersion.VERSION_11
58
- targetCompatibility JavaVersion.VERSION_11
59
- }
56
+ def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
57
+ if (agpVersion.tokenize('.')[0].toInteger() < 8) {
58
+ compileOptions {
59
+ sourceCompatibility JavaVersion.VERSION_11
60
+ targetCompatibility JavaVersion.VERSION_11
61
+ }
60
62
 
61
- kotlinOptions {
62
- jvmTarget = JavaVersion.VERSION_11.majorVersion
63
+ kotlinOptions {
64
+ jvmTarget = JavaVersion.VERSION_11.majorVersion
65
+ }
63
66
  }
64
67
 
65
68
  namespace "expo.modules.localauthentication"
@@ -67,7 +70,7 @@ android {
67
70
  minSdkVersion safeExtGet("minSdkVersion", 21)
68
71
  targetSdkVersion safeExtGet("targetSdkVersion", 33)
69
72
  versionCode 30
70
- versionName "13.5.0"
73
+ versionName "13.6.0"
71
74
  }
72
75
  lintOptions {
73
76
  abortOnError false
@@ -4,68 +4,167 @@ package expo.modules.localauthentication
4
4
  import android.app.Activity
5
5
  import android.app.KeyguardManager
6
6
  import android.content.Context
7
- import android.content.Intent
8
7
  import android.os.Build
9
8
  import android.os.Bundle
9
+ import androidx.annotation.UiThread
10
10
  import androidx.biometric.BiometricManager
11
11
  import androidx.biometric.BiometricPrompt
12
12
  import androidx.biometric.BiometricPrompt.PromptInfo
13
13
  import androidx.fragment.app.FragmentActivity
14
- import expo.modules.core.ExportedModule
15
- import expo.modules.core.ModuleRegistry
16
- import expo.modules.core.ModuleRegistryDelegate
17
- import expo.modules.core.Promise
18
- import expo.modules.core.interfaces.ActivityEventListener
19
- import expo.modules.core.interfaces.ActivityProvider
20
- import expo.modules.core.interfaces.ExpoMethod
21
- import expo.modules.core.interfaces.services.UIManager
22
- import java.util.*
14
+ import expo.modules.kotlin.Promise
15
+ import expo.modules.kotlin.exception.Exceptions
16
+ import expo.modules.kotlin.exception.UnexpectedException
17
+ import expo.modules.kotlin.functions.Queues
18
+ import expo.modules.kotlin.modules.Module
19
+ import expo.modules.kotlin.modules.ModuleDefinition
20
+ import expo.modules.kotlin.records.Field
21
+ import expo.modules.kotlin.records.Record
22
+ import kotlinx.coroutines.launch
23
23
  import java.util.concurrent.Executor
24
24
  import java.util.concurrent.Executors
25
25
 
26
- class LocalAuthenticationModule(context: Context) : ExportedModule(context), ActivityEventListener {
27
- private val AUTHENTICATION_TYPE_FINGERPRINT = 1
28
- private val AUTHENTICATION_TYPE_FACIAL_RECOGNITION = 2
29
- private val AUTHENTICATION_TYPE_IRIS = 3
30
- private val SECURITY_LEVEL_NONE = 0
31
- private val SECURITY_LEVEL_SECRET = 1
32
- private val SECURITY_LEVEL_BIOMETRIC = 2
33
- private val DEVICE_CREDENTIAL_FALLBACK_CODE = 6
34
- private val biometricManager = BiometricManager.from(context)
35
- private val packageManager = context.packageManager
36
- private var biometricPrompt: BiometricPrompt? = null
37
- private var promise: Promise? = null
38
- private var authOptions: Map<String?, Any?>? = null
39
- private var isRetryingWithDeviceCredentials = false
40
- private var isAuthenticating = false
41
- private val moduleRegistryDelegate: ModuleRegistryDelegate = ModuleRegistryDelegate()
42
- private val uIManager: UIManager by moduleRegistry()
26
+ private const val AUTHENTICATION_TYPE_FINGERPRINT = 1
27
+ private const val AUTHENTICATION_TYPE_FACIAL_RECOGNITION = 2
28
+ private const val AUTHENTICATION_TYPE_IRIS = 3
29
+ private const val SECURITY_LEVEL_NONE = 0
30
+ private const val SECURITY_LEVEL_SECRET = 1
31
+ private const val SECURITY_LEVEL_BIOMETRIC = 2
32
+ private const val DEVICE_CREDENTIAL_FALLBACK_CODE = 6
43
33
 
44
- private inline fun <reified T> moduleRegistry() = moduleRegistryDelegate.getFromModuleRegistry<T>()
34
+ class AuthOptions : Record {
35
+ @Field
36
+ val promptMessage: String = ""
45
37
 
46
- private fun convertErrorCode(code: Int): String {
47
- return when (code) {
48
- BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_NEGATIVE_BUTTON, BiometricPrompt.ERROR_USER_CANCELED -> "user_cancel"
49
- BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE, BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> "not_available"
50
- BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> "lockout"
51
- BiometricPrompt.ERROR_NO_SPACE -> "no_space"
52
- BiometricPrompt.ERROR_TIMEOUT -> "timeout"
53
- BiometricPrompt.ERROR_UNABLE_TO_PROCESS -> "unable_to_process"
54
- else -> "unknown"
38
+ @Field
39
+ val cancelLabel: String = ""
40
+
41
+ @Field
42
+ val disableDeviceFallback: Boolean = false
43
+
44
+ @Field
45
+ val requireConfirmation: Boolean = true
46
+ }
47
+
48
+ class LocalAuthenticationModule : Module() {
49
+ override fun definition() = ModuleDefinition {
50
+ Name("ExpoLocalAuthentication")
51
+
52
+ AsyncFunction("supportedAuthenticationTypesAsync") {
53
+ val results = mutableSetOf<Int>()
54
+ if (canAuthenticateUsingWeakBiometrics() == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
55
+ return@AsyncFunction results
56
+ }
57
+
58
+ // note(cedric): replace hardcoded system feature strings with constants from
59
+ // PackageManager when dropping support for Android SDK 28
60
+ results.apply {
61
+ addIf(hasSystemFeature("android.hardware.fingerprint"), AUTHENTICATION_TYPE_FINGERPRINT)
62
+ addIf(hasSystemFeature("android.hardware.biometrics.face"), AUTHENTICATION_TYPE_FACIAL_RECOGNITION)
63
+ addIf(hasSystemFeature("android.hardware.biometrics.iris"), AUTHENTICATION_TYPE_IRIS)
64
+ addIf(hasSystemFeature("com.samsung.android.bio.face"), AUTHENTICATION_TYPE_FACIAL_RECOGNITION)
65
+ }
66
+
67
+ return@AsyncFunction results
55
68
  }
56
- }
57
69
 
58
- private fun isBiometricUnavailable(code: Int): Boolean {
59
- return when (code) {
60
- BiometricPrompt.ERROR_HW_NOT_PRESENT,
61
- BiometricPrompt.ERROR_HW_UNAVAILABLE,
62
- BiometricPrompt.ERROR_NO_BIOMETRICS,
63
- BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
64
- BiometricPrompt.ERROR_NO_SPACE -> true
65
- else -> false
70
+ AsyncFunction("hasHardwareAsync") {
71
+ canAuthenticateUsingWeakBiometrics() != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
72
+ }
73
+
74
+ AsyncFunction("isEnrolledAsync") {
75
+ canAuthenticateUsingWeakBiometrics() == BiometricManager.BIOMETRIC_SUCCESS
76
+ }
77
+
78
+ AsyncFunction("getEnrolledLevelAsync") {
79
+ var level = SECURITY_LEVEL_NONE
80
+ if (isDeviceSecure) {
81
+ level = SECURITY_LEVEL_SECRET
82
+ }
83
+ if (canAuthenticateUsingWeakBiometrics() == BiometricManager.BIOMETRIC_SUCCESS) {
84
+ level = SECURITY_LEVEL_BIOMETRIC
85
+ }
86
+ return@AsyncFunction level
87
+ }
88
+
89
+ AsyncFunction("authenticateAsync") { options: AuthOptions, promise: Promise ->
90
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
91
+ promise.reject("E_NOT_SUPPORTED", "Cannot display biometric prompt on android versions below 6.0", null)
92
+ return@AsyncFunction
93
+ }
94
+ val fragmentActivity = currentActivity as? FragmentActivity
95
+ if (fragmentActivity == null) {
96
+ promise.reject(Exceptions.MissingActivity())
97
+ return@AsyncFunction
98
+ }
99
+ if (!keyguardManager.isDeviceSecure) {
100
+ promise.resolve(
101
+ createResponse(
102
+ error = "not_enrolled",
103
+ warning = "KeyguardManager#isDeviceSecure() returned false"
104
+ )
105
+ )
106
+ return@AsyncFunction
107
+ }
108
+
109
+ this@LocalAuthenticationModule.authOptions = options
110
+
111
+ // BiometricPrompt callbacks are invoked on the main thread so also run this there to avoid
112
+ // having to do locking.
113
+ appContext.mainQueue.launch {
114
+ authenticate(fragmentActivity, options, promise)
115
+ }
116
+ }
117
+
118
+ AsyncFunction<Unit>("cancelAuthenticate") {
119
+ biometricPrompt?.cancelAuthentication()
120
+ isAuthenticating = false
121
+ }.runOnQueue(Queues.MAIN)
122
+
123
+ OnActivityResult { activity, (requestCode, resultCode, data) ->
124
+ if (requestCode == DEVICE_CREDENTIAL_FALLBACK_CODE) {
125
+ if (resultCode == Activity.RESULT_OK) {
126
+ promise?.resolve(createResponse())
127
+ } else {
128
+ promise?.resolve(
129
+ createResponse(
130
+ error = "user_cancel",
131
+ warning = "Device Credentials canceled"
132
+ )
133
+ )
134
+ }
135
+
136
+ isAuthenticating = false
137
+ isRetryingWithDeviceCredentials = false
138
+ biometricPrompt = null
139
+ promise = null
140
+ authOptions = null
141
+ } else if (activity is FragmentActivity) {
142
+ // If the user uses PIN as an authentication method, the result will be passed to the `onActivityResult`.
143
+ // Unfortunately, react-native doesn't pass this value to the underlying fragment - we won't resolve the promise.
144
+ // So we need to do it manually.
145
+ val fragment = activity.supportFragmentManager.findFragmentByTag("androidx.biometric.BiometricFragment")
146
+ fragment?.onActivityResult(requestCode and 0xffff, resultCode, data)
147
+ }
66
148
  }
67
149
  }
68
150
 
151
+ private val context: Context
152
+ get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
153
+
154
+ private val keyguardManager: KeyguardManager
155
+ get() = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
156
+
157
+ private val currentActivity: Activity?
158
+ get() = appContext.currentActivity
159
+
160
+ private val biometricManager by lazy { BiometricManager.from(context) }
161
+ private val packageManager by lazy { context.packageManager }
162
+ private var biometricPrompt: BiometricPrompt? = null
163
+ private var promise: Promise? = null
164
+ private var authOptions: AuthOptions? = null
165
+ private var isRetryingWithDeviceCredentials = false
166
+ private var isAuthenticating = false
167
+
69
168
  private val authenticationCallback: BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
70
169
  override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
71
170
  isAuthenticating = false
@@ -86,10 +185,10 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
86
185
  val options = authOptions
87
186
 
88
187
  if (options != null) {
89
- val disableDeviceFallback = options["disableDeviceFallback"] as Boolean?
188
+ val disableDeviceFallback = options.disableDeviceFallback
90
189
 
91
190
  // Don't run the device credentials fallback if it's disabled.
92
- if (disableDeviceFallback != true) {
191
+ if (!disableDeviceFallback) {
93
192
  promise?.let {
94
193
  isRetryingWithDeviceCredentials = true
95
194
  promptDeviceCredentialsFallback(options, it)
@@ -103,271 +202,105 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
103
202
  isRetryingWithDeviceCredentials = false
104
203
  biometricPrompt = null
105
204
  promise?.resolve(
106
- Bundle().apply {
107
- putBoolean("success", false)
108
- putString("error", convertErrorCode(errMsgId))
109
- putString("warning", errString.toString())
110
- }
205
+ createResponse(
206
+ error = convertErrorCode(errMsgId),
207
+ warning = errString.toString()
208
+ )
111
209
  )
112
210
  promise = null
113
211
  authOptions = null
114
212
  }
115
213
  }
116
214
 
117
- override fun getName(): String {
118
- return "ExpoLocalAuthentication"
119
- }
120
-
121
- override fun onCreate(moduleRegistry: ModuleRegistry) {
122
- moduleRegistryDelegate.onCreate(moduleRegistry)
123
- uIManager.registerActivityEventListener(this)
124
- }
125
-
126
- @ExpoMethod
127
- fun supportedAuthenticationTypesAsync(promise: Promise) {
128
- val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
129
- val results: MutableList<Int> = ArrayList()
130
- if (result == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
131
- promise.resolve(results)
215
+ @UiThread
216
+ private fun authenticate(fragmentActivity: FragmentActivity, options: AuthOptions, promise: Promise) {
217
+ if (isAuthenticating) {
218
+ this.promise?.resolve(
219
+ createResponse(
220
+ error = "app_cancel"
221
+ )
222
+ )
223
+ this.promise = promise
132
224
  return
133
225
  }
134
226
 
135
- // note(cedric): replace hardcoded system feature strings with constants from
136
- // PackageManager when dropping support for Android SDK 28
137
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
138
- if (packageManager.hasSystemFeature("android.hardware.fingerprint")) {
139
- results.add(AUTHENTICATION_TYPE_FINGERPRINT)
140
- }
141
- }
142
- if (Build.VERSION.SDK_INT >= 29) {
143
- if (packageManager.hasSystemFeature("android.hardware.biometrics.face")) {
144
- results.add(AUTHENTICATION_TYPE_FACIAL_RECOGNITION)
145
- }
146
- if (packageManager.hasSystemFeature("android.hardware.biometrics.iris")) {
147
- results.add(AUTHENTICATION_TYPE_IRIS)
148
- }
149
- }
227
+ val promptMessage = options.promptMessage
228
+ val cancelLabel = options.cancelLabel
229
+ val disableDeviceFallback = options.disableDeviceFallback
230
+ val requireConfirmation = options.requireConfirmation
150
231
 
151
- // check for face recognition support on some samsung devices
152
- if (packageManager.hasSystemFeature("com.samsung.android.bio.face") && !results.contains(AUTHENTICATION_TYPE_FACIAL_RECOGNITION)) {
153
- results.add(AUTHENTICATION_TYPE_FACIAL_RECOGNITION)
154
- }
232
+ isAuthenticating = true
233
+ this.promise = promise
234
+ val executor: Executor = Executors.newSingleThreadExecutor()
235
+ biometricPrompt = BiometricPrompt(fragmentActivity, executor, authenticationCallback)
236
+ val promptInfoBuilder = PromptInfo.Builder().apply {
237
+ setTitle(promptMessage)
155
238
 
156
- promise.resolve(results)
157
- }
158
-
159
- @ExpoMethod
160
- fun hasHardwareAsync(promise: Promise) {
161
- val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
162
- promise.resolve(result != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE)
163
- }
164
-
165
- @ExpoMethod
166
- fun isEnrolledAsync(promise: Promise) {
167
- val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
168
- promise.resolve(result == BiometricManager.BIOMETRIC_SUCCESS)
169
- }
170
-
171
- @ExpoMethod
172
- fun getEnrolledLevelAsync(promise: Promise) {
173
- var level = SECURITY_LEVEL_NONE
174
- if (isDeviceSecure) {
175
- level = SECURITY_LEVEL_SECRET
239
+ if (disableDeviceFallback) {
240
+ setNegativeButtonText(cancelLabel)
241
+ } else {
242
+ setAllowedAuthenticators(
243
+ BiometricManager.Authenticators.BIOMETRIC_WEAK
244
+ or BiometricManager.Authenticators.DEVICE_CREDENTIAL
245
+ )
246
+ }
247
+ setConfirmationRequired(requireConfirmation)
176
248
  }
177
- val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
178
- if (result == BiometricManager.BIOMETRIC_SUCCESS) {
179
- level = SECURITY_LEVEL_BIOMETRIC
249
+
250
+ val promptInfo = promptInfoBuilder.build()
251
+ try {
252
+ biometricPrompt!!.authenticate(promptInfo)
253
+ } catch (e: NullPointerException) {
254
+ promise.reject(UnexpectedException("Canceled authentication due to an internal error", e))
180
255
  }
181
- promise.resolve(level)
182
256
  }
183
257
 
184
- @ExpoMethod
185
- fun authenticateAsync(options: Map<String?, Any?>, promise: Promise) {
186
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
187
- promise.reject("E_NOT_SUPPORTED", "Cannot display biometric prompt on android versions below 6.0")
188
- return
189
- }
190
- if (currentActivity == null) {
191
- promise.reject("E_NOT_FOREGROUND", "Cannot display biometric prompt when the app is not in the foreground")
192
- return
193
- }
194
- if (!keyguardManager.isDeviceSecure) {
195
- promise.resolve(
196
- Bundle().apply {
197
- putBoolean("success", false)
198
- putString("error", "not_enrolled")
199
- putString("warning", "KeyguardManager#isDeviceSecure() returned false")
200
- }
201
- )
202
- return
203
- }
258
+ private fun promptDeviceCredentialsFallback(options: AuthOptions, promise: Promise) {
204
259
  val fragmentActivity = currentActivity as FragmentActivity?
205
260
  if (fragmentActivity == null) {
206
261
  promise.resolve(
207
- Bundle().apply {
208
- putBoolean("success", false)
209
- putString("error", "not_available")
210
- putString("warning", "getCurrentActivity() returned null")
211
- }
262
+ createResponse(
263
+ error = "not_available",
264
+ warning = "getCurrentActivity() returned null"
265
+ )
212
266
  )
213
267
  return
214
268
  }
215
269
 
216
- this.authOptions = options
270
+ val promptMessage = options.promptMessage
271
+ val requireConfirmation = options.requireConfirmation
217
272
 
218
273
  // BiometricPrompt callbacks are invoked on the main thread so also run this there to avoid
219
274
  // having to do locking.
220
- uIManager.runOnUiQueueThread(
221
- Runnable {
222
- if (isAuthenticating) {
223
- this.promise?.resolve(
224
- Bundle().apply {
225
- putBoolean("success", false)
226
- putString("error", "app_cancel")
227
- }
228
- )
229
- this.promise = promise
230
- return@Runnable
231
- }
232
- val promptMessage = if (options.containsKey("promptMessage")) {
233
- options["promptMessage"] as String?
234
- } else {
235
- ""
236
- }
237
- val cancelLabel = if (options.containsKey("cancelLabel")) {
238
- options["cancelLabel"] as String?
239
- } else {
240
- ""
241
- }
242
- val disableDeviceFallback = if (options.containsKey("disableDeviceFallback")) {
243
- options["disableDeviceFallback"] as Boolean?
244
- } else {
245
- false
246
- }
247
- val requireConfirmation = options["requireConfirmation"] as? Boolean ?: true
248
- isAuthenticating = true
249
- this.promise = promise
250
- val executor: Executor = Executors.newSingleThreadExecutor()
251
- biometricPrompt = BiometricPrompt(fragmentActivity, executor, authenticationCallback)
252
- val promptInfoBuilder = PromptInfo.Builder()
253
- promptMessage?.let {
254
- promptInfoBuilder.setTitle(it)
255
- }
256
- if (disableDeviceFallback == true) {
257
- cancelLabel?.let {
258
- promptInfoBuilder.setNegativeButtonText(it)
259
- }
260
- } else {
261
- promptInfoBuilder.setAllowedAuthenticators(
262
- BiometricManager.Authenticators.BIOMETRIC_WEAK
263
- or BiometricManager.Authenticators.DEVICE_CREDENTIAL
264
- )
265
- }
266
- promptInfoBuilder.setConfirmationRequired(requireConfirmation)
267
- val promptInfo = promptInfoBuilder.build()
268
- try {
269
- biometricPrompt!!.authenticate(promptInfo)
270
- } catch (ex: NullPointerException) {
271
- promise.reject("E_INTERNAL_ERRROR", "Canceled authentication due to an internal error")
272
- }
275
+ appContext.mainQueue.launch {
276
+ // On Android devices older than 11, we need to use Keyguard to unlock by Device Credentials.
277
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
278
+ val credentialConfirmationIntent = keyguardManager.createConfirmDeviceCredentialIntent(promptMessage, "")
279
+ fragmentActivity.startActivityForResult(credentialConfirmationIntent, DEVICE_CREDENTIAL_FALLBACK_CODE)
280
+ return@launch
273
281
  }
274
- )
275
- }
276
282
 
277
- @ExpoMethod
278
- fun cancelAuthenticate(promise: Promise) {
279
- uIManager.runOnUiQueueThread {
280
- biometricPrompt?.cancelAuthentication()
281
- isAuthenticating = false
282
- promise.resolve(null)
283
- }
284
- }
283
+ val executor: Executor = Executors.newSingleThreadExecutor()
284
+ val localBiometricPrompt = BiometricPrompt(fragmentActivity, executor, authenticationCallback)
285
285
 
286
- fun promptDeviceCredentialsFallback(options: Map<String?, Any?>, promise: Promise) {
287
- val fragmentActivity = currentActivity as FragmentActivity?
288
- if (fragmentActivity == null) {
289
- promise.resolve(
290
- Bundle().apply {
291
- putBoolean("success", false)
292
- putString("error", "not_available")
293
- putString("warning", "getCurrentActivity() returned null")
294
- }
295
- )
296
- return
297
- }
298
-
299
- val promptMessage = options["promptMessage"] as? String ?: ""
300
- val requireConfirmation = options["requireConfirmation"] as? Boolean ?: true
301
-
302
- // BiometricPrompt callbacks are invoked on the main thread so also run this there to avoid
303
- // having to do locking.
304
- uIManager.runOnUiQueueThread(
305
- Runnable {
306
- // On Android devices older than 11, we need to use Keyguard to unlock by Device Credentials.
307
- if (Build.VERSION.SDK_INT < 30) {
308
- val credentialConfirmationIntent = keyguardManager.createConfirmDeviceCredentialIntent(promptMessage, "")
309
- fragmentActivity.startActivityForResult(credentialConfirmationIntent, DEVICE_CREDENTIAL_FALLBACK_CODE)
310
- return@Runnable
311
- }
312
-
313
- val executor: Executor = Executors.newSingleThreadExecutor()
314
- val localBiometricPrompt = BiometricPrompt(fragmentActivity, executor, authenticationCallback)
315
- if (localBiometricPrompt == null) {
316
- promise.reject("E_INTERNAL_ERRROR", "Canceled authentication due to an internal error")
317
- return@Runnable
318
- }
319
- biometricPrompt = localBiometricPrompt
286
+ biometricPrompt = localBiometricPrompt
320
287
 
321
- val promptInfoBuilder = PromptInfo.Builder()
322
- promptMessage?.let {
323
- promptInfoBuilder.setTitle(it)
324
- }
325
- promptInfoBuilder.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
326
- promptInfoBuilder.setConfirmationRequired(requireConfirmation)
327
- val promptInfo = promptInfoBuilder.build()
328
- try {
329
- localBiometricPrompt.authenticate(promptInfo)
330
- } catch (ex: NullPointerException) {
331
- promise.reject("E_INTERNAL_ERRROR", "Canceled authentication due to an internal error")
332
- }
288
+ val promptInfoBuilder = PromptInfo.Builder().apply {
289
+ setTitle(promptMessage)
290
+ setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
291
+ setConfirmationRequired(requireConfirmation)
333
292
  }
334
- )
335
- }
336
293
 
337
- override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
338
- // When Biometric is unavailable and using Keyguard fallback, the result will be handled here.
339
- if (requestCode == DEVICE_CREDENTIAL_FALLBACK_CODE) {
340
- if (resultCode == Activity.RESULT_OK) {
341
- promise?.resolve(
342
- Bundle().apply {
343
- putBoolean("success", true)
344
- }
345
- )
346
- } else {
347
- promise?.resolve(
348
- Bundle().apply {
349
- putBoolean("success", false)
350
- putString("error", "user_cancel")
351
- putString("warning", "Device Credentials canceled")
352
- }
353
- )
294
+ val promptInfo = promptInfoBuilder.build()
295
+ try {
296
+ localBiometricPrompt.authenticate(promptInfo)
297
+ } catch (e: NullPointerException) {
298
+ promise.reject(UnexpectedException("Canceled authentication due to an internal error", e))
354
299
  }
355
-
356
- isAuthenticating = false
357
- isRetryingWithDeviceCredentials = false
358
- biometricPrompt = null
359
- promise = null
360
- authOptions = null
361
- } else if (activity is FragmentActivity) {
362
- // If the user uses PIN as an authentication method, the result will be passed to the `onActivityResult`.
363
- // Unfortunately, react-native doesn't pass this value to the underlying fragment - we won't resolve the promise.
364
- // So we need to do it manually.
365
- val fragment = activity.supportFragmentManager.findFragmentByTag("androidx.biometric.BiometricFragment")
366
- fragment?.onActivityResult(requestCode and 0xffff, resultCode, data)
367
300
  }
368
301
  }
369
302
 
370
- override fun onNewIntent(intent: Intent) = Unit
303
+ private fun hasSystemFeature(feature: String) = packageManager.hasSystemFeature(feature)
371
304
 
372
305
  // NOTE: `KeyguardManager#isKeyguardSecure()` considers SIM locked state,
373
306
  // but it will be ignored on falling-back to device credential on biometric authentication.
@@ -392,11 +325,49 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
392
325
  keyguardManager.isKeyguardSecure
393
326
  }
394
327
 
395
- private val keyguardManager: KeyguardManager
396
- get() = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
397
- private val currentActivity: Activity?
398
- get() {
399
- val activityProvider: ActivityProvider by moduleRegistry()
400
- return activityProvider.currentActivity
328
+ private fun convertErrorCode(code: Int): String {
329
+ return when (code) {
330
+ BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_NEGATIVE_BUTTON, BiometricPrompt.ERROR_USER_CANCELED -> "user_cancel"
331
+ BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE, BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> "not_available"
332
+ BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> "lockout"
333
+ BiometricPrompt.ERROR_NO_SPACE -> "no_space"
334
+ BiometricPrompt.ERROR_TIMEOUT -> "timeout"
335
+ BiometricPrompt.ERROR_UNABLE_TO_PROCESS -> "unable_to_process"
336
+ else -> "unknown"
401
337
  }
338
+ }
339
+
340
+ private fun isBiometricUnavailable(code: Int): Boolean {
341
+ return when (code) {
342
+ BiometricPrompt.ERROR_HW_NOT_PRESENT,
343
+ BiometricPrompt.ERROR_HW_UNAVAILABLE,
344
+ BiometricPrompt.ERROR_NO_BIOMETRICS,
345
+ BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
346
+ BiometricPrompt.ERROR_NO_SPACE -> true
347
+
348
+ else -> false
349
+ }
350
+ }
351
+
352
+ private fun canAuthenticateUsingWeakBiometrics(): Int =
353
+ biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
354
+
355
+ private fun createResponse(
356
+ error: String? = null,
357
+ warning: String? = null
358
+ ) = Bundle().apply {
359
+ putBoolean("success", error == null)
360
+ error?.let {
361
+ putString("error", it)
362
+ }
363
+ warning?.let {
364
+ putString("warning", it)
365
+ }
366
+ }
367
+ }
368
+
369
+ fun <T> MutableSet<T>.addIf(condition: Boolean, valueToAdd: T) {
370
+ if (condition) {
371
+ add(valueToAdd)
372
+ }
402
373
  }
@@ -3,5 +3,8 @@
3
3
  "platforms": ["ios", "android"],
4
4
  "ios": {
5
5
  "modules": ["LocalAuthenticationModule"]
6
+ },
7
+ "android": {
8
+ "modules": ["expo.modules.localauthentication.LocalAuthenticationModule"]
6
9
  }
7
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-local-authentication",
3
- "version": "13.5.0",
3
+ "version": "13.6.0",
4
4
  "description": "Provides an API for FaceID and TouchID (iOS) or the Fingerprint API (Android) to authenticate the user with a face or fingerprint scan.",
5
5
  "main": "build/LocalAuthentication.js",
6
6
  "types": "build/LocalAuthentication.d.ts",
@@ -46,5 +46,5 @@
46
46
  "peerDependencies": {
47
47
  "expo": "*"
48
48
  },
49
- "gitHead": "2240630a92eb79a4e4bf73e1439916c394876478"
49
+ "gitHead": "79607a7325f47aa17c36d266100d09a4ff2cc544"
50
50
  }
@@ -1,11 +0,0 @@
1
- package expo.modules.localauthentication
2
-
3
- import android.content.Context
4
- import expo.modules.core.BasePackage
5
- import expo.modules.core.ExportedModule
6
-
7
- class LocalAuthenticationPackage : BasePackage() {
8
- override fun createExportedModules(context: Context): List<ExportedModule> {
9
- return listOf<ExportedModule>(LocalAuthenticationModule(context))
10
- }
11
- }