expo-local-authentication 13.4.1 → 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 +14 -0
- package/android/build.gradle +11 -8
- package/android/src/main/java/expo/modules/localauthentication/LocalAuthenticationModule.kt +255 -284
- package/expo-module.config.json +3 -0
- package/package.json +2 -2
- package/tsconfig.json +1 -1
- package/android/src/main/java/expo/modules/localauthentication/LocalAuthenticationPackage.kt +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@
|
|
|
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
|
+
|
|
23
|
+
## 13.5.0 — 2023-08-02
|
|
24
|
+
|
|
25
|
+
_This version does not introduce any user-facing changes._
|
|
26
|
+
|
|
13
27
|
## 13.4.1 — 2023-06-13
|
|
14
28
|
|
|
15
29
|
### 🐛 Bug fixes
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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.
|
|
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.
|
|
15
|
-
import expo.modules.
|
|
16
|
-
import expo.modules.
|
|
17
|
-
import expo.modules.
|
|
18
|
-
import expo.modules.
|
|
19
|
-
import expo.modules.
|
|
20
|
-
import expo.modules.
|
|
21
|
-
import expo.modules.
|
|
22
|
-
import
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
34
|
+
class AuthOptions : Record {
|
|
35
|
+
@Field
|
|
36
|
+
val promptMessage: String = ""
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
188
|
+
val disableDeviceFallback = options.disableDeviceFallback
|
|
90
189
|
|
|
91
190
|
// Don't run the device credentials fallback if it's disabled.
|
|
92
|
-
if (disableDeviceFallback
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
262
|
+
createResponse(
|
|
263
|
+
error = "not_available",
|
|
264
|
+
warning = "getCurrentActivity() returned null"
|
|
265
|
+
)
|
|
212
266
|
)
|
|
213
267
|
return
|
|
214
268
|
}
|
|
215
269
|
|
|
216
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
promptMessage
|
|
323
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
promise
|
|
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
|
-
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
}
|
package/expo-module.config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-local-authentication",
|
|
3
|
-
"version": "13.
|
|
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": "
|
|
49
|
+
"gitHead": "79607a7325f47aa17c36d266100d09a4ff2cc544"
|
|
50
50
|
}
|
package/tsconfig.json
CHANGED
package/android/src/main/java/expo/modules/localauthentication/LocalAuthenticationPackage.kt
DELETED
|
@@ -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
|
-
}
|