expo-local-authentication 13.5.0 → 13.7.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,22 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 13.7.0 — 2023-10-17
14
+
15
+ ### 🛠 Breaking changes
16
+
17
+ - Dropped support for Android SDK 21 and 22. ([#24201](https://github.com/expo/expo/pull/24201) by [@behenate](https://github.com/behenate))
18
+
19
+ ## 13.6.0 — 2023-09-04
20
+
21
+ ### 🎉 New features
22
+
23
+ - Added support for React Native 0.73. ([#24018](https://github.com/expo/expo/pull/24018) by [@kudo](https://github.com/kudo))
24
+
25
+ ### 💡 Others
26
+
27
+ - [Android] Migrate to use Expo Modules API. ([#24083](https://github.com/expo/expo/pull/24083) by [@lukmccall](https://github.com/lukmccall))
28
+
13
29
  ## 13.5.0 — 2023-08-02
14
30
 
15
31
  _This version does not introduce any user-facing changes._
@@ -3,15 +3,20 @@ 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.7.0'
7
7
 
8
- buildscript {
9
- def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10
- if (expoModulesCorePlugin.exists()) {
11
- apply from: expoModulesCorePlugin
12
- applyKotlinExpoModulesCorePlugin()
8
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
9
+ if (expoModulesCorePlugin.exists()) {
10
+ apply from: expoModulesCorePlugin
11
+ applyKotlinExpoModulesCorePlugin()
12
+ // Remove this check, but keep the contents after SDK49 support is dropped
13
+ if (safeExtGet("expoProvidesDefaultConfig", false)) {
14
+ useExpoPublishing()
15
+ useCoreDependencies()
13
16
  }
17
+ }
14
18
 
19
+ buildscript {
15
20
  // Simple helper that allows the root project to override versions declared by this library.
16
21
  ext.safeExtGet = { prop, fallback ->
17
22
  rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
@@ -35,54 +40,70 @@ buildscript {
35
40
  }
36
41
  }
37
42
 
38
- afterEvaluate {
39
- publishing {
40
- publications {
41
- release(MavenPublication) {
42
- from components.release
43
+ // Remove this if and it's contents, when support for SDK49 is dropped
44
+ if (!safeExtGet("expoProvidesDefaultConfig", false)) {
45
+ afterEvaluate {
46
+ publishing {
47
+ publications {
48
+ release(MavenPublication) {
49
+ from components.release
50
+ }
43
51
  }
44
- }
45
- repositories {
46
- maven {
47
- url = mavenLocal().url
52
+ repositories {
53
+ maven {
54
+ url = mavenLocal().url
55
+ }
48
56
  }
49
57
  }
50
58
  }
51
59
  }
52
60
 
53
61
  android {
54
- compileSdkVersion safeExtGet("compileSdkVersion", 33)
62
+ // Remove this if and it's contents, when support for SDK49 is dropped
63
+ if (!safeExtGet("expoProvidesDefaultConfig", false)) {
64
+ compileSdkVersion safeExtGet("compileSdkVersion", 33)
65
+
66
+ defaultConfig {
67
+ minSdkVersion safeExtGet("minSdkVersion", 23)
68
+ targetSdkVersion safeExtGet("targetSdkVersion", 33)
69
+ }
70
+
71
+ publishing {
72
+ singleVariant("release") {
73
+ withSourcesJar()
74
+ }
75
+ }
55
76
 
56
- compileOptions {
57
- sourceCompatibility JavaVersion.VERSION_11
58
- targetCompatibility JavaVersion.VERSION_11
77
+ lintOptions {
78
+ abortOnError false
79
+ }
59
80
  }
60
81
 
61
- kotlinOptions {
62
- jvmTarget = JavaVersion.VERSION_11.majorVersion
82
+ def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
83
+ if (agpVersion.tokenize('.')[0].toInteger() < 8) {
84
+ compileOptions {
85
+ sourceCompatibility JavaVersion.VERSION_11
86
+ targetCompatibility JavaVersion.VERSION_11
87
+ }
88
+
89
+ kotlinOptions {
90
+ jvmTarget = JavaVersion.VERSION_11.majorVersion
91
+ }
63
92
  }
64
93
 
65
94
  namespace "expo.modules.localauthentication"
66
95
  defaultConfig {
67
- minSdkVersion safeExtGet("minSdkVersion", 21)
68
- targetSdkVersion safeExtGet("targetSdkVersion", 33)
69
96
  versionCode 30
70
- versionName "13.5.0"
71
- }
72
- lintOptions {
73
- abortOnError false
74
- }
75
- publishing {
76
- singleVariant("release") {
77
- withSourcesJar()
78
- }
97
+ versionName "13.7.0"
79
98
  }
80
99
  }
81
100
 
82
101
  dependencies {
83
- implementation project(':expo-modules-core')
102
+ // Remove this if and it's contents, when support for SDK49 is dropped
103
+ if (!safeExtGet("expoProvidesDefaultConfig", false)) {
104
+ implementation project(':expo-modules-core')
105
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
106
+ }
84
107
 
85
108
  implementation "androidx.biometric:biometric:1.2.0-alpha04"
86
-
87
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
88
109
  }
@@ -4,68 +4,163 @@ 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
+ val fragmentActivity = currentActivity as? FragmentActivity
91
+ if (fragmentActivity == null) {
92
+ promise.reject(Exceptions.MissingActivity())
93
+ return@AsyncFunction
94
+ }
95
+ if (!keyguardManager.isDeviceSecure) {
96
+ promise.resolve(
97
+ createResponse(
98
+ error = "not_enrolled",
99
+ warning = "KeyguardManager#isDeviceSecure() returned false"
100
+ )
101
+ )
102
+ return@AsyncFunction
103
+ }
104
+
105
+ this@LocalAuthenticationModule.authOptions = options
106
+
107
+ // BiometricPrompt callbacks are invoked on the main thread so also run this there to avoid
108
+ // having to do locking.
109
+ appContext.mainQueue.launch {
110
+ authenticate(fragmentActivity, options, promise)
111
+ }
112
+ }
113
+
114
+ AsyncFunction<Unit>("cancelAuthenticate") {
115
+ biometricPrompt?.cancelAuthentication()
116
+ isAuthenticating = false
117
+ }.runOnQueue(Queues.MAIN)
118
+
119
+ OnActivityResult { activity, (requestCode, resultCode, data) ->
120
+ if (requestCode == DEVICE_CREDENTIAL_FALLBACK_CODE) {
121
+ if (resultCode == Activity.RESULT_OK) {
122
+ promise?.resolve(createResponse())
123
+ } else {
124
+ promise?.resolve(
125
+ createResponse(
126
+ error = "user_cancel",
127
+ warning = "Device Credentials canceled"
128
+ )
129
+ )
130
+ }
131
+
132
+ isAuthenticating = false
133
+ isRetryingWithDeviceCredentials = false
134
+ biometricPrompt = null
135
+ promise = null
136
+ authOptions = null
137
+ } else if (activity is FragmentActivity) {
138
+ // If the user uses PIN as an authentication method, the result will be passed to the `onActivityResult`.
139
+ // Unfortunately, react-native doesn't pass this value to the underlying fragment - we won't resolve the promise.
140
+ // So we need to do it manually.
141
+ val fragment = activity.supportFragmentManager.findFragmentByTag("androidx.biometric.BiometricFragment")
142
+ fragment?.onActivityResult(requestCode and 0xffff, resultCode, data)
143
+ }
66
144
  }
67
145
  }
68
146
 
147
+ private val context: Context
148
+ get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
149
+
150
+ private val keyguardManager: KeyguardManager
151
+ get() = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
152
+
153
+ private val currentActivity: Activity?
154
+ get() = appContext.currentActivity
155
+
156
+ private val biometricManager by lazy { BiometricManager.from(context) }
157
+ private val packageManager by lazy { context.packageManager }
158
+ private var biometricPrompt: BiometricPrompt? = null
159
+ private var promise: Promise? = null
160
+ private var authOptions: AuthOptions? = null
161
+ private var isRetryingWithDeviceCredentials = false
162
+ private var isAuthenticating = false
163
+
69
164
  private val authenticationCallback: BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
70
165
  override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
71
166
  isAuthenticating = false
@@ -86,10 +181,10 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
86
181
  val options = authOptions
87
182
 
88
183
  if (options != null) {
89
- val disableDeviceFallback = options["disableDeviceFallback"] as Boolean?
184
+ val disableDeviceFallback = options.disableDeviceFallback
90
185
 
91
186
  // Don't run the device credentials fallback if it's disabled.
92
- if (disableDeviceFallback != true) {
187
+ if (!disableDeviceFallback) {
93
188
  promise?.let {
94
189
  isRetryingWithDeviceCredentials = true
95
190
  promptDeviceCredentialsFallback(options, it)
@@ -103,271 +198,105 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
103
198
  isRetryingWithDeviceCredentials = false
104
199
  biometricPrompt = null
105
200
  promise?.resolve(
106
- Bundle().apply {
107
- putBoolean("success", false)
108
- putString("error", convertErrorCode(errMsgId))
109
- putString("warning", errString.toString())
110
- }
201
+ createResponse(
202
+ error = convertErrorCode(errMsgId),
203
+ warning = errString.toString()
204
+ )
111
205
  )
112
206
  promise = null
113
207
  authOptions = null
114
208
  }
115
209
  }
116
210
 
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)
211
+ @UiThread
212
+ private fun authenticate(fragmentActivity: FragmentActivity, options: AuthOptions, promise: Promise) {
213
+ if (isAuthenticating) {
214
+ this.promise?.resolve(
215
+ createResponse(
216
+ error = "app_cancel"
217
+ )
218
+ )
219
+ this.promise = promise
132
220
  return
133
221
  }
134
222
 
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
- }
223
+ val promptMessage = options.promptMessage
224
+ val cancelLabel = options.cancelLabel
225
+ val disableDeviceFallback = options.disableDeviceFallback
226
+ val requireConfirmation = options.requireConfirmation
150
227
 
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
- }
228
+ isAuthenticating = true
229
+ this.promise = promise
230
+ val executor: Executor = Executors.newSingleThreadExecutor()
231
+ biometricPrompt = BiometricPrompt(fragmentActivity, executor, authenticationCallback)
232
+ val promptInfoBuilder = PromptInfo.Builder().apply {
233
+ setTitle(promptMessage)
155
234
 
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
235
+ if (disableDeviceFallback) {
236
+ setNegativeButtonText(cancelLabel)
237
+ } else {
238
+ setAllowedAuthenticators(
239
+ BiometricManager.Authenticators.BIOMETRIC_WEAK
240
+ or BiometricManager.Authenticators.DEVICE_CREDENTIAL
241
+ )
242
+ }
243
+ setConfirmationRequired(requireConfirmation)
176
244
  }
177
- val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
178
- if (result == BiometricManager.BIOMETRIC_SUCCESS) {
179
- level = SECURITY_LEVEL_BIOMETRIC
245
+
246
+ val promptInfo = promptInfoBuilder.build()
247
+ try {
248
+ biometricPrompt!!.authenticate(promptInfo)
249
+ } catch (e: NullPointerException) {
250
+ promise.reject(UnexpectedException("Canceled authentication due to an internal error", e))
180
251
  }
181
- promise.resolve(level)
182
252
  }
183
253
 
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
- }
254
+ private fun promptDeviceCredentialsFallback(options: AuthOptions, promise: Promise) {
204
255
  val fragmentActivity = currentActivity as FragmentActivity?
205
256
  if (fragmentActivity == null) {
206
257
  promise.resolve(
207
- Bundle().apply {
208
- putBoolean("success", false)
209
- putString("error", "not_available")
210
- putString("warning", "getCurrentActivity() returned null")
211
- }
258
+ createResponse(
259
+ error = "not_available",
260
+ warning = "getCurrentActivity() returned null"
261
+ )
212
262
  )
213
263
  return
214
264
  }
215
265
 
216
- this.authOptions = options
266
+ val promptMessage = options.promptMessage
267
+ val requireConfirmation = options.requireConfirmation
217
268
 
218
269
  // BiometricPrompt callbacks are invoked on the main thread so also run this there to avoid
219
270
  // 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
- }
271
+ appContext.mainQueue.launch {
272
+ // On Android devices older than 11, we need to use Keyguard to unlock by Device Credentials.
273
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
274
+ val credentialConfirmationIntent = keyguardManager.createConfirmDeviceCredentialIntent(promptMessage, "")
275
+ fragmentActivity.startActivityForResult(credentialConfirmationIntent, DEVICE_CREDENTIAL_FALLBACK_CODE)
276
+ return@launch
273
277
  }
274
- )
275
- }
276
278
 
277
- @ExpoMethod
278
- fun cancelAuthenticate(promise: Promise) {
279
- uIManager.runOnUiQueueThread {
280
- biometricPrompt?.cancelAuthentication()
281
- isAuthenticating = false
282
- promise.resolve(null)
283
- }
284
- }
279
+ val executor: Executor = Executors.newSingleThreadExecutor()
280
+ val localBiometricPrompt = BiometricPrompt(fragmentActivity, executor, authenticationCallback)
285
281
 
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
282
+ biometricPrompt = localBiometricPrompt
320
283
 
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
- }
284
+ val promptInfoBuilder = PromptInfo.Builder().apply {
285
+ setTitle(promptMessage)
286
+ setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
287
+ setConfirmationRequired(requireConfirmation)
333
288
  }
334
- )
335
- }
336
289
 
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
- )
290
+ val promptInfo = promptInfoBuilder.build()
291
+ try {
292
+ localBiometricPrompt.authenticate(promptInfo)
293
+ } catch (e: NullPointerException) {
294
+ promise.reject(UnexpectedException("Canceled authentication due to an internal error", e))
354
295
  }
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
296
  }
368
297
  }
369
298
 
370
- override fun onNewIntent(intent: Intent) = Unit
299
+ private fun hasSystemFeature(feature: String) = packageManager.hasSystemFeature(feature)
371
300
 
372
301
  // NOTE: `KeyguardManager#isKeyguardSecure()` considers SIM locked state,
373
302
  // but it will be ignored on falling-back to device credential on biometric authentication.
@@ -378,25 +307,51 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act
378
307
  // supported prior to API 30.
379
308
  // https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
380
309
  private val isDeviceSecure: Boolean
381
- get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
382
- keyguardManager.isDeviceSecure
383
- } else {
384
- // NOTE: `KeyguardManager#isKeyguardSecure()` considers SIM locked state,
385
- // but it will be ignored on falling-back to device credential on biometric authentication.
386
- // That means, setting level to `SECURITY_LEVEL_SECRET` might be misleading for some users.
387
- // But there is no equivalent APIs prior to M.
388
- // `andriodx.biometric.BiometricManager#canAuthenticate(int)` looks like an alternative,
389
- // but specifying `BiometricManager.Authenticators.DEVICE_CREDENTIAL` alone is not
390
- // supported prior to API 30.
391
- // https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
392
- keyguardManager.isKeyguardSecure
310
+ get() = keyguardManager.isDeviceSecure
311
+
312
+ private fun convertErrorCode(code: Int): String {
313
+ return when (code) {
314
+ BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_NEGATIVE_BUTTON, BiometricPrompt.ERROR_USER_CANCELED -> "user_cancel"
315
+ BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE, BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> "not_available"
316
+ BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> "lockout"
317
+ BiometricPrompt.ERROR_NO_SPACE -> "no_space"
318
+ BiometricPrompt.ERROR_TIMEOUT -> "timeout"
319
+ BiometricPrompt.ERROR_UNABLE_TO_PROCESS -> "unable_to_process"
320
+ else -> "unknown"
393
321
  }
322
+ }
394
323
 
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
324
+ private fun isBiometricUnavailable(code: Int): Boolean {
325
+ return when (code) {
326
+ BiometricPrompt.ERROR_HW_NOT_PRESENT,
327
+ BiometricPrompt.ERROR_HW_UNAVAILABLE,
328
+ BiometricPrompt.ERROR_NO_BIOMETRICS,
329
+ BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
330
+ BiometricPrompt.ERROR_NO_SPACE -> true
331
+
332
+ else -> false
333
+ }
334
+ }
335
+
336
+ private fun canAuthenticateUsingWeakBiometrics(): Int =
337
+ biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
338
+
339
+ private fun createResponse(
340
+ error: String? = null,
341
+ warning: String? = null
342
+ ) = Bundle().apply {
343
+ putBoolean("success", error == null)
344
+ error?.let {
345
+ putString("error", it)
401
346
  }
347
+ warning?.let {
348
+ putString("warning", it)
349
+ }
350
+ }
351
+ }
352
+
353
+ fun <T> MutableSet<T>.addIf(condition: Boolean, valueToAdd: T) {
354
+ if (condition) {
355
+ add(valueToAdd)
356
+ }
402
357
  }
@@ -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.7.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": "da25937e2a99661cbe5eb60ca1d8d6245fc96a50"
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
- }