expo-local-authentication 13.3.0 → 13.4.1

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,7 +10,18 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
- ## 13.3.0 — 2023-04-10
13
+ ## 13.4.1 — 2023-06-13
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [Android] Fixed device credentials fallback when biometric sensors are unavailable ([#22388](https://github.com/expo/expo/pull/22388) by [@hubastard](https://github.com/hubastard))
18
+ - Fixed Android build warnings for Gradle version 8. ([#22537](https://github.com/expo/expo/pull/22537), [#22609](https://github.com/expo/expo/pull/22609) by [@kudo](https://github.com/kudo))
19
+
20
+ ## 13.4.0 — 2023-05-08
21
+
22
+ _This version does not introduce any user-facing changes._
23
+
24
+ ## 13.3.0 - 2023-04-10
14
25
 
15
26
  ### 🐛 Bug fixes
16
27
 
package/README.md CHANGED
@@ -16,7 +16,7 @@ Provides an API for FaceID and TouchID (iOS) or the Fingerprint API (Android) to
16
16
 
17
17
  # Installation in managed Expo projects
18
18
 
19
- For [managed](https://docs.expo.dev/versions/latest/introduction/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/local-authentication/).
19
+ For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/local-authentication/).
20
20
 
21
21
  # Installation in bare React Native projects
22
22
 
@@ -25,7 +25,7 @@ For bare React Native projects, you must ensure that you have [installed and con
25
25
  ### Add the package to your npm dependencies
26
26
 
27
27
  ```
28
- expo install expo-local-authentication
28
+ npx expo install expo-local-authentication
29
29
  ```
30
30
 
31
31
  ### Configure for iOS
@@ -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.3.0'
6
+ version = '13.4.1'
7
7
 
8
8
  buildscript {
9
9
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
@@ -35,19 +35,11 @@ buildscript {
35
35
  }
36
36
  }
37
37
 
38
- // Creating sources with comments
39
- task androidSourcesJar(type: Jar) {
40
- classifier = 'sources'
41
- from android.sourceSets.main.java.srcDirs
42
- }
43
-
44
38
  afterEvaluate {
45
39
  publishing {
46
40
  publications {
47
41
  release(MavenPublication) {
48
42
  from components.release
49
- // Add additional sourcesJar to artifacts
50
- artifact(androidSourcesJar)
51
43
  }
52
44
  }
53
45
  repositories {
@@ -70,15 +62,21 @@ android {
70
62
  jvmTarget = JavaVersion.VERSION_11.majorVersion
71
63
  }
72
64
 
65
+ namespace "expo.modules.localauthentication"
73
66
  defaultConfig {
74
67
  minSdkVersion safeExtGet("minSdkVersion", 21)
75
68
  targetSdkVersion safeExtGet("targetSdkVersion", 33)
76
69
  versionCode 30
77
- versionName "13.3.0"
70
+ versionName "13.4.1"
78
71
  }
79
72
  lintOptions {
80
73
  abortOnError false
81
74
  }
75
+ publishing {
76
+ singleVariant("release") {
77
+ withSourcesJar()
78
+ }
79
+ }
82
80
  }
83
81
 
84
82
  dependencies {
@@ -1,4 +1,4 @@
1
- <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="expo.modules.localauthentication">
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
2
  <uses-permission android:name="android.permission.USE_FINGERPRINT" />
3
3
  <uses-permission android:name="android.permission.USE_BIOMETRIC" />
4
4
  </manifest>
@@ -30,10 +30,13 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
30
30
  private val SECURITY_LEVEL_NONE = 0
31
31
  private val SECURITY_LEVEL_SECRET = 1
32
32
  private val SECURITY_LEVEL_BIOMETRIC = 2
33
+ private val DEVICE_CREDENTIAL_FALLBACK_CODE = 6
33
34
  private val biometricManager = BiometricManager.from(context)
34
35
  private val packageManager = context.packageManager
35
36
  private var biometricPrompt: BiometricPrompt? = null
36
37
  private var promise: Promise? = null
38
+ private var authOptions: Map<String?, Any?>? = null
39
+ private var isRetryingWithDeviceCredentials = false
37
40
  private var isAuthenticating = false
38
41
  private val moduleRegistryDelegate: ModuleRegistryDelegate = ModuleRegistryDelegate()
39
42
  private val uIManager: UIManager by moduleRegistry()
@@ -52,9 +55,21 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
52
55
  }
53
56
  }
54
57
 
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
66
+ }
67
+ }
68
+
55
69
  private val authenticationCallback: BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
56
70
  override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
57
71
  isAuthenticating = false
72
+ isRetryingWithDeviceCredentials = false
58
73
  biometricPrompt = null
59
74
  promise?.resolve(
60
75
  Bundle().apply {
@@ -62,10 +77,30 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
62
77
  }
63
78
  )
64
79
  promise = null
80
+ authOptions = null
65
81
  }
66
82
 
67
83
  override fun onAuthenticationError(errMsgId: Int, errString: CharSequence) {
84
+ // Make sure to fallback to the Device Credentials if the Biometrics hardware is unavailable.
85
+ if (isBiometricUnavailable(errMsgId) && isDeviceSecure && !isRetryingWithDeviceCredentials) {
86
+ val options = authOptions
87
+
88
+ if (options != null) {
89
+ val disableDeviceFallback = options["disableDeviceFallback"] as Boolean?
90
+
91
+ // Don't run the device credentials fallback if it's disabled.
92
+ if (disableDeviceFallback != true) {
93
+ promise?.let {
94
+ isRetryingWithDeviceCredentials = true
95
+ promptDeviceCredentialsFallback(options, it)
96
+ return
97
+ }
98
+ }
99
+ }
100
+ }
101
+
68
102
  isAuthenticating = false
103
+ isRetryingWithDeviceCredentials = false
69
104
  biometricPrompt = null
70
105
  promise?.resolve(
71
106
  Bundle().apply {
@@ -75,6 +110,7 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
75
110
  }
76
111
  )
77
112
  promise = null
113
+ authOptions = null
78
114
  }
79
115
  }
80
116
 
@@ -177,6 +213,8 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
177
213
  return
178
214
  }
179
215
 
216
+ this.authOptions = options
217
+
180
218
  // BiometricPrompt callbacks are invoked on the main thread so also run this there to avoid
181
219
  // having to do locking.
182
220
  uIManager.runOnUiQueueThread(
@@ -245,11 +283,85 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
245
283
  }
246
284
  }
247
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
320
+
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
+ }
333
+ }
334
+ )
335
+ }
336
+
248
337
  override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
249
- // If the user uses PIN as an authentication method, the result will be passed to the `onActivityResult`.
250
- // Unfortunately, react-native doesn't pass this value to the underlying fragment - we won't resolve the promise.
251
- // So we need to do it manually.
252
- if (activity is FragmentActivity) {
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
+ )
354
+ }
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.
253
365
  val fragment = activity.supportFragmentManager.findFragmentByTag("androidx.biometric.BiometricFragment")
254
366
  fragment?.onActivityResult(requestCode and 0xffff, resultCode, data)
255
367
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-local-authentication",
3
- "version": "13.3.0",
3
+ "version": "13.4.1",
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": "5613f862a64dfa5e046b9c4a336280e3ac6dd9df"
49
+ "gitHead": "3ccd2edee9cbfed217557675cb50f0ba5e55a9e4"
50
50
  }