expo-local-authentication
Advanced tools
| // Copyright 2015-present 650 Industries. All rights reserved. | ||
| package expo.modules.localauthentication | ||
| import android.app.Activity | ||
| import android.app.KeyguardManager | ||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.os.Build | ||
| import android.os.Bundle | ||
| import androidx.biometric.BiometricManager | ||
| import androidx.biometric.BiometricPrompt | ||
| import androidx.biometric.BiometricPrompt.PromptInfo | ||
| import androidx.fragment.app.FragmentActivity | ||
| import expo.modules.core.ExportedModule | ||
| import expo.modules.core.ModuleRegistry | ||
| import expo.modules.core.ModuleRegistryDelegate | ||
| import expo.modules.core.Promise | ||
| import expo.modules.core.interfaces.ActivityEventListener | ||
| import expo.modules.core.interfaces.ActivityProvider | ||
| import expo.modules.core.interfaces.ExpoMethod | ||
| import expo.modules.core.interfaces.services.UIManager | ||
| import java.util.* | ||
| import java.util.concurrent.Executor | ||
| import java.util.concurrent.Executors | ||
| class LocalAuthenticationModule(context: Context) : ExportedModule(context), ActivityEventListener { | ||
| private val AUTHENTICATION_TYPE_FINGERPRINT = 1 | ||
| private val AUTHENTICATION_TYPE_FACIAL_RECOGNITION = 2 | ||
| private val AUTHENTICATION_TYPE_IRIS = 3 | ||
| private val SECURITY_LEVEL_NONE = 0 | ||
| private val SECURITY_LEVEL_SECRET = 1 | ||
| private val SECURITY_LEVEL_BIOMETRIC = 2 | ||
| private val biometricManager = BiometricManager.from(context) | ||
| private val packageManager = context.packageManager | ||
| private var biometricPrompt: BiometricPrompt? = null | ||
| private var promise: Promise? = null | ||
| private var isAuthenticating = false | ||
| private val moduleRegistryDelegate: ModuleRegistryDelegate = ModuleRegistryDelegate() | ||
| private val uIManager: UIManager by moduleRegistry() | ||
| private inline fun <reified T> moduleRegistry() = moduleRegistryDelegate.getFromModuleRegistry<T>() | ||
| private fun convertErrorCode(code: Int): String { | ||
| return when (code) { | ||
| BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_NEGATIVE_BUTTON, BiometricPrompt.ERROR_USER_CANCELED -> "user_cancel" | ||
| BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE, BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> "not_available" | ||
| BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> "lockout" | ||
| BiometricPrompt.ERROR_NO_SPACE -> "no_space" | ||
| BiometricPrompt.ERROR_TIMEOUT -> "timeout" | ||
| BiometricPrompt.ERROR_UNABLE_TO_PROCESS -> "unable_to_process" | ||
| else -> "unknown" | ||
| } | ||
| } | ||
| private val authenticationCallback: BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { | ||
| override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { | ||
| isAuthenticating = false | ||
| biometricPrompt = null | ||
| promise?.resolve( | ||
| Bundle().apply { | ||
| putBoolean("success", true) | ||
| } | ||
| ) | ||
| promise = null | ||
| } | ||
| override fun onAuthenticationError(errMsgId: Int, errString: CharSequence) { | ||
| isAuthenticating = false | ||
| biometricPrompt = null | ||
| promise?.resolve( | ||
| Bundle().apply { | ||
| putBoolean("success", false) | ||
| putString("error", convertErrorCode(errMsgId)) | ||
| putString("warning", errString.toString()) | ||
| } | ||
| ) | ||
| promise = null | ||
| } | ||
| } | ||
| override fun getName(): String { | ||
| return "ExpoLocalAuthentication" | ||
| } | ||
| override fun onCreate(moduleRegistry: ModuleRegistry) { | ||
| moduleRegistryDelegate.onCreate(moduleRegistry) | ||
| uIManager.registerActivityEventListener(this) | ||
| } | ||
| @ExpoMethod | ||
| fun supportedAuthenticationTypesAsync(promise: Promise) { | ||
| val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) | ||
| val results: MutableList<Int> = ArrayList() | ||
| if (result == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { | ||
| promise.resolve(results) | ||
| return | ||
| } | ||
| // note(cedric): replace hardcoded system feature strings with constants from | ||
| // PackageManager when dropping support for Android SDK 28 | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||
| if (packageManager.hasSystemFeature("android.hardware.fingerprint")) { | ||
| results.add(AUTHENTICATION_TYPE_FINGERPRINT) | ||
| } | ||
| } | ||
| if (Build.VERSION.SDK_INT >= 29) { | ||
| if (packageManager.hasSystemFeature("android.hardware.biometrics.face")) { | ||
| results.add(AUTHENTICATION_TYPE_FACIAL_RECOGNITION) | ||
| } | ||
| if (packageManager.hasSystemFeature("android.hardware.biometrics.iris")) { | ||
| results.add(AUTHENTICATION_TYPE_IRIS) | ||
| } | ||
| } | ||
| // check for face recognition support on some samsung devices | ||
| if (packageManager.hasSystemFeature("com.samsung.android.bio.face") && !results.contains(AUTHENTICATION_TYPE_FACIAL_RECOGNITION)) { | ||
| results.add(AUTHENTICATION_TYPE_FACIAL_RECOGNITION) | ||
| } | ||
| promise.resolve(results) | ||
| } | ||
| @ExpoMethod | ||
| fun hasHardwareAsync(promise: Promise) { | ||
| val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) | ||
| promise.resolve(result != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) | ||
| } | ||
| @ExpoMethod | ||
| fun isEnrolledAsync(promise: Promise) { | ||
| val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) | ||
| promise.resolve(result == BiometricManager.BIOMETRIC_SUCCESS) | ||
| } | ||
| @ExpoMethod | ||
| fun getEnrolledLevelAsync(promise: Promise) { | ||
| var level = SECURITY_LEVEL_NONE | ||
| if (isDeviceSecure) { | ||
| level = SECURITY_LEVEL_SECRET | ||
| } | ||
| val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) | ||
| if (result == BiometricManager.BIOMETRIC_SUCCESS) { | ||
| level = SECURITY_LEVEL_BIOMETRIC | ||
| } | ||
| promise.resolve(level) | ||
| } | ||
| @ExpoMethod | ||
| fun authenticateAsync(options: Map<String?, Any?>, promise: Promise) { | ||
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { | ||
| promise.reject("E_NOT_SUPPORTED", "Cannot display biometric prompt on android versions below 6.0") | ||
| return | ||
| } | ||
| if (currentActivity == null) { | ||
| promise.reject("E_NOT_FOREGROUND", "Cannot display biometric prompt when the app is not in the foreground") | ||
| return | ||
| } | ||
| if (!keyguardManager.isDeviceSecure) { | ||
| promise.resolve( | ||
| Bundle().apply { | ||
| putBoolean("success", false) | ||
| putString("error", "not_enrolled") | ||
| putString("warning", "KeyguardManager#isDeviceSecure() returned false") | ||
| } | ||
| ) | ||
| return | ||
| } | ||
| val fragmentActivity = currentActivity as FragmentActivity? | ||
| if (fragmentActivity == null) { | ||
| promise.resolve( | ||
| Bundle().apply { | ||
| putBoolean("success", false) | ||
| putString("error", "not_available") | ||
| putString("warning", "getCurrentActivity() returned null") | ||
| } | ||
| ) | ||
| return | ||
| } | ||
| // BiometricPrompt callbacks are invoked on the main thread so also run this there to avoid | ||
| // having to do locking. | ||
| uIManager.runOnUiQueueThread( | ||
| Runnable { | ||
| if (isAuthenticating) { | ||
| this.promise?.resolve( | ||
| Bundle().apply { | ||
| putBoolean("success", false) | ||
| putString("error", "app_cancel") | ||
| } | ||
| ) | ||
| this.promise = promise | ||
| return@Runnable | ||
| } | ||
| val promptMessage = if (options.containsKey("promptMessage")) { | ||
| options["promptMessage"] as String? | ||
| } else { | ||
| "" | ||
| } | ||
| val cancelLabel = if (options.containsKey("cancelLabel")) { | ||
| options["cancelLabel"] as String? | ||
| } else { | ||
| "" | ||
| } | ||
| val disableDeviceFallback = if (options.containsKey("disableDeviceFallback")) { | ||
| options["disableDeviceFallback"] as Boolean? | ||
| } else { | ||
| false | ||
| } | ||
| isAuthenticating = true | ||
| this.promise = promise | ||
| val executor: Executor = Executors.newSingleThreadExecutor() | ||
| biometricPrompt = BiometricPrompt(fragmentActivity, executor, authenticationCallback) | ||
| val promptInfoBuilder = PromptInfo.Builder() | ||
| promptMessage?.let { | ||
| promptInfoBuilder.setTitle(it) | ||
| } | ||
| if (disableDeviceFallback == true) { | ||
| cancelLabel?.let { | ||
| promptInfoBuilder.setNegativeButtonText(it) | ||
| } | ||
| } else { | ||
| promptInfoBuilder.setAllowedAuthenticators( | ||
| BiometricManager.Authenticators.BIOMETRIC_WEAK | ||
| or BiometricManager.Authenticators.DEVICE_CREDENTIAL | ||
| ) | ||
| } | ||
| val promptInfo = promptInfoBuilder.build() | ||
| try { | ||
| biometricPrompt!!.authenticate(promptInfo) | ||
| } catch (ex: NullPointerException) { | ||
| promise.reject("E_INTERNAL_ERRROR", "Canceled authentication due to an internal error") | ||
| } | ||
| } | ||
| ) | ||
| } | ||
| @ExpoMethod | ||
| fun cancelAuthenticate(promise: Promise) { | ||
| uIManager.runOnUiQueueThread { | ||
| biometricPrompt?.cancelAuthentication() | ||
| isAuthenticating = false | ||
| promise.resolve(null) | ||
| } | ||
| } | ||
| override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) { | ||
| // If the user uses PIN as an authentication method, the result will be passed to the `onActivityResult`. | ||
| // Unfortunately, react-native doesn't pass this value to the underlying fragment - we won't resolve the promise. | ||
| // So we need to do it manually. | ||
| if (activity is FragmentActivity) { | ||
| val fragment = activity.supportFragmentManager.findFragmentByTag("androidx.biometric.BiometricFragment") | ||
| fragment?.onActivityResult(requestCode and 0xffff, resultCode, data) | ||
| } | ||
| } | ||
| override fun onNewIntent(intent: Intent) = Unit | ||
| // NOTE: `KeyguardManager#isKeyguardSecure()` considers SIM locked state, | ||
| // but it will be ignored on falling-back to device credential on biometric authentication. | ||
| // That means, setting level to `SECURITY_LEVEL_SECRET` might be misleading for some users. | ||
| // But there is no equivalent APIs prior to M. | ||
| // `andriodx.biometric.BiometricManager#canAuthenticate(int)` looks like an alternative, | ||
| // but specifying `BiometricManager.Authenticators.DEVICE_CREDENTIAL` alone is not | ||
| // supported prior to API 30. | ||
| // https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int) | ||
| private val isDeviceSecure: Boolean | ||
| get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||
| keyguardManager.isDeviceSecure | ||
| } else { | ||
| // NOTE: `KeyguardManager#isKeyguardSecure()` considers SIM locked state, | ||
| // but it will be ignored on falling-back to device credential on biometric authentication. | ||
| // That means, setting level to `SECURITY_LEVEL_SECRET` might be misleading for some users. | ||
| // But there is no equivalent APIs prior to M. | ||
| // `andriodx.biometric.BiometricManager#canAuthenticate(int)` looks like an alternative, | ||
| // but specifying `BiometricManager.Authenticators.DEVICE_CREDENTIAL` alone is not | ||
| // supported prior to API 30. | ||
| // https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int) | ||
| keyguardManager.isKeyguardSecure | ||
| } | ||
| private val keyguardManager: KeyguardManager | ||
| get() = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager | ||
| private val currentActivity: Activity? | ||
| get() { | ||
| val activityProvider: ActivityProvider by moduleRegistry() | ||
| return activityProvider.currentActivity | ||
| } | ||
| } |
| package expo.modules.localauthentication | ||
| import android.content.Context | ||
| import expo.modules.core.BasePackage | ||
| import expo.modules.core.ExportedModule | ||
| class LocalAuthenticationPackage : BasePackage() { | ||
| override fun createExportedModules(context: Context): List<ExportedModule> { | ||
| return listOf<ExportedModule>(LocalAuthenticationModule(context)) | ||
| } | ||
| } |
+7
-11
@@ -6,3 +6,3 @@ apply plugin: 'com.android.library' | ||
| group = 'host.exp.exponent' | ||
| version = '11.1.1' | ||
| version = '12.0.0' | ||
@@ -57,2 +57,6 @@ buildscript { | ||
| kotlinOptions { | ||
| jvmTarget = JavaVersion.VERSION_1_8 | ||
| } | ||
| defaultConfig { | ||
@@ -62,3 +66,3 @@ minSdkVersion safeExtGet("minSdkVersion", 21) | ||
| versionCode 30 | ||
| versionName "11.1.1" | ||
| versionName "12.0.0" | ||
| } | ||
@@ -70,12 +74,4 @@ lintOptions { | ||
| if (new File(rootProject.projectDir.parentFile, 'package.json').exists()) { | ||
| apply from: project(":unimodules-core").file("../unimodules-core.gradle") | ||
| } else { | ||
| throw new GradleException( | ||
| "'unimodules-core.gradle' was not found in the usual React Native dependency location. " + | ||
| "This package can only be used in such projects. Are you sure you've installed the dependencies properly?") | ||
| } | ||
| dependencies { | ||
| unimodule "unimodules-core" | ||
| implementation project(':expo-modules-core') | ||
@@ -82,0 +78,0 @@ api "androidx.biometric:biometric:1.1.0" |
@@ -1,2 +0,2 @@ | ||
| declare const _default: import("@unimodules/core").ProxyNativeModule; | ||
| declare const _default: import("expo-modules-core").ProxyNativeModule; | ||
| export default _default; |
@@ -1,3 +0,3 @@ | ||
| import { NativeModulesProxy } from '@unimodules/core'; | ||
| import { NativeModulesProxy } from 'expo-modules-core'; | ||
| export default NativeModulesProxy.ExpoLocalAuthentication; | ||
| //# sourceMappingURL=ExpoLocalAuthentication.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"ExpoLocalAuthentication.js","sourceRoot":"","sources":["../src/ExpoLocalAuthentication.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,eAAe,kBAAkB,CAAC,uBAAuB,CAAC","sourcesContent":["import { NativeModulesProxy } from '@unimodules/core';\n\nexport default NativeModulesProxy.ExpoLocalAuthentication;\n"]} | ||
| {"version":3,"file":"ExpoLocalAuthentication.js","sourceRoot":"","sources":["../src/ExpoLocalAuthentication.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,eAAe,kBAAkB,CAAC,uBAAuB,CAAC","sourcesContent":["import { NativeModulesProxy } from 'expo-modules-core';\n\nexport default NativeModulesProxy.ExpoLocalAuthentication;\n"]} |
| import { LocalAuthenticationOptions, AuthenticationType, LocalAuthenticationResult, SecurityLevel } from './LocalAuthentication.types'; | ||
| export { LocalAuthenticationOptions, AuthenticationType, LocalAuthenticationResult, SecurityLevel }; | ||
| /** | ||
| * Determine whether a face or fingerprint scanner is available on the device. | ||
| * @return Returns a promise which fulfils with a `boolean` value indicating whether a face or | ||
| * fingerprint scanner is available on this device. | ||
| */ | ||
| export declare function hasHardwareAsync(): Promise<boolean>; | ||
| /** | ||
| * Determine what kinds of authentications are available on the device. | ||
| * @return Returns a promise which fulfils to an array containing [`AuthenticationType`s](#authenticationtype). | ||
| * | ||
| * Devices can support multiple authentication methods- i.e. `[1,2]` means the device supports both | ||
| * fingerprint and facial recognition. If none are supported, this method returns an empty array. | ||
| */ | ||
| export declare function supportedAuthenticationTypesAsync(): Promise<AuthenticationType[]>; | ||
| /** | ||
| * Determine whether the device has saved fingerprints or facial data to use for authentication. | ||
| * @return Returns a promise which fulfils to `boolean` value indicating whether the device has | ||
| * saved fingerprints or facial data for authentication. | ||
| */ | ||
| export declare function isEnrolledAsync(): Promise<boolean>; | ||
| /** | ||
| * Determine what kind of authentication is enrolled on the device. | ||
| * @return Returns a promise which fulfils with [`SecurityLevel`](#securitylevel). | ||
| * > **Note:** On Android devices prior to M, `SECRET` can be returned if only the SIM lock has been | ||
| * enrolled, which is not the method that [`authenticateAsync`](#localauthenticationauthenticateasyncoptions) | ||
| * prompts. | ||
| */ | ||
| export declare function getEnrolledLevelAsync(): Promise<SecurityLevel>; | ||
| /** | ||
| * Attempts to authenticate via Fingerprint/TouchID (or FaceID if available on the device). | ||
| * > **Note:** Apple requires apps which use FaceID to provide a description of why they use this API. | ||
| * If you try to use FaceID on an iPhone with FaceID without providing `infoPlist.NSFaceIDUsageDescription` | ||
| * in `app.json`, the module will authenticate using device passcode. For more information about | ||
| * usage descriptions on iOS, see [Deploying to App Stores](/distribution/app-stores#system-permissions-dialogs-on-ios). | ||
| * @param options | ||
| * @return Returns a promise which fulfils with [`LocalAuthenticationResult`](#localauthenticationresult). | ||
| */ | ||
| export declare function authenticateAsync(options?: LocalAuthenticationOptions): Promise<LocalAuthenticationResult>; | ||
| /** | ||
| * **(Android Only)** Cancels authentication flow. | ||
| */ | ||
| export declare function cancelAuthenticate(): Promise<void>; |
@@ -1,2 +0,2 @@ | ||
| import { UnavailabilityError } from '@unimodules/core'; | ||
| import { UnavailabilityError } from 'expo-modules-core'; | ||
| import invariant from 'invariant'; | ||
@@ -6,2 +6,8 @@ import ExpoLocalAuthentication from './ExpoLocalAuthentication'; | ||
| export { AuthenticationType, SecurityLevel }; | ||
| // @needsAudit | ||
| /** | ||
| * Determine whether a face or fingerprint scanner is available on the device. | ||
| * @return Returns a promise which fulfils with a `boolean` value indicating whether a face or | ||
| * fingerprint scanner is available on this device. | ||
| */ | ||
| export async function hasHardwareAsync() { | ||
@@ -13,2 +19,10 @@ if (!ExpoLocalAuthentication.hasHardwareAsync) { | ||
| } | ||
| // @needsAudit | ||
| /** | ||
| * Determine what kinds of authentications are available on the device. | ||
| * @return Returns a promise which fulfils to an array containing [`AuthenticationType`s](#authenticationtype). | ||
| * | ||
| * Devices can support multiple authentication methods- i.e. `[1,2]` means the device supports both | ||
| * fingerprint and facial recognition. If none are supported, this method returns an empty array. | ||
| */ | ||
| export async function supportedAuthenticationTypesAsync() { | ||
@@ -20,2 +34,8 @@ if (!ExpoLocalAuthentication.supportedAuthenticationTypesAsync) { | ||
| } | ||
| // @needsAudit | ||
| /** | ||
| * Determine whether the device has saved fingerprints or facial data to use for authentication. | ||
| * @return Returns a promise which fulfils to `boolean` value indicating whether the device has | ||
| * saved fingerprints or facial data for authentication. | ||
| */ | ||
| export async function isEnrolledAsync() { | ||
@@ -27,2 +47,10 @@ if (!ExpoLocalAuthentication.isEnrolledAsync) { | ||
| } | ||
| // @needsAudit | ||
| /** | ||
| * Determine what kind of authentication is enrolled on the device. | ||
| * @return Returns a promise which fulfils with [`SecurityLevel`](#securitylevel). | ||
| * > **Note:** On Android devices prior to M, `SECRET` can be returned if only the SIM lock has been | ||
| * enrolled, which is not the method that [`authenticateAsync`](#localauthenticationauthenticateasyncoptions) | ||
| * prompts. | ||
| */ | ||
| export async function getEnrolledLevelAsync() { | ||
@@ -34,2 +62,12 @@ if (!ExpoLocalAuthentication.getEnrolledLevelAsync) { | ||
| } | ||
| // @needsAudit | ||
| /** | ||
| * Attempts to authenticate via Fingerprint/TouchID (or FaceID if available on the device). | ||
| * > **Note:** Apple requires apps which use FaceID to provide a description of why they use this API. | ||
| * If you try to use FaceID on an iPhone with FaceID without providing `infoPlist.NSFaceIDUsageDescription` | ||
| * in `app.json`, the module will authenticate using device passcode. For more information about | ||
| * usage descriptions on iOS, see [Deploying to App Stores](/distribution/app-stores#system-permissions-dialogs-on-ios). | ||
| * @param options | ||
| * @return Returns a promise which fulfils with [`LocalAuthenticationResult`](#localauthenticationresult). | ||
| */ | ||
| export async function authenticateAsync(options = {}) { | ||
@@ -49,2 +87,6 @@ if (!ExpoLocalAuthentication.authenticateAsync) { | ||
| } | ||
| // @needsAudit | ||
| /** | ||
| * **(Android Only)** Cancels authentication flow. | ||
| */ | ||
| export async function cancelAuthenticate() { | ||
@@ -51,0 +93,0 @@ if (!ExpoLocalAuthentication.cancelAuthenticate) { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"LocalAuthentication.js","sourceRoot":"","sources":["../src/LocalAuthentication.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC,OAAO,uBAAuB,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAEL,kBAAkB,EAElB,aAAa,GACd,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAA8B,kBAAkB,EAA6B,aAAa,EAAE,CAAC;AAEpG,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,CAAC,uBAAuB,CAAC,gBAAgB,EAAE;QAC7C,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,kBAAkB,CAAC,CAAC;KAChF;IACD,OAAO,MAAM,uBAAuB,CAAC,gBAAgB,EAAE,CAAC;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iCAAiC;IACrD,IAAI,CAAC,uBAAuB,CAAC,iCAAiC,EAAE;QAC9D,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,mCAAmC,CAAC,CAAC;KACjG;IACD,OAAO,MAAM,uBAAuB,CAAC,iCAAiC,EAAE,CAAC;AAC3E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,IAAI,CAAC,uBAAuB,CAAC,eAAe,EAAE;QAC5C,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,iBAAiB,CAAC,CAAC;KAC/E;IACD,OAAO,MAAM,uBAAuB,CAAC,eAAe,EAAE,CAAC;AACzD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,IAAI,CAAC,uBAAuB,CAAC,qBAAqB,EAAE;QAClD,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,uBAAuB,CAAC,CAAC;KACrF;IACD,OAAO,MAAM,uBAAuB,CAAC,qBAAqB,EAAE,CAAC;AAC/D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,UAAsC,EAAE;IAExC,IAAI,CAAC,uBAAuB,CAAC,iBAAiB,EAAE;QAC9C,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,mBAAmB,CAAC,CAAC;KACjF;IAED,IAAI,OAAO,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE;QAC3C,SAAS,CACP,OAAO,OAAO,CAAC,aAAa,KAAK,QAAQ,IAAI,OAAO,CAAC,aAAa,CAAC,MAAM,EACzE,6FAA6F,CAC9F,CAAC;KACH;IAED,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,cAAc,CAAC;IAC9D,MAAM,MAAM,GAAG,MAAM,uBAAuB,CAAC,iBAAiB,CAAC,EAAE,GAAG,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;IAE9F,IAAI,MAAM,CAAC,OAAO,EAAE;QAClB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;KAC9B;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,IAAI,CAAC,uBAAuB,CAAC,kBAAkB,EAAE;QAC/C,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,oBAAoB,CAAC,CAAC;KAClF;IACD,MAAM,uBAAuB,CAAC,kBAAkB,EAAE,CAAC;AACrD,CAAC","sourcesContent":["import { UnavailabilityError } from '@unimodules/core';\nimport invariant from 'invariant';\n\nimport ExpoLocalAuthentication from './ExpoLocalAuthentication';\nimport {\n LocalAuthenticationOptions,\n AuthenticationType,\n LocalAuthenticationResult,\n SecurityLevel,\n} from './LocalAuthentication.types';\n\nexport { LocalAuthenticationOptions, AuthenticationType, LocalAuthenticationResult, SecurityLevel };\n\nexport async function hasHardwareAsync(): Promise<boolean> {\n if (!ExpoLocalAuthentication.hasHardwareAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'hasHardwareAsync');\n }\n return await ExpoLocalAuthentication.hasHardwareAsync();\n}\n\nexport async function supportedAuthenticationTypesAsync(): Promise<AuthenticationType[]> {\n if (!ExpoLocalAuthentication.supportedAuthenticationTypesAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'supportedAuthenticationTypesAsync');\n }\n return await ExpoLocalAuthentication.supportedAuthenticationTypesAsync();\n}\n\nexport async function isEnrolledAsync(): Promise<boolean> {\n if (!ExpoLocalAuthentication.isEnrolledAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'isEnrolledAsync');\n }\n return await ExpoLocalAuthentication.isEnrolledAsync();\n}\n\nexport async function getEnrolledLevelAsync(): Promise<SecurityLevel> {\n if (!ExpoLocalAuthentication.getEnrolledLevelAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'getEnrolledLevelAsync');\n }\n return await ExpoLocalAuthentication.getEnrolledLevelAsync();\n}\n\nexport async function authenticateAsync(\n options: LocalAuthenticationOptions = {}\n): Promise<LocalAuthenticationResult> {\n if (!ExpoLocalAuthentication.authenticateAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'authenticateAsync');\n }\n\n if (options.hasOwnProperty('promptMessage')) {\n invariant(\n typeof options.promptMessage === 'string' && options.promptMessage.length,\n 'LocalAuthentication.authenticateAsync : `options.promptMessage` must be a non-empty string.'\n );\n }\n\n const promptMessage = options.promptMessage || 'Authenticate';\n const result = await ExpoLocalAuthentication.authenticateAsync({ ...options, promptMessage });\n\n if (result.warning) {\n console.warn(result.warning);\n }\n return result;\n}\n\nexport async function cancelAuthenticate(): Promise<void> {\n if (!ExpoLocalAuthentication.cancelAuthenticate) {\n throw new UnavailabilityError('expo-local-authentication', 'cancelAuthenticate');\n }\n await ExpoLocalAuthentication.cancelAuthenticate();\n}\n"]} | ||
| {"version":3,"file":"LocalAuthentication.js","sourceRoot":"","sources":["../src/LocalAuthentication.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC,OAAO,uBAAuB,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAEL,kBAAkB,EAElB,aAAa,GACd,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAA8B,kBAAkB,EAA6B,aAAa,EAAE,CAAC;AAEpG,cAAc;AACd;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,CAAC,uBAAuB,CAAC,gBAAgB,EAAE;QAC7C,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,kBAAkB,CAAC,CAAC;KAChF;IACD,OAAO,MAAM,uBAAuB,CAAC,gBAAgB,EAAE,CAAC;AAC1D,CAAC;AAED,cAAc;AACd;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iCAAiC;IACrD,IAAI,CAAC,uBAAuB,CAAC,iCAAiC,EAAE;QAC9D,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,mCAAmC,CAAC,CAAC;KACjG;IACD,OAAO,MAAM,uBAAuB,CAAC,iCAAiC,EAAE,CAAC;AAC3E,CAAC;AAED,cAAc;AACd;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,IAAI,CAAC,uBAAuB,CAAC,eAAe,EAAE;QAC5C,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,iBAAiB,CAAC,CAAC;KAC/E;IACD,OAAO,MAAM,uBAAuB,CAAC,eAAe,EAAE,CAAC;AACzD,CAAC;AAED,cAAc;AACd;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,IAAI,CAAC,uBAAuB,CAAC,qBAAqB,EAAE;QAClD,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,uBAAuB,CAAC,CAAC;KACrF;IACD,OAAO,MAAM,uBAAuB,CAAC,qBAAqB,EAAE,CAAC;AAC/D,CAAC;AAED,cAAc;AACd;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,UAAsC,EAAE;IAExC,IAAI,CAAC,uBAAuB,CAAC,iBAAiB,EAAE;QAC9C,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,mBAAmB,CAAC,CAAC;KACjF;IAED,IAAI,OAAO,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE;QAC3C,SAAS,CACP,OAAO,OAAO,CAAC,aAAa,KAAK,QAAQ,IAAI,OAAO,CAAC,aAAa,CAAC,MAAM,EACzE,6FAA6F,CAC9F,CAAC;KACH;IAED,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,cAAc,CAAC;IAC9D,MAAM,MAAM,GAAG,MAAM,uBAAuB,CAAC,iBAAiB,CAAC,EAAE,GAAG,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;IAE9F,IAAI,MAAM,CAAC,OAAO,EAAE;QAClB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;KAC9B;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,cAAc;AACd;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,IAAI,CAAC,uBAAuB,CAAC,kBAAkB,EAAE;QAC/C,MAAM,IAAI,mBAAmB,CAAC,2BAA2B,EAAE,oBAAoB,CAAC,CAAC;KAClF;IACD,MAAM,uBAAuB,CAAC,kBAAkB,EAAE,CAAC;AACrD,CAAC","sourcesContent":["import { UnavailabilityError } from 'expo-modules-core';\nimport invariant from 'invariant';\n\nimport ExpoLocalAuthentication from './ExpoLocalAuthentication';\nimport {\n LocalAuthenticationOptions,\n AuthenticationType,\n LocalAuthenticationResult,\n SecurityLevel,\n} from './LocalAuthentication.types';\n\nexport { LocalAuthenticationOptions, AuthenticationType, LocalAuthenticationResult, SecurityLevel };\n\n// @needsAudit\n/**\n * Determine whether a face or fingerprint scanner is available on the device.\n * @return Returns a promise which fulfils with a `boolean` value indicating whether a face or\n * fingerprint scanner is available on this device.\n */\nexport async function hasHardwareAsync(): Promise<boolean> {\n if (!ExpoLocalAuthentication.hasHardwareAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'hasHardwareAsync');\n }\n return await ExpoLocalAuthentication.hasHardwareAsync();\n}\n\n// @needsAudit\n/**\n * Determine what kinds of authentications are available on the device.\n * @return Returns a promise which fulfils to an array containing [`AuthenticationType`s](#authenticationtype).\n *\n * Devices can support multiple authentication methods- i.e. `[1,2]` means the device supports both\n * fingerprint and facial recognition. If none are supported, this method returns an empty array.\n */\nexport async function supportedAuthenticationTypesAsync(): Promise<AuthenticationType[]> {\n if (!ExpoLocalAuthentication.supportedAuthenticationTypesAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'supportedAuthenticationTypesAsync');\n }\n return await ExpoLocalAuthentication.supportedAuthenticationTypesAsync();\n}\n\n// @needsAudit\n/**\n * Determine whether the device has saved fingerprints or facial data to use for authentication.\n * @return Returns a promise which fulfils to `boolean` value indicating whether the device has\n * saved fingerprints or facial data for authentication.\n */\nexport async function isEnrolledAsync(): Promise<boolean> {\n if (!ExpoLocalAuthentication.isEnrolledAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'isEnrolledAsync');\n }\n return await ExpoLocalAuthentication.isEnrolledAsync();\n}\n\n// @needsAudit\n/**\n * Determine what kind of authentication is enrolled on the device.\n * @return Returns a promise which fulfils with [`SecurityLevel`](#securitylevel).\n * > **Note:** On Android devices prior to M, `SECRET` can be returned if only the SIM lock has been\n * enrolled, which is not the method that [`authenticateAsync`](#localauthenticationauthenticateasyncoptions)\n * prompts.\n */\nexport async function getEnrolledLevelAsync(): Promise<SecurityLevel> {\n if (!ExpoLocalAuthentication.getEnrolledLevelAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'getEnrolledLevelAsync');\n }\n return await ExpoLocalAuthentication.getEnrolledLevelAsync();\n}\n\n// @needsAudit\n/**\n * Attempts to authenticate via Fingerprint/TouchID (or FaceID if available on the device).\n * > **Note:** Apple requires apps which use FaceID to provide a description of why they use this API.\n * If you try to use FaceID on an iPhone with FaceID without providing `infoPlist.NSFaceIDUsageDescription`\n * in `app.json`, the module will authenticate using device passcode. For more information about\n * usage descriptions on iOS, see [Deploying to App Stores](/distribution/app-stores#system-permissions-dialogs-on-ios).\n * @param options\n * @return Returns a promise which fulfils with [`LocalAuthenticationResult`](#localauthenticationresult).\n */\nexport async function authenticateAsync(\n options: LocalAuthenticationOptions = {}\n): Promise<LocalAuthenticationResult> {\n if (!ExpoLocalAuthentication.authenticateAsync) {\n throw new UnavailabilityError('expo-local-authentication', 'authenticateAsync');\n }\n\n if (options.hasOwnProperty('promptMessage')) {\n invariant(\n typeof options.promptMessage === 'string' && options.promptMessage.length,\n 'LocalAuthentication.authenticateAsync : `options.promptMessage` must be a non-empty string.'\n );\n }\n\n const promptMessage = options.promptMessage || 'Authenticate';\n const result = await ExpoLocalAuthentication.authenticateAsync({ ...options, promptMessage });\n\n if (result.warning) {\n console.warn(result.warning);\n }\n return result;\n}\n\n// @needsAudit\n/**\n * **(Android Only)** Cancels authentication flow.\n */\nexport async function cancelAuthenticate(): Promise<void> {\n if (!ExpoLocalAuthentication.cancelAuthenticate) {\n throw new UnavailabilityError('expo-local-authentication', 'cancelAuthenticate');\n }\n await ExpoLocalAuthentication.cancelAuthenticate();\n}\n"]} |
@@ -6,18 +6,56 @@ export declare type LocalAuthenticationResult = { | ||
| error: string; | ||
| warning?: string; | ||
| }; | ||
| export declare enum AuthenticationType { | ||
| /** | ||
| * Indicates fingerprint support. | ||
| */ | ||
| FINGERPRINT = 1, | ||
| /** | ||
| * Indicates facial recognition support. | ||
| */ | ||
| FACIAL_RECOGNITION = 2, | ||
| /** | ||
| * __Android-only.__ Indicates iris recognition support. | ||
| */ | ||
| IRIS = 3 | ||
| } | ||
| export declare enum SecurityLevel { | ||
| /** | ||
| * Indicates no enrolled authentication. | ||
| */ | ||
| NONE = 0, | ||
| /** | ||
| * Indicates non-biometric authentication (e.g. PIN, Pattern). | ||
| */ | ||
| SECRET = 1, | ||
| /** | ||
| * Indicates biometric authentication. | ||
| */ | ||
| BIOMETRIC = 2 | ||
| } | ||
| export declare type LocalAuthenticationOptions = { | ||
| /** | ||
| * A message that is shown alongside the TouchID or FaceID prompt. | ||
| */ | ||
| promptMessage?: string; | ||
| /** | ||
| * Allows to customize the default `Cancel` label shown. | ||
| */ | ||
| cancelLabel?: string; | ||
| /** | ||
| * After several failed attempts the system will fallback to the device passcode. This setting | ||
| * allows you to disable this option and instead handle the fallback yourself. This can be | ||
| * preferable in certain custom authentication workflows. This behaviour maps to using the iOS | ||
| * [LAPolicyDeviceOwnerAuthenticationWithBiometrics](https://developer.apple.com/documentation/localauthentication/lapolicy/lapolicydeviceownerauthenticationwithbiometrics?language=objc) | ||
| * policy rather than the [LAPolicyDeviceOwnerAuthentication](https://developer.apple.com/documentation/localauthentication/lapolicy/lapolicydeviceownerauthentication?language=objc) | ||
| * policy. Defaults to `false`. | ||
| */ | ||
| disableDeviceFallback?: boolean; | ||
| /** | ||
| * **iOS only.** Allows to customize the default `Use Passcode` label shown after several failed | ||
| * authentication attempts. Setting this option to an empty string disables this button from | ||
| * showing in the prompt. | ||
| */ | ||
| fallbackLabel?: string; | ||
| }; |
@@ -0,14 +1,33 @@ | ||
| // @needsAudit | ||
| export var AuthenticationType; | ||
| (function (AuthenticationType) { | ||
| /** | ||
| * Indicates fingerprint support. | ||
| */ | ||
| AuthenticationType[AuthenticationType["FINGERPRINT"] = 1] = "FINGERPRINT"; | ||
| /** | ||
| * Indicates facial recognition support. | ||
| */ | ||
| AuthenticationType[AuthenticationType["FACIAL_RECOGNITION"] = 2] = "FACIAL_RECOGNITION"; | ||
| // Android only | ||
| /** | ||
| * __Android-only.__ Indicates iris recognition support. | ||
| */ | ||
| AuthenticationType[AuthenticationType["IRIS"] = 3] = "IRIS"; | ||
| })(AuthenticationType || (AuthenticationType = {})); | ||
| // @needsAudit | ||
| export var SecurityLevel; | ||
| (function (SecurityLevel) { | ||
| /** | ||
| * Indicates no enrolled authentication. | ||
| */ | ||
| SecurityLevel[SecurityLevel["NONE"] = 0] = "NONE"; | ||
| /** | ||
| * Indicates non-biometric authentication (e.g. PIN, Pattern). | ||
| */ | ||
| SecurityLevel[SecurityLevel["SECRET"] = 1] = "SECRET"; | ||
| /** | ||
| * Indicates biometric authentication. | ||
| */ | ||
| SecurityLevel[SecurityLevel["BIOMETRIC"] = 2] = "BIOMETRIC"; | ||
| })(SecurityLevel || (SecurityLevel = {})); | ||
| //# sourceMappingURL=LocalAuthentication.types.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"LocalAuthentication.types.js","sourceRoot":"","sources":["../src/LocalAuthentication.types.ts"],"names":[],"mappings":"AAEA,MAAM,CAAN,IAAY,kBAKX;AALD,WAAY,kBAAkB;IAC5B,yEAAe,CAAA;IACf,uFAAsB,CAAA;IACtB,eAAe;IACf,2DAAQ,CAAA;AACV,CAAC,EALW,kBAAkB,KAAlB,kBAAkB,QAK7B;AAED,MAAM,CAAN,IAAY,aAIX;AAJD,WAAY,aAAa;IACvB,iDAAQ,CAAA;IACR,qDAAU,CAAA;IACV,2DAAa,CAAA;AACf,CAAC,EAJW,aAAa,KAAb,aAAa,QAIxB","sourcesContent":["export type LocalAuthenticationResult = { success: true } | { success: false; error: string };\n\nexport enum AuthenticationType {\n FINGERPRINT = 1,\n FACIAL_RECOGNITION = 2,\n // Android only\n IRIS = 3,\n}\n\nexport enum SecurityLevel {\n NONE = 0,\n SECRET = 1,\n BIOMETRIC = 2,\n}\n\nexport type LocalAuthenticationOptions = {\n promptMessage?: string;\n cancelLabel?: string;\n disableDeviceFallback?: boolean;\n // iOS only\n fallbackLabel?: string;\n};\n"]} | ||
| {"version":3,"file":"LocalAuthentication.types.js","sourceRoot":"","sources":["../src/LocalAuthentication.types.ts"],"names":[],"mappings":"AAIA,cAAc;AACd,MAAM,CAAN,IAAY,kBAaX;AAbD,WAAY,kBAAkB;IAC5B;;OAEG;IACH,yEAAe,CAAA;IACf;;OAEG;IACH,uFAAsB,CAAA;IACtB;;OAEG;IACH,2DAAQ,CAAA;AACV,CAAC,EAbW,kBAAkB,KAAlB,kBAAkB,QAa7B;AAED,cAAc;AACd,MAAM,CAAN,IAAY,aAaX;AAbD,WAAY,aAAa;IACvB;;OAEG;IACH,iDAAQ,CAAA;IACR;;OAEG;IACH,qDAAU,CAAA;IACV;;OAEG;IACH,2DAAa,CAAA;AACf,CAAC,EAbW,aAAa,KAAb,aAAa,QAaxB","sourcesContent":["export type LocalAuthenticationResult =\n | { success: true }\n | { success: false; error: string; warning?: string };\n\n// @needsAudit\nexport enum AuthenticationType {\n /**\n * Indicates fingerprint support.\n */\n FINGERPRINT = 1,\n /**\n * Indicates facial recognition support.\n */\n FACIAL_RECOGNITION = 2,\n /**\n * __Android-only.__ Indicates iris recognition support.\n */\n IRIS = 3,\n}\n\n// @needsAudit\nexport enum SecurityLevel {\n /**\n * Indicates no enrolled authentication.\n */\n NONE = 0,\n /**\n * Indicates non-biometric authentication (e.g. PIN, Pattern).\n */\n SECRET = 1,\n /**\n * Indicates biometric authentication.\n */\n BIOMETRIC = 2,\n}\n\n// @needsAudit\nexport type LocalAuthenticationOptions = {\n /**\n * A message that is shown alongside the TouchID or FaceID prompt.\n */\n promptMessage?: string;\n /**\n * Allows to customize the default `Cancel` label shown.\n */\n cancelLabel?: string;\n /**\n * After several failed attempts the system will fallback to the device passcode. This setting\n * allows you to disable this option and instead handle the fallback yourself. This can be\n * preferable in certain custom authentication workflows. This behaviour maps to using the iOS\n * [LAPolicyDeviceOwnerAuthenticationWithBiometrics](https://developer.apple.com/documentation/localauthentication/lapolicy/lapolicydeviceownerauthenticationwithbiometrics?language=objc)\n * policy rather than the [LAPolicyDeviceOwnerAuthentication](https://developer.apple.com/documentation/localauthentication/lapolicy/lapolicydeviceownerauthentication?language=objc)\n * policy. Defaults to `false`.\n */\n disableDeviceFallback?: boolean;\n /**\n * **iOS only.** Allows to customize the default `Use Passcode` label shown after several failed\n * authentication attempts. Setting this option to an empty string disables this button from\n * showing in the prompt.\n */\n fallbackLabel?: string;\n};\n"]} |
+15
-2
@@ -13,6 +13,19 @@ # Changelog | ||
| ## 11.1.1 — 2021-06-24 | ||
| ## 12.0.0 — 2021-09-28 | ||
| _This version does not introduce any user-facing changes._ | ||
| ### 🛠 Breaking changes | ||
| - Dropped support for iOS 11.0 ([#14383](https://github.com/expo/expo/pull/14383) by [@cruzach](https://github.com/cruzach)) | ||
| ### 🐛 Bug fixes | ||
| - Added missing definition on type LocalAuthenticationResult. ([#13636](https://github.com/expo/expo/pull/13636) by [@mstach60161](https://github.com/mstach60161)) | ||
| - Fixed detection of the available authentication types on some Samsung devices on Android. ([#14300](https://github.com/expo/expo/pull/14300) by [@beaur](https://github.com/beaur)) | ||
| - Fix building errors from use_frameworks! in Podfile. ([#14523](https://github.com/expo/expo/pull/14523) by [@kudo](https://github.com/kudo)) | ||
| ### 💡 Others | ||
| - Rewrite module from Java to Kotlin. ([#13582](https://github.com/expo/expo/pull/13582) by [@mstach60161](https://github.com/mstach60161)) | ||
| - Updated `@expo/config-plugins` ([#14443](https://github.com/expo/expo/pull/14443) by [@EvanBacon](https://github.com/EvanBacon)) | ||
| ## 11.1.0 — 2021-06-16 | ||
@@ -19,0 +32,0 @@ |
| // Copyright 2018-present 650 Industries. All rights reserved. | ||
| #import <UMCore/UMExportedModule.h> | ||
| #import <UMCore/UMModuleRegistryConsumer.h> | ||
| #import <ExpoModulesCore/EXExportedModule.h> | ||
| #import <ExpoModulesCore/EXModuleRegistryConsumer.h> | ||
| @interface EXLocalAuthentication : UMExportedModule | ||
| @interface EXLocalAuthentication : EXExportedModule | ||
| - (void)authenticateWithOptions:(NSDictionary *)options | ||
| resolve:(UMPromiseResolveBlock)resolve | ||
| reject:(UMPromiseRejectBlock)reject; | ||
| resolve:(EXPromiseResolveBlock)resolve | ||
| reject:(EXPromiseRejectBlock)reject; | ||
| - (NSString *)convertErrorCode:(NSError *)error; | ||
@@ -12,0 +12,0 @@ + (BOOL)isTouchIdDevice; |
@@ -5,3 +5,3 @@ // Copyright 2018-present 650 Industries. All rights reserved. | ||
| #import <UMCore/UMUtilities.h> | ||
| #import <ExpoModulesCore/EXUtilities.h> | ||
| #import <EXLocalAuthentication/EXLocalAuthentication.h> | ||
@@ -22,7 +22,7 @@ | ||
| UM_EXPORT_MODULE(ExpoLocalAuthentication) | ||
| EX_EXPORT_MODULE(ExpoLocalAuthentication) | ||
| UM_EXPORT_METHOD_AS(supportedAuthenticationTypesAsync, | ||
| supportedAuthenticationTypesAsync:(UMPromiseResolveBlock)resolve | ||
| reject:(UMPromiseRejectBlock)reject) | ||
| EX_EXPORT_METHOD_AS(supportedAuthenticationTypesAsync, | ||
| supportedAuthenticationTypesAsync:(EXPromiseResolveBlock)resolve | ||
| reject:(EXPromiseRejectBlock)reject) | ||
| { | ||
@@ -39,5 +39,5 @@ NSMutableArray *results = [NSMutableArray array]; | ||
| UM_EXPORT_METHOD_AS(hasHardwareAsync, | ||
| hasHardwareAsync:(UMPromiseResolveBlock)resolve | ||
| reject:(UMPromiseRejectBlock)reject) | ||
| EX_EXPORT_METHOD_AS(hasHardwareAsync, | ||
| hasHardwareAsync:(EXPromiseResolveBlock)resolve | ||
| reject:(EXPromiseRejectBlock)reject) | ||
| { | ||
@@ -48,16 +48,10 @@ LAContext *context = [LAContext new]; | ||
| BOOL isSupported = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]; | ||
| BOOL isAvailable; | ||
| BOOL isAvailable = isSupported || error.code != LAErrorBiometryNotAvailable; | ||
| if (@available(iOS 11.0.1, *)) { | ||
| isAvailable = isSupported || error.code != LAErrorBiometryNotAvailable; | ||
| } else { | ||
| isAvailable = isSupported || error.code != LAErrorTouchIDNotAvailable; | ||
| } | ||
| resolve(@(isAvailable)); | ||
| } | ||
| UM_EXPORT_METHOD_AS(isEnrolledAsync, | ||
| isEnrolledAsync:(UMPromiseResolveBlock)resolve | ||
| reject:(UMPromiseRejectBlock)reject) | ||
| EX_EXPORT_METHOD_AS(isEnrolledAsync, | ||
| isEnrolledAsync:(EXPromiseResolveBlock)resolve | ||
| reject:(EXPromiseRejectBlock)reject) | ||
| { | ||
@@ -73,5 +67,5 @@ LAContext *context = [LAContext new]; | ||
| UM_EXPORT_METHOD_AS(getEnrolledLevelAsync, | ||
| getEnrolledLevelAsync:(UMPromiseResolveBlock)resolve | ||
| reject:(UMPromiseRejectBlock)reject) | ||
| EX_EXPORT_METHOD_AS(getEnrolledLevelAsync, | ||
| getEnrolledLevelAsync:(EXPromiseResolveBlock)resolve | ||
| reject:(EXPromiseRejectBlock)reject) | ||
| { | ||
@@ -95,6 +89,6 @@ LAContext *context = [LAContext new]; | ||
| UM_EXPORT_METHOD_AS(authenticateAsync, | ||
| EX_EXPORT_METHOD_AS(authenticateAsync, | ||
| authenticateWithOptions:(NSDictionary *)options | ||
| resolve:(UMPromiseResolveBlock)resolve | ||
| reject:(UMPromiseRejectBlock)reject) | ||
| resolve:(EXPromiseResolveBlock)resolve | ||
| reject:(EXPromiseRejectBlock)reject) | ||
| { | ||
@@ -125,5 +119,3 @@ NSString *warningMessage; | ||
| if (@available(iOS 11.0, *)) { | ||
| context.interactionNotAllowed = false; | ||
| } | ||
| context.interactionNotAllowed = false; | ||
@@ -147,3 +139,3 @@ if ([disableDeviceFallback boolValue]) { | ||
| @"error": error == nil ? [NSNull null] : [self convertErrorCode:error], | ||
| @"warning": UMNullIfNil(warningMessage), | ||
| @"warning": EXNullIfNil(warningMessage), | ||
| }); | ||
@@ -158,3 +150,3 @@ }]; | ||
| @"error": error == nil ? [NSNull null] : [self convertErrorCode:error], | ||
| @"warning": UMNullIfNil(warningMessage), | ||
| @"warning": EXNullIfNil(warningMessage), | ||
| }); | ||
@@ -198,11 +190,9 @@ }]; | ||
| if (@available(iOS 11.0.1, *)) { | ||
| static dispatch_once_t onceToken; | ||
| static dispatch_once_t onceToken; | ||
| dispatch_once(&onceToken, ^{ | ||
| LAContext *context = [LAContext new]; | ||
| [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil]; | ||
| isFaceIDDevice = context.biometryType == LABiometryTypeFaceID; | ||
| }); | ||
| } | ||
| dispatch_once(&onceToken, ^{ | ||
| LAContext *context = [LAContext new]; | ||
| [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil]; | ||
| isFaceIDDevice = context.biometryType == LABiometryTypeFaceID; | ||
| }); | ||
@@ -220,7 +210,3 @@ return isFaceIDDevice; | ||
| [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil]; | ||
| if (@available(iOS 11.0.1, *)) { | ||
| isTouchIDDevice = context.biometryType == LABiometryTypeTouchID; | ||
| } else { | ||
| isTouchIDDevice = true; | ||
| } | ||
| isTouchIDDevice = context.biometryType == LABiometryTypeTouchID; | ||
| }); | ||
@@ -227,0 +213,0 @@ |
+4
-4
| { | ||
| "name": "expo-local-authentication", | ||
| "version": "11.1.1", | ||
| "version": "12.0.0", | ||
| "description": "Provides an API for FaceID and TouchID (iOS) or the Fingerprint API (Android) to authenticate the user with a face or fingerprint scan.", | ||
@@ -36,3 +36,3 @@ "main": "build/LocalAuthentication.js", | ||
| "license": "MIT", | ||
| "homepage": "https://docs.expo.io/versions/latest/sdk/local-authentication/", | ||
| "homepage": "https://docs.expo.dev/versions/latest/sdk/local-authentication/", | ||
| "jest": { | ||
@@ -42,3 +42,3 @@ "preset": "expo-module-scripts" | ||
| "dependencies": { | ||
| "@expo/config-plugins": "^3.0.0", | ||
| "@expo/config-plugins": "^3.1.0", | ||
| "invariant": "^2.2.4" | ||
@@ -49,3 +49,3 @@ }, | ||
| }, | ||
| "gitHead": "4fc9d282ff7ab2fa9040b775aeca7c30f5167b17" | ||
| "gitHead": "1fffde73411ee7a642b98f1506a8de921805d52b" | ||
| } |
@@ -1,3 +0,3 @@ | ||
| import { NativeModulesProxy } from '@unimodules/core'; | ||
| import { NativeModulesProxy } from 'expo-modules-core'; | ||
| export default NativeModulesProxy.ExpoLocalAuthentication; |
@@ -1,2 +0,2 @@ | ||
| import { UnavailabilityError } from '@unimodules/core'; | ||
| import { UnavailabilityError } from 'expo-modules-core'; | ||
| import invariant from 'invariant'; | ||
@@ -14,2 +14,8 @@ | ||
| // @needsAudit | ||
| /** | ||
| * Determine whether a face or fingerprint scanner is available on the device. | ||
| * @return Returns a promise which fulfils with a `boolean` value indicating whether a face or | ||
| * fingerprint scanner is available on this device. | ||
| */ | ||
| export async function hasHardwareAsync(): Promise<boolean> { | ||
@@ -22,2 +28,10 @@ if (!ExpoLocalAuthentication.hasHardwareAsync) { | ||
| // @needsAudit | ||
| /** | ||
| * Determine what kinds of authentications are available on the device. | ||
| * @return Returns a promise which fulfils to an array containing [`AuthenticationType`s](#authenticationtype). | ||
| * | ||
| * Devices can support multiple authentication methods- i.e. `[1,2]` means the device supports both | ||
| * fingerprint and facial recognition. If none are supported, this method returns an empty array. | ||
| */ | ||
| export async function supportedAuthenticationTypesAsync(): Promise<AuthenticationType[]> { | ||
@@ -30,2 +44,8 @@ if (!ExpoLocalAuthentication.supportedAuthenticationTypesAsync) { | ||
| // @needsAudit | ||
| /** | ||
| * Determine whether the device has saved fingerprints or facial data to use for authentication. | ||
| * @return Returns a promise which fulfils to `boolean` value indicating whether the device has | ||
| * saved fingerprints or facial data for authentication. | ||
| */ | ||
| export async function isEnrolledAsync(): Promise<boolean> { | ||
@@ -38,2 +58,10 @@ if (!ExpoLocalAuthentication.isEnrolledAsync) { | ||
| // @needsAudit | ||
| /** | ||
| * Determine what kind of authentication is enrolled on the device. | ||
| * @return Returns a promise which fulfils with [`SecurityLevel`](#securitylevel). | ||
| * > **Note:** On Android devices prior to M, `SECRET` can be returned if only the SIM lock has been | ||
| * enrolled, which is not the method that [`authenticateAsync`](#localauthenticationauthenticateasyncoptions) | ||
| * prompts. | ||
| */ | ||
| export async function getEnrolledLevelAsync(): Promise<SecurityLevel> { | ||
@@ -46,2 +74,12 @@ if (!ExpoLocalAuthentication.getEnrolledLevelAsync) { | ||
| // @needsAudit | ||
| /** | ||
| * Attempts to authenticate via Fingerprint/TouchID (or FaceID if available on the device). | ||
| * > **Note:** Apple requires apps which use FaceID to provide a description of why they use this API. | ||
| * If you try to use FaceID on an iPhone with FaceID without providing `infoPlist.NSFaceIDUsageDescription` | ||
| * in `app.json`, the module will authenticate using device passcode. For more information about | ||
| * usage descriptions on iOS, see [Deploying to App Stores](/distribution/app-stores#system-permissions-dialogs-on-ios). | ||
| * @param options | ||
| * @return Returns a promise which fulfils with [`LocalAuthenticationResult`](#localauthenticationresult). | ||
| */ | ||
| export async function authenticateAsync( | ||
@@ -70,2 +108,6 @@ options: LocalAuthenticationOptions = {} | ||
| // @needsAudit | ||
| /** | ||
| * **(Android Only)** Cancels authentication flow. | ||
| */ | ||
| export async function cancelAuthenticate(): Promise<void> { | ||
@@ -72,0 +114,0 @@ if (!ExpoLocalAuthentication.cancelAuthenticate) { |
@@ -1,22 +0,62 @@ | ||
| export type LocalAuthenticationResult = { success: true } | { success: false; error: string }; | ||
| export type LocalAuthenticationResult = | ||
| | { success: true } | ||
| | { success: false; error: string; warning?: string }; | ||
| // @needsAudit | ||
| export enum AuthenticationType { | ||
| /** | ||
| * Indicates fingerprint support. | ||
| */ | ||
| FINGERPRINT = 1, | ||
| /** | ||
| * Indicates facial recognition support. | ||
| */ | ||
| FACIAL_RECOGNITION = 2, | ||
| // Android only | ||
| /** | ||
| * __Android-only.__ Indicates iris recognition support. | ||
| */ | ||
| IRIS = 3, | ||
| } | ||
| // @needsAudit | ||
| export enum SecurityLevel { | ||
| /** | ||
| * Indicates no enrolled authentication. | ||
| */ | ||
| NONE = 0, | ||
| /** | ||
| * Indicates non-biometric authentication (e.g. PIN, Pattern). | ||
| */ | ||
| SECRET = 1, | ||
| /** | ||
| * Indicates biometric authentication. | ||
| */ | ||
| BIOMETRIC = 2, | ||
| } | ||
| // @needsAudit | ||
| export type LocalAuthenticationOptions = { | ||
| /** | ||
| * A message that is shown alongside the TouchID or FaceID prompt. | ||
| */ | ||
| promptMessage?: string; | ||
| /** | ||
| * Allows to customize the default `Cancel` label shown. | ||
| */ | ||
| cancelLabel?: string; | ||
| /** | ||
| * After several failed attempts the system will fallback to the device passcode. This setting | ||
| * allows you to disable this option and instead handle the fallback yourself. This can be | ||
| * preferable in certain custom authentication workflows. This behaviour maps to using the iOS | ||
| * [LAPolicyDeviceOwnerAuthenticationWithBiometrics](https://developer.apple.com/documentation/localauthentication/lapolicy/lapolicydeviceownerauthenticationwithbiometrics?language=objc) | ||
| * policy rather than the [LAPolicyDeviceOwnerAuthentication](https://developer.apple.com/documentation/localauthentication/lapolicy/lapolicydeviceownerauthentication?language=objc) | ||
| * policy. Defaults to `false`. | ||
| */ | ||
| disableDeviceFallback?: boolean; | ||
| // iOS only | ||
| /** | ||
| * **iOS only.** Allows to customize the default `Use Passcode` label shown after several failed | ||
| * authentication attempts. Setting this option to an empty string disables this button from | ||
| * showing in the prompt. | ||
| */ | ||
| fallbackLabel?: string; | ||
| }; |
| // Copyright 2015-present 650 Industries. All rights reserved. | ||
| package expo.modules.localauthentication; | ||
| import android.app.Activity; | ||
| import android.app.KeyguardManager; | ||
| import android.content.Context; | ||
| import android.content.Intent; | ||
| import android.content.pm.PackageManager; | ||
| import android.os.Build; | ||
| import android.os.Bundle; | ||
| import androidx.biometric.BiometricManager; | ||
| import androidx.biometric.BiometricPrompt; | ||
| import androidx.fragment.app.Fragment; | ||
| import androidx.fragment.app.FragmentActivity; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.Executor; | ||
| import java.util.concurrent.Executors; | ||
| import org.unimodules.core.ExportedModule; | ||
| import org.unimodules.core.ModuleRegistry; | ||
| import org.unimodules.core.Promise; | ||
| import org.unimodules.core.interfaces.ActivityEventListener; | ||
| import org.unimodules.core.interfaces.ActivityProvider; | ||
| import org.unimodules.core.interfaces.ExpoMethod; | ||
| import org.unimodules.core.interfaces.services.UIManager; | ||
| public class LocalAuthenticationModule extends ExportedModule implements ActivityEventListener { | ||
| private final BiometricManager mBiometricManager; | ||
| private final PackageManager mPackageManager; | ||
| private BiometricPrompt mBiometricPrompt; | ||
| private Promise mPromise; | ||
| private boolean mIsAuthenticating = false; | ||
| private ModuleRegistry mModuleRegistry; | ||
| private UIManager mUIManager; | ||
| private static final int AUTHENTICATION_TYPE_FINGERPRINT = 1; | ||
| private static final int AUTHENTICATION_TYPE_FACIAL_RECOGNITION = 2; | ||
| private static final int AUTHENTICATION_TYPE_IRIS = 3; | ||
| private static final int SECURITY_LEVEL_NONE = 0; | ||
| private static final int SECURITY_LEVEL_SECRET = 1; | ||
| private static final int SECURITY_LEVEL_BIOMETRIC = 2; | ||
| private final BiometricPrompt.AuthenticationCallback mAuthenticationCallback = | ||
| new BiometricPrompt.AuthenticationCallback () { | ||
| @Override | ||
| public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { | ||
| mIsAuthenticating = false; | ||
| mBiometricPrompt = null; | ||
| Bundle successResult = new Bundle(); | ||
| successResult.putBoolean("success", true); | ||
| safeResolve(successResult); | ||
| } | ||
| @Override | ||
| public void onAuthenticationError(int errMsgId, CharSequence errString) { | ||
| mIsAuthenticating = false; | ||
| mBiometricPrompt = null; | ||
| Bundle errorResult = new Bundle(); | ||
| errorResult.putBoolean("success", false); | ||
| errorResult.putString("error", convertErrorCode(errMsgId)); | ||
| errorResult.putString("message", errString.toString()); | ||
| safeResolve(errorResult); | ||
| } | ||
| }; | ||
| public LocalAuthenticationModule(Context context) { | ||
| super(context); | ||
| mBiometricManager = BiometricManager.from(context); | ||
| mPackageManager = context.getPackageManager(); | ||
| } | ||
| @Override | ||
| public String getName() { | ||
| return "ExpoLocalAuthentication"; | ||
| } | ||
| @Override | ||
| public void onCreate(ModuleRegistry moduleRegistry) { | ||
| mModuleRegistry = moduleRegistry; | ||
| mUIManager = moduleRegistry.getModule(UIManager.class); | ||
| mUIManager.registerActivityEventListener(this); | ||
| } | ||
| @ExpoMethod | ||
| public void supportedAuthenticationTypesAsync(final Promise promise) { | ||
| int result = mBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK); | ||
| List<Integer> results = new ArrayList<>(); | ||
| if (result == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { | ||
| promise.resolve(results); | ||
| return; | ||
| } | ||
| // note(cedric): replace hardcoded system feature strings with constants from | ||
| // PackageManager when dropping support for Android SDK 28 | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||
| if (mPackageManager.hasSystemFeature("android.hardware.fingerprint")) { | ||
| results.add(AUTHENTICATION_TYPE_FINGERPRINT); | ||
| } | ||
| } | ||
| if (Build.VERSION.SDK_INT >= 29) { | ||
| if (mPackageManager.hasSystemFeature("android.hardware.biometrics.face")) { | ||
| results.add(AUTHENTICATION_TYPE_FACIAL_RECOGNITION); | ||
| } | ||
| if (mPackageManager.hasSystemFeature("android.hardware.biometrics.iris")) { | ||
| results.add(AUTHENTICATION_TYPE_IRIS); | ||
| } | ||
| } | ||
| promise.resolve(results); | ||
| } | ||
| @ExpoMethod | ||
| public void hasHardwareAsync(final Promise promise) { | ||
| int result = mBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK); | ||
| promise.resolve(result != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE); | ||
| } | ||
| @ExpoMethod | ||
| public void isEnrolledAsync(final Promise promise) { | ||
| int result = mBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK); | ||
| promise.resolve(result == BiometricManager.BIOMETRIC_SUCCESS); | ||
| } | ||
| @ExpoMethod | ||
| public void getEnrolledLevelAsync(final Promise promise) { | ||
| int level = SECURITY_LEVEL_NONE; | ||
| if (isDeviceSecure()) { | ||
| level = SECURITY_LEVEL_SECRET; | ||
| } | ||
| int result = mBiometricManager.canAuthenticate(); | ||
| if (result == BiometricManager.BIOMETRIC_SUCCESS) { | ||
| level = SECURITY_LEVEL_BIOMETRIC; | ||
| } | ||
| promise.resolve(level); | ||
| } | ||
| @ExpoMethod | ||
| public void authenticateAsync(final Map<String, Object> options, final Promise promise) { | ||
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { | ||
| promise.reject("E_NOT_SUPPORTED", "Cannot display biometric prompt on android versions below 6.0"); | ||
| return; | ||
| } | ||
| if (getCurrentActivity() == null) { | ||
| promise.reject("E_NOT_FOREGROUND", "Cannot display biometric prompt when the app is not in the foreground"); | ||
| return; | ||
| } | ||
| if (getKeyguardManager().isDeviceSecure() == false) { | ||
| Bundle errorResult = new Bundle(); | ||
| errorResult.putBoolean("success", false); | ||
| errorResult.putString("error", "not_enrolled"); | ||
| errorResult.putString("message", "KeyguardManager#isDeviceSecure() returned false"); | ||
| promise.resolve(errorResult); | ||
| return; | ||
| } | ||
| final FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity(); | ||
| if (fragmentActivity == null) { | ||
| Bundle errorResult = new Bundle(); | ||
| errorResult.putBoolean("success", false); | ||
| errorResult.putString("error", "not_available"); | ||
| errorResult.putString("message", "getCurrentActivity() returned null"); | ||
| promise.resolve(errorResult); | ||
| return; | ||
| } | ||
| // BiometricPrompt callbacks are invoked on the main thread so also run this there to avoid | ||
| // having to do locking. | ||
| mUIManager.runOnUiQueueThread(new Runnable() { | ||
| @Override | ||
| public void run() { | ||
| if (mIsAuthenticating) { | ||
| Bundle cancelResult = new Bundle(); | ||
| cancelResult.putBoolean("success", false); | ||
| cancelResult.putString("error", "app_cancel"); | ||
| safeResolve(cancelResult); | ||
| mPromise = promise; | ||
| return; | ||
| } | ||
| String promptMessage = ""; | ||
| String cancelLabel = ""; | ||
| boolean disableDeviceFallback = false; | ||
| if (options.containsKey("promptMessage")) { | ||
| promptMessage = (String) options.get("promptMessage"); | ||
| } | ||
| if (options.containsKey("cancelLabel")) { | ||
| cancelLabel = (String) options.get("cancelLabel"); | ||
| } | ||
| if (options.containsKey("disableDeviceFallback")) { | ||
| disableDeviceFallback = (Boolean) options.get("disableDeviceFallback"); | ||
| } | ||
| mIsAuthenticating = true; | ||
| mPromise = promise; | ||
| Executor executor = Executors.newSingleThreadExecutor(); | ||
| mBiometricPrompt = new BiometricPrompt(fragmentActivity, executor, mAuthenticationCallback); | ||
| BiometricPrompt.PromptInfo.Builder promptInfoBuilder = new BiometricPrompt.PromptInfo.Builder() | ||
| .setTitle(promptMessage); | ||
| if (disableDeviceFallback) { | ||
| promptInfoBuilder.setNegativeButtonText(cancelLabel); | ||
| } else { | ||
| promptInfoBuilder.setAllowedAuthenticators( | ||
| BiometricManager.Authenticators.BIOMETRIC_WEAK | ||
| | BiometricManager.Authenticators.DEVICE_CREDENTIAL | ||
| ); | ||
| } | ||
| BiometricPrompt.PromptInfo promptInfo = promptInfoBuilder.build(); | ||
| try { | ||
| mBiometricPrompt.authenticate(promptInfo); | ||
| } catch (NullPointerException ex) { | ||
| promise.reject("E_INTERNAL_ERRROR", "Canceled authentication due to an internal error"); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| @ExpoMethod | ||
| public void cancelAuthenticate(final Promise promise) { | ||
| mUIManager.runOnUiQueueThread(new Runnable() { | ||
| @Override | ||
| public void run() { | ||
| safeCancel(); | ||
| promise.resolve(null); | ||
| } | ||
| }); | ||
| } | ||
| @Override | ||
| public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { | ||
| // If the user uses PIN as an authentication method, the result will be passed to the `onActivityResult`. | ||
| // Unfortunately, react-native doesn't pass this value to the underlying fragment - we won't resolve the promise. | ||
| // So we need to do it manually. | ||
| if (activity instanceof FragmentActivity) { | ||
| FragmentActivity fragmentActivity = (FragmentActivity) activity; | ||
| Fragment fragment = fragmentActivity.getSupportFragmentManager().findFragmentByTag("androidx.biometric.BiometricFragment"); | ||
| if (fragment != null) { | ||
| fragment.onActivityResult(requestCode & 0xffff, resultCode, data); | ||
| } | ||
| } | ||
| } | ||
| @Override | ||
| public void onNewIntent(Intent intent) { | ||
| // noop | ||
| } | ||
| private boolean isDeviceSecure() { | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||
| return getKeyguardManager().isDeviceSecure(); | ||
| } else { | ||
| // NOTE: `KeyguardManager#isKeyguardSecure()` considers SIM locked state, | ||
| // but it will be ignored on falling-back to device credential on biometric authentication. | ||
| // That means, setting level to `SECURITY_LEVEL_SECRET` might be misleading for some users. | ||
| // But there is no equivalent APIs prior to M. | ||
| // `andriodx.biometric.BiometricManager#canAuthenticate(int)` looks like an alternative, | ||
| // but specifying `BiometricManager.Authenticators.DEVICE_CREDENTIAL` alone is not | ||
| // supported prior to API 30. | ||
| // https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int) | ||
| return getKeyguardManager().isKeyguardSecure(); | ||
| } | ||
| } | ||
| private void safeCancel() { | ||
| if (mBiometricPrompt != null && mIsAuthenticating) { | ||
| mBiometricPrompt.cancelAuthentication(); | ||
| mIsAuthenticating = false; | ||
| } | ||
| } | ||
| private void safeResolve(Object result) { | ||
| if (mPromise != null) { | ||
| mPromise.resolve(result); | ||
| mPromise = null; | ||
| } | ||
| } | ||
| private static String convertErrorCode(int code) { | ||
| switch (code) { | ||
| case BiometricPrompt.ERROR_CANCELED: | ||
| case BiometricPrompt.ERROR_NEGATIVE_BUTTON: | ||
| case BiometricPrompt.ERROR_USER_CANCELED: | ||
| return "user_cancel"; | ||
| case BiometricPrompt.ERROR_HW_NOT_PRESENT: | ||
| case BiometricPrompt.ERROR_HW_UNAVAILABLE: | ||
| case BiometricPrompt.ERROR_NO_BIOMETRICS: | ||
| case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL: | ||
| return "not_available"; | ||
| case BiometricPrompt.ERROR_LOCKOUT: | ||
| case BiometricPrompt.ERROR_LOCKOUT_PERMANENT: | ||
| return "lockout"; | ||
| case BiometricPrompt.ERROR_NO_SPACE: | ||
| return "no_space"; | ||
| case BiometricPrompt.ERROR_TIMEOUT: | ||
| return "timeout"; | ||
| case BiometricPrompt.ERROR_UNABLE_TO_PROCESS: | ||
| return "unable_to_process"; | ||
| default: | ||
| return "unknown"; | ||
| } | ||
| } | ||
| private KeyguardManager getKeyguardManager() { | ||
| return (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE); | ||
| } | ||
| private Activity getCurrentActivity() { | ||
| ActivityProvider activityProvider = mModuleRegistry.getModule(ActivityProvider.class); | ||
| return activityProvider != null ? activityProvider.getCurrentActivity() : null; | ||
| } | ||
| } |
| package expo.modules.localauthentication; | ||
| import android.content.Context; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import org.unimodules.core.BasePackage; | ||
| import org.unimodules.core.ExportedModule; | ||
| public class LocalAuthenticationPackage extends BasePackage { | ||
| @Override | ||
| public List<ExportedModule> createExportedModules(Context context) { | ||
| return Collections.<ExportedModule>singletonList(new LocalAuthenticationModule(context)); | ||
| } | ||
| } |
Sorry, the diff of this file is not supported yet
62636
26.39%503
-13.43%Updated