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 +16 -0
- package/android/build.gradle +56 -35
- package/android/src/main/java/expo/modules/localauthentication/LocalAuthenticationModule.kt +251 -296
- package/expo-module.config.json +3 -0
- package/package.json +2 -2
- package/android/src/main/java/expo/modules/localauthentication/LocalAuthenticationPackage.kt +0 -11
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._
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
6
|
+
version = '13.7.0'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
repositories {
|
|
53
|
+
maven {
|
|
54
|
+
url = mavenLocal().url
|
|
55
|
+
}
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
android {
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
lintOptions {
|
|
78
|
+
abortOnError false
|
|
79
|
+
}
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
+
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
|
|
184
|
+
val disableDeviceFallback = options.disableDeviceFallback
|
|
90
185
|
|
|
91
186
|
// Don't run the device credentials fallback if it's disabled.
|
|
92
|
-
if (disableDeviceFallback
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
}
|
|
223
|
+
val promptMessage = options.promptMessage
|
|
224
|
+
val cancelLabel = options.cancelLabel
|
|
225
|
+
val disableDeviceFallback = options.disableDeviceFallback
|
|
226
|
+
val requireConfirmation = options.requireConfirmation
|
|
150
227
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
258
|
+
createResponse(
|
|
259
|
+
error = "not_available",
|
|
260
|
+
warning = "getCurrentActivity() returned null"
|
|
261
|
+
)
|
|
212
262
|
)
|
|
213
263
|
return
|
|
214
264
|
}
|
|
215
265
|
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
)
|
|
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
|
-
|
|
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() =
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
}
|
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.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": "
|
|
49
|
+
"gitHead": "da25937e2a99661cbe5eb60ca1d8d6245fc96a50"
|
|
50
50
|
}
|
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
|
-
}
|