From eaa9e9b8f290b11859e0ea0497128f1e6510bc9b Mon Sep 17 00:00:00 2001 From: Frank Kotsianas Date: Tue, 14 Jan 2020 14:27:10 -0500 Subject: [PATCH] Provides native prompt to authenticate; brings Android to parity with iOS - isSensorAvailable uses android BiometricManager - provides 'Biometrics' biometryType, as opposed to 'Fingerprint' - on Android Q (v29)+, uses device's preferred biometry type (user-decided) - on Android P (v28) and below, defaults to Fingerprint, supported by FingerprintCompat lib - on Android M-P (v23), checks for native fingerprint support on device (Samsung & MeiZu), and defaults to it if available - prefers Android native > other device native - authenticate uses android BiometricPrompt - now provides native prompt, as opposed to in addition to a lower-level key-signing API - now accepts description parameter for title text - in v23+, removes onAttempt, as event emitting can be removed from app in favor of being handled by promise resolve/rejection - update README and examples doc to reflect --- .travis.yml | 4 +- README.md | 180 +++++++++---- android/build.gradle | 10 +- android/src/main/AndroidManifest.xml | 1 + .../ReactNativeFingerprintScannerModule.java | 240 +++++++++++++++--- .../android/app/src/main/AndroidManifest.xml | 3 + examples/android/build.gradle | 8 +- .../src/FingerprintPopup.component.android.js | 90 +++++-- index.d.ts | 34 ++- package-lock.json | 2 +- package.json | 9 +- src/authenticate.android.js | 64 +++-- src/createError.js | 16 +- src/release.android.js | 7 +- 14 files changed, 532 insertions(+), 136 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8e03c7db..66465184 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,8 +23,8 @@ matrix: components: - tools - platform-tools - - build-tools-28.0.3 - - android-28 + - build-tools-29.0.2 + - android-29 sudo: true before_install: - nvm install --lts diff --git a/README.md b/README.md index 0e0132cb..338c11eb 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ It provides a **Default View** that prompts the user to place a finger to the iP ### Android Version +4.0.0 Prefers the new native Android BiometricPrompt lib on any Android >= v23 (M) +4.0.0 also DEPRECATES support for the legacy library that provides support for Samsung & MeiZu phones + +3.0.2 and below: Using an expandable Android Fingerprint API library, which combines [Samsung](http://developer.samsung.com/galaxy/pass#) and [MeiZu](http://open-wiki.flyme.cn/index.php?title=%E6%8C%87%E7%BA%B9%E8%AF%86%E5%88%ABAPI)'s official Fingerprint API. Samsung and MeiZu's Fingerprint SDK supports most devices which system versions less than Android 6.0. @@ -76,7 +80,7 @@ $ react-native link react-native-fingerprint-scanner ``` 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: ``` - compile project(':react-native-fingerprint-scanner') + implementation project(':react-native-fingerprint-scanner') ``` ### App Permissions @@ -86,12 +90,18 @@ Add the following permissions to their respective files: #### Android In your `AndroidManifest.xml`: -API level 28+ ([Reference](https://developer.android.com/reference/android/Manifest.permission#USE_BIOMETRIC)) +API level 28+ (Uses Android native BiometricPrompt) ([Reference](https://developer.android.com/reference/android/Manifest.permission#USE_BIOMETRIC)) ```xml ``` -API level <28 ([Reference](https://developer.android.com/reference/android/Manifest.permission#USE_FINGERPRINT)) +API level 23-28 (Uses Android native FingerprintCompat) [Reference](https://developer.android.com/reference/android/Manifest.permission#USE_FINGERPRINT)) +```xml + +``` + +// DEPRECATED in 4.0.0 +API level <23 (Uses device-specific native fingerprinting, if available - Samsung & MeiZu only) [Reference](https://developer.android.com/reference/android/Manifest.permission#USE_FINGERPRINT)) ```xml ``` @@ -107,23 +117,26 @@ In your `Info.plist`: 1. Make sure the following versions are all correct in `android/app/build.gradle` ``` + // API v29 enables FaceId android { - compileSdkVersion 25 - buildToolsVersion "25.0.3" + compileSdkVersion 29 + buildToolsVersion "29.0.2" ... defaultConfig { - targetSdkVersion 25 + targetSdkVersion 29 ``` 2. Add necessary rules to `android/app/proguard-rules.pro` if you are using proguard: ``` # MeiZu Fingerprint + // DEPRECATED in 4.0.0 -keep class com.fingerprints.service.** { *; } -dontwarn com.fingerprints.service.** # Samsung Fingerprint + // DEPRECATED in 4.0.0 -keep class com.samsung.android.sdk.** { *; } -dontwarn com.samsung.android.sdk.** ``` @@ -133,6 +146,7 @@ In your `Info.plist`: * For Gradle < 3 you MUST install react-native-fingerprint-scanner at version <= 2.5.0 * For RN >= 0.57 and/or Gradle >= 3 you MUST install react-native-fingerprint-scanner at version >= 2.6.0 * For RN >= 0.60 you MUST install react-native-fingerprint-scanner at version >= 3.0.0 +* For Android native Face Unlock, MUST use >= 4.0.0 ## Example @@ -174,54 +188,84 @@ export default FingerprintPopup; **Android Implementation** ```javascript + import React, { Component } from 'react'; import PropTypes from 'prop-types'; - import { Alert, Image, Text, TouchableOpacity, View, - ViewPropTypes + ViewPropTypes, + Platform, } from 'react-native'; -import FingerprintScanner from 'react-native-fingerprint-scanner'; -import ShakingText from './ShakingText.component'; +import FingerprintScanner from 'react-native-fingerprint-scanner'; import styles from './FingerprintPopup.component.styles'; +import ShakingText from './ShakingText.component'; -class FingerprintPopup extends Component { +// - this example component supports both the +// legacy device-specific (Android < v23) and +// current (Android >= 23) biometric APIs +// - your lib and implementation may not need both +class BiometricPopup extends Component { constructor(props) { super(props); - this.state = { errorMessage: undefined }; + this.state = { + errorMessageLegacy: undefined, + biometricLegacy: undefined + }; + + this.description = null; } componentDidMount() { + if (requiresLegacyAuthentication()) { + authLegacy(); + } else { + authCurrent(); + } + } + + componentWillUnmount = () => { + FingerprintScanner.release(); + } + + requiresLegacyAuthentication() { + return Platform.Version < 23; + } + + authCurrent() { FingerprintScanner - .authenticate({ onAttempt: this.handleAuthenticationAttempted }) + .authenticate({ description: 'Log in with Biometrics' }) .then(() => { - this.props.handlePopupDismissed(); + this.props.onAuthenticate(); + }); + } + + authLegacy() { + FingerprintScanner + .authenticate({ onAttempt: this.handleAuthenticationAttemptedLegacy }) + .then(() => { + this.props.handlePopupDismissedLegacy(); Alert.alert('Fingerprint Authentication', 'Authenticated successfully'); }) .catch((error) => { - this.setState({ errorMessage: error.message }); + this.setState({ errorMessageLegacy: error.message, biometricLegacy: error.biometric }); this.description.shake(); }); } - componentWillUnmount() { - FingerprintScanner.release(); - } - - handleAuthenticationAttempted = (error) => { - this.setState({ errorMessage: error.message }); + handleAuthenticationAttemptedLegacy = (error) => { + this.setState({ errorMessageLegacy: error.message }); this.description.shake(); }; - render() { - const { errorMessage } = this.state; - const { style, handlePopupDismissed } = this.props; + renderLegacy() { + const { errorMessageLegacy, biometricLegacy } = this.state; + const { style, handlePopupDismissedLegacy } = this.props; return ( @@ -233,17 +277,17 @@ class FingerprintPopup extends Component { /> - Fingerprint{'\n'}Authentication + Biometric{'\n'}Authentication { this.description = instance; }} - style={styles.description(!!errorMessage)}> - {errorMessage || 'Scan your fingerprint on the\ndevice scanner to continue'} + style={styles.description(!!errorMessageLegacy)}> + {errorMessageLegacy || `Scan your ${biometricLegacy} on the\ndevice scanner to continue`} BACK TO MAIN @@ -254,14 +298,25 @@ class FingerprintPopup extends Component { ); } + + + render = () => { + if (this.requiresLegacyAuthentication()) { + return this.renderLegacy(); + } + + // current API UI provided by native BiometricPrompt + return null; + } } -FingerprintPopup.propTypes = { +BiometricPopup.propTypes = { + onAuthenticate: PropTypes.func.isRequired, + handlePopupDismissedLegacy: PropTypes.func, style: ViewPropTypes.style, - handlePopupDismissed: PropTypes.func.isRequired, }; -export default FingerprintPopup; +export default BiometricPopup; ``` ## API @@ -271,6 +326,8 @@ Checks if Fingerprint Scanner is able to be used by now. - Returns a `Promise` - `biometryType: String` - The type of biometric authentication supported by the device. + - iOS: biometryType = 'Touch ID', 'Face ID' + - Android: biometryType = 'Biometrics' - `error: FingerprintScannerError { name, message, biometric }` - The name and message of failure and the biometric type in use. ```javascript @@ -304,29 +361,59 @@ componentDidMount() { } ``` -### `authenticate({ onAttempt })`: (Android) +### `authenticate({ description="Log In", onAttempt=() => (null) })`: (Android) Starts Fingerprint authentication on Android. - Returns a `Promise` +- `description: String` the title text to display in the native Android popup - `onAttempt: Function` - a callback function when users are trying to scan their fingerprint but failed. ```javascript componentDidMount() { + if (requiresLegacyAuthentication()) { + authLegacy(); + } else { + authCurrent(); + } +} + +componentWillUnmount = () => { + FingerprintScanner.release(); +} + +requiresLegacyAuthentication() { + return Platform.Version < 23; +} + +authCurrent() { FingerprintScanner - .authenticate({ onAttempt: this.handleAuthenticationAttempted }) + .authenticate({ description: 'Log in with Biometrics' }) .then(() => { - this.props.handlePopupDismissed(); + this.props.onAuthenticate(); + }); +} + +authLegacy() { + FingerprintScanner + .authenticate({ onAttempt: this.handleAuthenticationAttemptedLegacy }) + .then(() => { + this.props.handlePopupDismissedLegacy(); Alert.alert('Fingerprint Authentication', 'Authenticated successfully'); }) .catch((error) => { - this.setState({ errorMessage: error.message }); + this.setState({ errorMessageLegacy: error.message, biometricLegacy: error.biometric }); this.description.shake(); }); } + +handleAuthenticationAttemptedLegacy = (error) => { + this.setState({ errorMessageLegacy: error.message }); + this.description.shake(); +}; ``` ### `release()`: (Android) -Stops fingerprint scanner listener, releases cache of internal state in native code. +Stops fingerprint scanner listener, releases cache of internal state in native code, and cancels native prompt if visible. - Returns a `Void` @@ -338,11 +425,11 @@ componentWillUnmount() { ### `Types of Biometrics` -| Value | OS | -|---|---| -| Touch ID | iOS | -| Face ID | iOS | -| Fingerprint | Android | +| Value | OS | Description| +|---|---|---| +| Touch ID | iOS | | +| Face ID | iOS | | +| Biometrics | Android | Refers to the biometric set as preferred on the device | ### `Errors` @@ -350,15 +437,20 @@ componentWillUnmount() { |---|---| | AuthenticationNotMatch | No match | | AuthenticationFailed | Authentication was not successful because the user failed to provide valid credentials | +| AuthenticationTimeout | Authentication was not successful because the operation timed out | +| AuthenticationProcessFailed | 'Sensor was unable to process the image. Please try again | | UserCancel | Authentication was canceled by the user - e.g. the user tapped Cancel in the dialog | | UserFallback | Authentication was canceled because the user tapped the fallback button (Enter Password) | | SystemCancel | Authentication was canceled by system - e.g. if another application came to foreground while the authentication dialog was up | | PasscodeNotSet | Authentication could not start because the passcode is not set on the device | -| FingerprintScannerNotAvailable | Authentication could not start because Fingerprint Scanner is not available on the device | -| FingerprintScannerNotEnrolled | Authentication could not start because Fingerprint Scanner has no enrolled fingers | +| DeviceLocked | Authentication was not successful, the device currently in a lockout of 30 seconds | +| DeviceLockedPermanent | Authentication was not successful, device must be unlocked via password | +| DeviceOutOfMemory | Authentication could not proceed because there is not enough free memory on the device | +| HardwareError | A hardware error occurred | | FingerprintScannerUnknownError | Could not authenticate for an unknown reason | | FingerprintScannerNotSupported | Device does not support Fingerprint Scanner | -| DeviceLocked | Authentication was not successful, the device currently in a lockout of 30 seconds | +| FingerprintScannerNotEnrolled | Authentication could not start because Fingerprint Scanner has no enrolled fingers | +| FingerprintScannerNotAvailable | Authentication could not start because Fingerprint Scanner is not available on the device | ## License diff --git a/android/build.gradle b/android/build.gradle index e43c3a0d..daa5fb1f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,12 +21,12 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion safeExtGet('compileSdkVersion', 25) - buildToolsVersion safeExtGet('buildToolsVersion', '25.0.3') + compileSdkVersion safeExtGet('compileSdkVersion', 29) + buildToolsVersion safeExtGet('buildToolsVersion', '29.0.2') defaultConfig { minSdkVersion safeExtGet('minSdkVersion', 16) - targetSdkVersion safeExtGet('targetSdkVersion', 25) + targetSdkVersion safeExtGet('targetSdkVersion', 29) versionCode 1 versionName "1.0" } @@ -42,6 +42,10 @@ repositories { dependencies { compile 'com.facebook.react:react-native:+' + // androidx:biometric now supports fingerprint back to Android v23 + implementation "androidx.biometric:biometric:1.0.1" + + // retain fingerprintScanner lib for compat with Android v16-23 device-specific drivers (Samsung & MeiZu) // 1.2.3 is the minimum version compatible with androidx. // See https://github.com/uccmawei/FingerprintIdentify/issues/74 // (translation https://translate.google.com/translate?sl=zh-CN&tl=en&u=https://github.com/uccmawei/FingerprintIdentify/issues/74) diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 6610695d..6f979fa1 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + diff --git a/android/src/main/java/com/hieuvp/fingerprint/ReactNativeFingerprintScannerModule.java b/android/src/main/java/com/hieuvp/fingerprint/ReactNativeFingerprintScannerModule.java index f1dfba14..28372f23 100644 --- a/android/src/main/java/com/hieuvp/fingerprint/ReactNativeFingerprintScannerModule.java +++ b/android/src/main/java/com/hieuvp/fingerprint/ReactNativeFingerprintScannerModule.java @@ -1,23 +1,45 @@ package com.hieuvp.fingerprint; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.biometric.BiometricPrompt; +import androidx.biometric.BiometricManager; +import androidx.biometric.BiometricPrompt.AuthenticationCallback; +import androidx.biometric.BiometricPrompt.PromptInfo; +import androidx.fragment.app.FragmentActivity; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.bridge.UiThreadUtil; + +// for Samsung/MeiZu compat, Android v16-23 import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; import com.wei.android.lib.fingerprintidentify.FingerprintIdentify; import com.wei.android.lib.fingerprintidentify.base.BaseFingerprint.ExceptionListener; import com.wei.android.lib.fingerprintidentify.base.BaseFingerprint.IdentifyListener; + @ReactModule(name="ReactNativeFingerprintScanner") -public class ReactNativeFingerprintScannerModule extends ReactContextBaseJavaModule - implements LifecycleEventListener { +public class ReactNativeFingerprintScannerModule + extends ReactContextBaseJavaModule + implements LifecycleEventListener +{ public static final int MAX_AVAILABLE_TIMES = Integer.MAX_VALUE; - public static final String TYPE_FINGERPRINT = "Fingerprint"; + public static final String TYPE_BIOMETRICS = "Biometrics"; + public static final String TYPE_FINGERPRINT_LEGACY = "Fingerprint"; private final ReactApplicationContext mReactContext; + private BiometricPrompt biometricPrompt; + + // for Samsung/MeiZu compat, Android v16-23 private FingerprintIdentify mFingerprintIdentify; public ReactNativeFingerprintScannerModule(ReactApplicationContext reactContext) { @@ -43,6 +65,183 @@ public void onHostDestroy() { this.release(); } + private int currentAndroidVersion() { + return Build.VERSION.SDK_INT; + } + + private boolean requiresLegacyAuthentication() { + return currentAndroidVersion() < 23; + } + + public class AuthCallback extends BiometricPrompt.AuthenticationCallback { + private Promise promise; + + public AuthCallback(final Promise promise) { + super(); + this.promise = promise; + } + + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + this.promise.reject(biometricPromptErrName(errorCode), TYPE_BIOMETRICS); + } + + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + this.promise.resolve(true); + } + } + + public BiometricPrompt getBiometricPrompt(final Promise promise) { + // memoize so can be accessed to cancel + if (biometricPrompt != null) { + return biometricPrompt; + } + + // listen for onHost* methods + mReactContext.addLifecycleEventListener(this); + + AuthCallback authCallback = new AuthCallback(promise); + FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity(); + Executor executor = Executors.newSingleThreadExecutor(); + biometricPrompt = new BiometricPrompt( + fragmentActivity, + executor, + authCallback + ); + + return biometricPrompt; + } + + private void biometricAuthenticate(final String description, final Promise promise) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + BiometricPrompt bioPrompt = getBiometricPrompt(promise); + + PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() + .setDeviceCredentialAllowed(false) + .setConfirmationRequired(false) + .setNegativeButtonText("Cancel") + .setTitle(description) + .build(); + + bioPrompt.authenticate(promptInfo); + } + }); + } + + // the below constants are consistent across BiometricPrompt and BiometricManager + private String biometricPromptErrName(int errCode) { + switch (errCode) { + case BiometricPrompt.ERROR_CANCELED: + return "SystemCancel"; + case BiometricPrompt.ERROR_HW_NOT_PRESENT: + return "FingerprintScannerNotSupported"; + case BiometricPrompt.ERROR_HW_UNAVAILABLE: + return "FingerprintScannerNotAvailable"; + case BiometricPrompt.ERROR_LOCKOUT: + return "DeviceLocked"; + case BiometricPrompt.ERROR_LOCKOUT_PERMANENT: + return "DeviceLocked"; + case BiometricPrompt.ERROR_NEGATIVE_BUTTON: + return "UserCancel"; + case BiometricPrompt.ERROR_NO_BIOMETRICS: + return "FingerprintScannerNotEnrolled"; + case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL: + return "PasscodeNotSet"; + case BiometricPrompt.ERROR_NO_SPACE: + return "DeviceOutOfMemory"; + case BiometricPrompt.ERROR_TIMEOUT: + return "AuthenticationTimeout"; + case BiometricPrompt.ERROR_UNABLE_TO_PROCESS: + return "AuthenticationProcessFailed"; + case BiometricPrompt.ERROR_USER_CANCELED: // actually 'user elected another auth method' + return "UserFallback"; + case BiometricPrompt.ERROR_VENDOR: + // hardware-specific error codes + return "HardwareError"; + default: + return "FingerprintScannerUnknownError"; + } + } + + private String getSensorError() { + BiometricManager biometricManager = BiometricManager.from(mReactContext); + int authResult = biometricManager.canAuthenticate(); + + if (authResult == BiometricManager.BIOMETRIC_SUCCESS) { + return null; + } + if (authResult == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { + return "FingerprintScannerNotSupported"; + } else if (authResult == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { + return "FingerprintScannerNotEnrolled"; + } else if (authResult == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE) { + return "FingerprintScannerNotAvailable"; + } + + return null; + } + + @ReactMethod + public void authenticate(String description, final Promise promise) { + if (requiresLegacyAuthentication()) { + legacyAuthenticate(promise); + } + else { + final String errorName = getSensorError(); + if (errorName != null) { + promise.reject(errorName, TYPE_BIOMETRICS); + ReactNativeFingerprintScannerModule.this.release(); + return; + } + + biometricAuthenticate(description, promise); + } + } + + @ReactMethod + public void release() { + if (requiresLegacyAuthentication()) { + getFingerprintIdentify().cancelIdentify(); + mFingerprintIdentify = null; + } + + // consistent across legacy and current API + if (biometricPrompt != null) { + biometricPrompt.cancelAuthentication(); // if release called from eg React + } + biometricPrompt = null; + mReactContext.removeLifecycleEventListener(this); + } + + @ReactMethod + public void isSensorAvailable(final Promise promise) { + if (requiresLegacyAuthentication()) { + String errorMessage = legacyGetErrorMessage(); + if (errorMessage != null) { + promise.reject(errorMessage, TYPE_FINGERPRINT_LEGACY); + } else { + promise.resolve(TYPE_FINGERPRINT_LEGACY); + } + return; + } + + // current API + String errorName = getSensorError(); + if (errorName != null) { + promise.reject(errorName, TYPE_BIOMETRICS); + } else { + promise.resolve(TYPE_BIOMETRICS); + } + } + + + // for Samsung/MeiZu compat, Android v16-23 private FingerprintIdentify getFingerprintIdentify() { if (mFingerprintIdentify != null) { return mFingerprintIdentify; @@ -54,8 +253,7 @@ private FingerprintIdentify getFingerprintIdentify() { new ExceptionListener() { @Override public void onCatchException(Throwable exception) { - mReactContext.removeLifecycleEventListener( - ReactNativeFingerprintScannerModule.this); + mReactContext.removeLifecycleEventListener(ReactNativeFingerprintScannerModule.this); } } ); @@ -63,7 +261,7 @@ public void onCatchException(Throwable exception) { return mFingerprintIdentify; } - private String getErrorMessage() { + private String legacyGetErrorMessage() { if (!getFingerprintIdentify().isHardwareEnable()) { return "FingerprintScannerNotSupported"; } else if (!getFingerprintIdentify().isRegisteredFingerprint()) { @@ -71,14 +269,15 @@ private String getErrorMessage() { } else if (!getFingerprintIdentify().isFingerprintEnable()) { return "FingerprintScannerNotAvailable"; } + return null; } - @ReactMethod - public void authenticate(final Promise promise) { - final String errorMessage = getErrorMessage(); + + private void legacyAuthenticate(final Promise promise) { + final String errorMessage = legacyGetErrorMessage(); if (errorMessage != null) { - promise.reject(errorMessage, TYPE_FINGERPRINT); + promise.reject(errorMessage, TYPE_FINGERPRINT_LEGACY); ReactNativeFingerprintScannerModule.this.release(); return; } @@ -94,7 +293,7 @@ public void onSucceed() { @Override public void onNotMatch(int availableTimes) { mReactContext.getJSModule(RCTDeviceEventEmitter.class) - .emit("FINGERPRINT_SCANNER_AUTHENTICATION", "AuthenticationNotMatch"); + .emit("FINGERPRINT_SCANNER_AUTHENTICATION", "AuthenticationNotMatch"); } @Override @@ -102,7 +301,7 @@ public void onFailed(boolean isDeviceLocked) { if(isDeviceLocked){ promise.reject("AuthenticationFailed", "DeviceLocked"); } else { - promise.reject("AuthenticationFailed", TYPE_FINGERPRINT); + promise.reject("AuthenticationFailed", TYPE_FINGERPRINT_LEGACY); } ReactNativeFingerprintScannerModule.this.release(); } @@ -114,21 +313,4 @@ public void onStartFailedByDeviceLocked() { } }); } - - @ReactMethod - public void release() { - getFingerprintIdentify().cancelIdentify(); - mFingerprintIdentify = null; - mReactContext.removeLifecycleEventListener(this); - } - - @ReactMethod - public void isSensorAvailable(final Promise promise) { - String errorMessage = getErrorMessage(); - if (errorMessage != null) { - promise.reject(errorMessage, TYPE_FINGERPRINT); - } else { - promise.resolve(TYPE_FINGERPRINT); - } - } } diff --git a/examples/android/app/src/main/AndroidManifest.xml b/examples/android/app/src/main/AndroidManifest.xml index 92e1447c..13e28d64 100644 --- a/examples/android/app/src/main/AndroidManifest.xml +++ b/examples/android/app/src/main/AndroidManifest.xml @@ -5,7 +5,10 @@ + + + = 23) biometric APIs +// - your lib and implementation may not need both +class BiometricPopup extends Component { constructor(props) { super(props); - this.state = { errorMessage: undefined, biometric: undefined }; + this.state = { + errorMessageLegacy: undefined, + biometricLegacy: undefined + }; + + this.description = null; } componentDidMount() { + if (requiresLegacyAuthentication()) { + authLegacy(); + } else { + authCurrent(); + } + } + + componentWillUnmount = () => { + FingerprintScanner.release(); + } + + requiresLegacyAuthentication() { + return Platform.Version < 23; + } + + authCurrent() { FingerprintScanner - .authenticate({ onAttempt: this.handleAuthenticationAttempted }) + .authenticate({ description: this.props.description || 'Log in with Biometrics' }) .then(() => { - this.props.handlePopupDismissed(); + this.props.onAuthenticate(); + }); + } + + authLegacy() { + FingerprintScanner + .authenticate({ onAttempt: this.handleAuthenticationAttemptedLegacy }) + .then(() => { + this.props.handlePopupDismissedLegacy(); Alert.alert('Fingerprint Authentication', 'Authenticated successfully'); }) .catch((error) => { - this.setState({ errorMessage: error.message, biometric: error.biometric }); + this.setState({ errorMessageLegacy: error.message, biometricLegacy: error.biometric }); this.description.shake(); }); } - componentWillUnmount() { - FingerprintScanner.release(); - } - - handleAuthenticationAttempted = (error) => { - this.setState({ errorMessage: error.message }); + handleAuthenticationAttemptedLegacy = (error) => { + this.setState({ errorMessageLegacy: error.message }); this.description.shake(); }; - render() { - const { errorMessage, biometric } = this.state; - const { style, handlePopupDismissed } = this.props; + renderLegacy() { + const { errorMessageLegacy, biometricLegacy } = this.state; + const { style, handlePopupDismissedLegacy } = this.props; return ( @@ -60,13 +91,13 @@ class FingerprintPopup extends Component { { this.description = instance; }} - style={styles.description(!!errorMessage)}> - {errorMessage || `Scan your ${biometric} on the\ndevice scanner to continue`} + style={styles.description(!!errorMessageLegacy)}> + {errorMessageLegacy || `Scan your ${biometricLegacy} on the\ndevice scanner to continue`} BACK TO MAIN @@ -77,11 +108,24 @@ class FingerprintPopup extends Component { ); } + + + render = () => { + if (this.requiresLegacyAuthentication()) { + return this.renderLegacy(); + } + + // current API UI provided by native BiometricPrompt + return null; + } } -FingerprintPopup.propTypes = { +BiometricPopup.propTypes = { + description: PropTypes.string, + onAuthenticate: PropTypes.func.isRequired, + handlePopupDismissedLegacy: PropTypes.func, style: ViewPropTypes.style, - handlePopupDismissed: PropTypes.func.isRequired, }; -export default FingerprintPopup; +export default BiometricPopup; + diff --git a/index.d.ts b/index.d.ts index 58eb55e1..e05866d9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,7 +4,7 @@ export type AuthenticateIOS = { }; export type AuthenticateAndroid = { onAttempt: (error: FingerprintScannerError) => void }; -export type Biometrics = 'Touch ID' | 'Face ID' | 'Fingerprint'; +export type Biometrics = 'Touch ID' | 'Face ID' | 'Biometrics'; export type Errors = | { name: 'AuthenticationNotMatch'; message: 'No match' } @@ -12,6 +12,14 @@ export type Errors = name: 'AuthenticationFailed'; message: 'Authentication was not successful because the user failed to provide valid credentials'; } + | { + name: 'AuthenticationTimeout'; + message: 'Authentication was not successful because the operation timed out.'; + } + | { + name: 'AuthenticationProcessFailed'; + message: 'Sensor was unable to process the image. Please try again.'; + } | { name: 'UserCancel'; message: 'Authentication was canceled by the user - e.g. the user tapped Cancel in the dialog'; @@ -47,6 +55,18 @@ export type Errors = | { name: 'DeviceLocked'; message: 'Authentication was not successful, the device currently in a lockout of 30 seconds'; + } + | { + name: 'DeviceLockedPermanent'; + message: 'Authentication was not successful, device must be unlocked via password.'; + } + | { + name: 'DeviceOutOfMemory'; + message: 'Authentication could not proceed because there is not enough free memory on the device.'; + } + | { + name: 'HardwareError'; + message: 'A hardware error occurred.'; }; export type FingerprintScannerError = { biometric: Biometrics } & Errors; @@ -109,19 +129,23 @@ export interface FingerPrintProps { this.props.handlePopupDismissed(); AlertIOS.alert(error.message); }); - ``` + ``` ----------------- - - ### authenticate({ onAttempt }): (Android) + + ### authenticate({ description: 'Log in with Biometrics', onAttempt: () => (null) }): (Android) - Returns a `Promise` + - `description: String` - the title text to appear on the native Android prompt - `onAttempt: Function` - a callback function when users are trying to scan their fingerprint but failed. ----------------- - Example: ``` FingerprintScanner - .authenticate({ onAttempt: this.handleAuthenticationAttempted }) + .authenticate({ + description: 'Log in with Biometrics', + onAttempt: this.handleAuthenticationAttempted, + }) .then(() => { this.props.handlePopupDismissed(); Alert.alert('Fingerprint Authentication', 'Authenticated successfully'); diff --git a/package-lock.json b/package-lock.json index 3db920b1..1cc934ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { "name": "react-native-fingerprint-scanner", - "version": "3.0.0", + "version": "4.0.0", "lockfileVersion": 1 } diff --git a/package.json b/package.json index a4dd6063..05a8a576 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-fingerprint-scanner", - "version": "3.0.2", - "description": "React Native Fingerprint Scanner for Android and iOS", + "version": "4.0.0", + "description": "React Native Biometrics Scanner for Android and iOS", "main": "index.js", "keywords": [ "react-native", @@ -16,7 +16,10 @@ "fingerprint-scanner", "authentication", "authenticate", - "auth" + "auth", + "face-id", + "faceid", + "biometrics" ], "peerDependencies": { "react-native": ">=0.60 <1.0.0" diff --git a/src/authenticate.android.js b/src/authenticate.android.js index 4ac932eb..87ac2308 100644 --- a/src/authenticate.android.js +++ b/src/authenticate.android.js @@ -1,24 +1,56 @@ -import { DeviceEventEmitter, NativeModules } from 'react-native'; +import { + DeviceEventEmitter, + NativeModules, + Platform, +} from 'react-native'; import createError from './createError'; const { ReactNativeFingerprintScanner } = NativeModules; -export default ({ onAttempt }) => { - return new Promise((resolve, reject) => { - DeviceEventEmitter.addListener('FINGERPRINT_SCANNER_AUTHENTICATION', (name) => { - if (name === 'AuthenticationNotMatch' && typeof onAttempt === 'function') { - onAttempt(createError(name)); - } +const authCurrent = (description) => { + ReactNativeFingerprintScanner.authenticate(description) + .then(() => { + resolve(true); + }) + .catch((error) => { + // translate errors + reject(createError(error.code, error.message)); + }); +} + +const authLegacy = (onAttempt) => { + DeviceEventEmitter.addListener('FINGERPRINT_SCANNER_AUTHENTICATION', (name) => { + if (name === 'AuthenticationNotMatch' && typeof onAttempt === 'function') { + onAttempt(createError(name)); + } + }); + + ReactNativeFingerprintScanner.authenticate() + .then(() => { + DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION'); + resolve(true); + }) + .catch((error) => { + DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION'); + reject(createError(error.code, error.message)); }); +} + +const nullOnAttempt = () => null; + +export default ({ description, onAttempt }) => { + return new Promise((resolve, reject) => { + if (!description) { + description = "Log In"; + } + if (!onAttempt) { + onAttempt = nullOnAttempt; + } + + if (Platform.Version < 23) { + return authLegacy(onAttempt); + } - ReactNativeFingerprintScanner.authenticate() - .then(() => { - DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION'); - resolve(true); - }) - .catch((error) => { - DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION'); - reject(createError(error.code, error.message)); - }); + return authCurrent(description); }); } diff --git a/src/createError.js b/src/createError.js index 335cacb6..f4535649 100644 --- a/src/createError.js +++ b/src/createError.js @@ -1,15 +1,23 @@ const ERRORS = { + // sensor availability + FingerprintScannerNotSupported: 'Device does not support Fingerprint Scanner.', + FingerprintScannerNotEnrolled: 'Authentication could not start because Fingerprint Scanner has no enrolled fingers.', + FingerprintScannerNotAvailable: 'Authentication could not start because Fingerprint Scanner is not available on the device.', + + // auth failures AuthenticationNotMatch: 'No match.', AuthenticationFailed: 'Authentication was not successful because the user failed to provide valid credentials.', + AuthenticationTimeout: 'Authentication was not successful because the operation timed out.', + AuthenticationProcessFailed: 'Sensor was unable to process the image. Please try again.', UserCancel: 'Authentication was canceled by the user - e.g. the user tapped Cancel in the dialog.', UserFallback: 'Authentication was canceled because the user tapped the fallback button (Enter Password).', SystemCancel: 'Authentication was canceled by system - e.g. if another application came to foreground while the authentication dialog was up.', PasscodeNotSet: 'Authentication could not start because the passcode is not set on the device.', - FingerprintScannerNotAvailable: 'Authentication could not start because Fingerprint Scanner is not available on the device.', - FingerprintScannerNotEnrolled: 'Authentication could not start because Fingerprint Scanner has no enrolled fingers.', FingerprintScannerUnknownError: 'Could not authenticate for an unknown reason.', - FingerprintScannerNotSupported: 'Device does not support Fingerprint Scanner.', - DeviceLocked: 'Authentication was not successful, the device currently in a lockout of 30 seconds' + DeviceLocked: 'Authentication was not successful, the device currently in a lockout of 30 seconds.', + DeviceLockedPermanent: 'Authentication was not successful, device must be unlocked via password.', + DeviceOutOfMemory: 'Authentication could not proceed because there is not enough free memory on the device.', + HardwareError: 'A hardware error occurred.', }; class FingerprintScannerError extends Error { diff --git a/src/release.android.js b/src/release.android.js index 43954866..79859d8a 100644 --- a/src/release.android.js +++ b/src/release.android.js @@ -1,8 +1,11 @@ -import { DeviceEventEmitter, NativeModules } from 'react-native'; +import { DeviceEventEmitter, NativeModules, Platform } from 'react-native'; const { ReactNativeFingerprintScanner } = NativeModules; export default () => { - DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION'); + if (Platform.Version < 23) { + DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION'); + } + ReactNativeFingerprintScanner.release(); }