diff --git a/README.md b/README.md index 42ff194..c2bcde6 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ It provides a **Default View** that prompts the user to place a finger to the iP 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: +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. @@ -74,14 +74,14 @@ $ react-native link react-native-fingerprint-scanner - Add `import com.hieuvp.fingerprint.ReactNativeFingerprintScannerPackage;` to the imports at the top of the file - Add `new ReactNativeFingerprintScannerPackage()` to the list returned by the `getPackages()` method 2. Append the following lines to `android/settings.gradle`: - ``` - include ':react-native-fingerprint-scanner' - project(':react-native-fingerprint-scanner').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fingerprint-scanner/android') - ``` + ``` + include ':react-native-fingerprint-scanner' + project(':react-native-fingerprint-scanner').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fingerprint-scanner/android') + ``` 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: - ``` + ``` implementation project(':react-native-fingerprint-scanner') - ``` + ``` ### App Permissions @@ -95,13 +95,13 @@ API level 28+ (Uses Android native BiometricPrompt) ([Reference](https://develop ``` -API level 23-28 (Uses Android native FingerprintCompat) [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)) +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 ``` @@ -162,27 +162,62 @@ import FingerprintScanner from 'react-native-fingerprint-scanner'; class FingerprintPopup extends Component { componentDidMount() { + this._iosTouchID() + } + + _iosTouchID = () => { FingerprintScanner .authenticate({ description: 'Scan your fingerprint on the device scanner to continue' }) .then(() => { - this.props.handlePopupDismissed(); AlertIOS.alert('Authenticated successfully'); }) .catch((error) => { - this.props.handlePopupDismissed(); + switch (error.biometric) { + case 'UserCancel': + AlertIOS.alert('The user clicks the cancel button') + break + case 'AuthenticationFailed': + AlertIOS.alert('User failed to identify 3 times') + break + case 'AuthenticationLockout': + // console.log('Accumulated 5 identification failures, fingerprint identification was locked') + AlertIOS.alert('Identify cumulative multiple failures, temporarily unavailable', [ + { + text: 'cancel', + style: 'default', + onPress: () => { + } + }, { + text: 'To unlock', + style: 'default', + onPress: () => { + this._iosAuthenticateDevice() + } + } + ]) + break + default: + break + } AlertIOS.alert(error.message); }); } + _iosAuthenticateDevice = () => { + FingerprintScanner.authenticateDevice().then(() => { + // console.log('Device unlocked') + this._iosTouchID() + }).catch((error) => { + // error.biometric + AlertIOS.alert('catch error:', error.message) + }) + } + render() { return false; } } -FingerprintPopup.propTypes = { - handlePopupDismissed: PropTypes.func.isRequired, -}; - export default FingerprintPopup; ``` @@ -222,11 +257,7 @@ class BiometricPopup extends Component { } componentDidMount() { - if (this.requiresLegacyAuthentication()) { - this.authLegacy(); - } else { - this.authCurrent(); - } + this._androidTouchID(); } componentWillUnmount = () => { @@ -237,24 +268,28 @@ class BiometricPopup extends Component { return Platform.Version < 23; } - authCurrent() { + _androidTouchID() { + FingerprintScanner.release() FingerprintScanner .authenticate({ title: 'Log in with Biometrics' }) .then(() => { this.props.onAuthenticate(); - }); - } - - authLegacy() { - FingerprintScanner - .authenticate({ onAttempt: this.handleAuthenticationAttemptedLegacy }) - .then(() => { - this.props.handlePopupDismissedLegacy(); - Alert.alert('Fingerprint Authentication', 'Authenticated successfully'); }) - .catch((error) => { - this.setState({ errorMessageLegacy: error.message, biometricLegacy: error.biometric }); - this.description.shake(); + .catch(error => { + FingerprintScanner.release() + switch (error.biometric) { + case 'UserCancel': + AlertIOS.alert('Click the cancel button') + break + case 'DeviceLocked': + AlertIOS.alert('Accumulated 5 identification failures, fingerprint identification was locked') + break + case 'DeviceLockedPermanent': + AlertIOS.alert('Accumulates many times to recognize the failure, is locked permanently, needs to unlock') + break + default: + break + } }); } @@ -339,6 +374,24 @@ componentDidMount() { } ``` +### `authenticateDevice()`: (iOS) +Unlock with the device password. + +- Returns a `Promise` +- `error: FingerprintScannerError { name, message, biometric }` - The name and message of failure and the biometric type in use. + + +```javascript + FingerprintScanner + .authenticateDevice() + .then(() => { + // AlertIOS.alert('Device unlocked') + this._iosTouchID() + }).catch((error) => { + // AlertIOS.alert('catch error:', error.message, error.biometric) + }) +``` + ### `authenticate({ description, fallbackEnabled })`: (iOS) Starts Fingerprint authentication on iOS. @@ -438,6 +491,7 @@ componentWillUnmount() { | Name | Message | |---|---| +| AuthenticationLockout | Authentication lockout | | 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 | @@ -450,6 +504,7 @@ componentWillUnmount() { | 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 | +| UserDeviceCancel | Authentication Device was canceled | | FingerprintScannerUnknownError | Could not authenticate for an unknown reason | | FingerprintScannerNotSupported | Device does not support Fingerprint Scanner | | FingerprintScannerNotEnrolled | Authentication could not start because Fingerprint Scanner has no enrolled fingers | @@ -458,3 +513,5 @@ componentWillUnmount() { ## License MIT + + diff --git a/android/src/main/java/com/hieuvp/fingerprint/ReactNativeFingerprintScannerModule.java b/android/src/main/java/com/hieuvp/fingerprint/ReactNativeFingerprintScannerModule.java index 2db879d..dfb4232 100644 --- a/android/src/main/java/com/hieuvp/fingerprint/ReactNativeFingerprintScannerModule.java +++ b/android/src/main/java/com/hieuvp/fingerprint/ReactNativeFingerprintScannerModule.java @@ -84,7 +84,7 @@ public AuthCallback(final Promise promise) { @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { super.onAuthenticationError(errorCode, errString); - this.promise.reject(biometricPromptErrName(errorCode), TYPE_BIOMETRICS); + this.promise.reject(biometricPromptErrName(errorCode), biometricPromptErrName(errorCode)); } @Override @@ -193,12 +193,10 @@ private String getSensorError() { public void authenticate(String title, String subtitle, String description, String cancelButton, final Promise promise) { if (requiresLegacyAuthentication()) { legacyAuthenticate(promise); - } - else { + } else { final String errorName = getSensorError(); if (errorName != null) { - promise.reject(errorName, TYPE_BIOMETRICS); - ReactNativeFingerprintScannerModule.this.release(); + promise.reject(errorName, errorName); return; } @@ -236,13 +234,12 @@ public void isSensorAvailable(final Promise promise) { // current API String errorName = getSensorError(); if (errorName != null) { - promise.reject(errorName, TYPE_BIOMETRICS); + promise.reject(errorName, errorName); } else { promise.resolve(TYPE_BIOMETRICS); } } - // for Samsung/MeiZu compat, Android v16-23 private FingerprintIdentify getFingerprintIdentify() { if (mFingerprintIdentify != null) { @@ -305,12 +302,11 @@ public void onNotMatch(int availableTimes) { @Override public void onFailed(boolean isDeviceLocked) { - if(isDeviceLocked){ + if (isDeviceLocked) { promise.reject("AuthenticationFailed", "DeviceLocked"); } else { promise.reject("AuthenticationFailed", TYPE_FINGERPRINT_LEGACY); } - ReactNativeFingerprintScannerModule.this.release(); } @Override diff --git a/index.d.ts b/index.d.ts index 6de5573..c8625e3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,7 +13,8 @@ export type AuthenticateAndroid = { export type Biometrics = 'Touch ID' | 'Face ID' | 'Biometrics'; export type Errors = - | { name: 'AuthenticationNotMatch'; message: 'No match' } + | { name: 'AuthenticationLockout'; message: 'Authentication lockout'; } + | { name: 'AuthenticationNotMatch'; message: 'No match'; } | { name: 'AuthenticationFailed'; message: 'Authentication was not successful because the user failed to provide valid credentials'; @@ -44,11 +45,11 @@ export type Errors = } | { name: 'FingerprintScannerNotAvailable'; - message: ' Authentication could not start because Fingerprint Scanner is not available on the device'; + message: ' Authentication could not start because Fingerprint Scanner is not available on the device'; } | { name: 'FingerprintScannerNotEnrolled'; - message: ' Authentication could not start because Fingerprint Scanner has no enrolled fingers'; + message: ' Authentication could not start because Fingerprint Scanner has no enrolled fingers'; } | { name: 'FingerprintScannerUnknownError'; @@ -73,6 +74,10 @@ export type Errors = | { name: 'HardwareError'; message: 'A hardware error occurred.'; + } + | { + name: 'UserDeviceCancel'; + message: 'Authentication Device was canceled'; }; export type FingerprintScannerError = { biometric: Biometrics } & Errors; @@ -166,6 +171,27 @@ export interface FingerPrintProps { authenticate: ( platformProps: AuthenticateIOS | AuthenticateAndroid ) => Promise; + + /** + ### authenticateDevice(): (iOS) + Unlock with the device password. + - Returns a `Promise` + - `error: FingerprintScannerError { name, message, biometric }` - The name and message of failure and the biometric type in use. + + ------------- + Exemple + + ``` + FingerprintScanner + .authenticateDevice() + .then(() => { + AlertIOS.alert('Authenticated successfully'); + }) + .catch(error => this.setState({ errorMessage: error.message })); + ``` + ------------ + */ + authenticateDevice: () => Promise; } declare const FingerprintScanner: FingerPrintProps; diff --git a/ios/ReactNativeFingerprintScanner.m b/ios/ReactNativeFingerprintScanner.m index e29bad7..33408a7 100644 --- a/ios/ReactNativeFingerprintScanner.m +++ b/ios/ReactNativeFingerprintScanner.m @@ -26,29 +26,54 @@ @implementation ReactNativeFingerprintScanner code = @"FingerprintScannerNotAvailable"; message = [self getBiometryType:context]; break; - + case LAErrorBiometryNotEnrolled: code = @"FingerprintScannerNotEnrolled"; message = [self getBiometryType:context]; break; + case LAErrorTouchIDLockout: + code = @"AuthenticationLockout"; + message = [self getBiometryType:context]; + break; + + case LAErrorAuthenticationFailed: + code = @"AuthenticationFailed"; + message = [self getBiometryType:context]; + break; + + case LAErrorUserCancel: + code = @"UserCancel"; + message = [self getBiometryType:context]; + break; + + case LAErrorUserFallback: + code = @"UserFallback"; + message = [self getBiometryType:context]; + break; + + case LAErrorSystemCancel: + code = @"SystemCancel"; + message = [self getBiometryType:context]; + break; + case LAErrorBiometryLockout: code = @"DeviceLockedPermanent"; message = [self getBiometryType:context]; break; - + case LAErrorPasscodeNotSet: code = @"PasscodeNotSet"; message = [self getBiometryType:context]; break; default: - code = @"FingerprintScannerNotSupported"; + code = @"AuthenticationNotMatch"; message = nil; break; } - - callback(@[RCTJSErrorFromCodeMessageAndNSError(code, message, nil)]); + NSLog(@"Authentication failed: %@", code); + callback(@[RCTJSErrorFromCodeMessageAndNSError(code, code, nil)]); return; } } @@ -77,6 +102,11 @@ @implementation ReactNativeFingerprintScanner NSString *errorReason; switch (error.code) { + + case LAErrorTouchIDLockout: + errorReason = @"AuthenticationLockout"; + break; + case LAErrorAuthenticationFailed: errorReason = @"AuthenticationFailed"; break; @@ -110,7 +140,7 @@ @implementation ReactNativeFingerprintScanner break; default: - errorReason = @"FingerprintScannerUnknownError"; + errorReason = @"AuthenticationNotMatch"; break; } @@ -159,11 +189,65 @@ @implementation ReactNativeFingerprintScanner return; } // Device does not support FingerprintScanner - callback(@[RCTJSErrorFromCodeMessageAndNSError(@"FingerprintScannerNotSupported", @"FingerprintScannerNotSupported", nil)]); + // callback(@[RCTJSErrorFromCodeMessageAndNSError(@"FingerprintScannerNotSupported", @"FingerprintScannerNotSupported", nil)]); + NSString *errorReason; + + switch (error.code) { + case LAErrorTouchIDNotAvailable: + errorReason = @"FingerprintScannerNotAvailable"; + break; + + case LAErrorTouchIDNotEnrolled: + errorReason = @"FingerprintScannerNotEnrolled"; + break; + + case LAErrorTouchIDLockout: + errorReason = @"AuthenticationLockout"; + break; + + case LAErrorAuthenticationFailed: + errorReason = @"AuthenticationFailed"; + break; + + case LAErrorUserCancel: + errorReason = @"UserCancel"; + break; + + case LAErrorUserFallback: + errorReason = @"UserFallback"; + break; + + case LAErrorSystemCancel: + errorReason = @"SystemCancel"; + break; + + case LAErrorPasscodeNotSet: + errorReason = @"PasscodeNotSet"; + break; + + default: + errorReason = @"AuthenticationNotMatch"; + break; + } + callback(@[RCTJSErrorFromCodeMessageAndNSError(errorReason, errorReason, nil)]); return; } } +RCT_EXPORT_METHOD(authenticateDevice: (RCTResponseSenderBlock)callback) +{ + LAContext *context = [[LAContext alloc] init]; + [context evaluatePolicy:LAPolicyDeviceOwnerAuthentication localizedReason: @" " reply:^(BOOL success, NSError * _Nullable error) { + if(error) { + NSString *errorReason = @"UserDeviceCancel"; + NSLog(@"Authentication failed: %@", errorReason); + callback(@[RCTJSErrorFromCodeMessageAndNSError(errorReason, errorReason, nil)]); + } else { + callback(@[[NSNull null], @"Authentication unlock."]); + } + }]; +} + - (NSString *)getBiometryType:(LAContext *)context { if (@available(iOS 11, *)) { diff --git a/src/authenticateDevice.android.js b/src/authenticateDevice.android.js new file mode 100644 index 0000000..461f67a --- /dev/null +++ b/src/authenticateDevice.android.js @@ -0,0 +1 @@ +export default () => null; diff --git a/src/authenticateDevice.ios.js b/src/authenticateDevice.ios.js new file mode 100644 index 0000000..a878cdd --- /dev/null +++ b/src/authenticateDevice.ios.js @@ -0,0 +1,16 @@ +import { NativeModules } from 'react-native'; +import createError from 'react-native-fingerprint-scanner/src/createError'; + +const { ReactNativeFingerprintScanner } = NativeModules; + +export default () => { + return new Promise((resolve, reject) => { + ReactNativeFingerprintScanner.authenticateDevice(error => { + if (error) { + return reject(createError(error.code, error.message)) + } + + return resolve(true); + }); + }); +} diff --git a/src/createError.js b/src/createError.js index f453564..34a2c13 100644 --- a/src/createError.js +++ b/src/createError.js @@ -5,6 +5,7 @@ const ERRORS = { FingerprintScannerNotAvailable: 'Authentication could not start because Fingerprint Scanner is not available on the device.', // auth failures + AuthenticationLockout: 'Authentication lockout', 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.', @@ -18,6 +19,7 @@ const ERRORS = { 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.', + UserDeviceCancel: 'Authentication Device was canceled' }; class FingerprintScannerError extends Error { diff --git a/src/index.js b/src/index.js index 3fc84b5..4d9400e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,11 @@ import authenticate from './authenticate'; import isSensorAvailable from './isSensorAvailable'; import release from './release'; +import authenticateDevice from './authenticateDevice'; export default { authenticate, release, isSensorAvailable, + authenticateDevice };