Compare commits

...

44 Commits

Author SHA1 Message Date
Marcos Rodriguez Vélez
ce64467368
Update ReactNativeFingerprintScanner.m 2020-09-08 19:56:47 -04:00
Marcos Rodriguez Vélez
5988a07c31
Update ReactNativeFingerprintScanner.m 2020-09-08 00:07:36 -04:00
Phill Baker
910e563bb5 Bump version. 2020-06-13 18:15:48 -04:00
Iagor Moraes
0d9f8219cb
Fix NPE on BiometricPrompt instance (#141)
* update lock

* check fragment activity
2020-06-11 22:24:26 -04:00
Phillip Baker
4cea388974 Bump version. 2020-05-05 08:58:09 -04:00
SaeedZhiany
7e30ab9828
Update build.gradle (#104)
changed deprecated 'compile' keyword to 'implementation'
2020-05-04 09:11:18 -04:00
java-james
770465db16
feat: return additional biometric statuses (#131)
* feat: return device lockout on biometrics status

* feat: Add more error management in ios authenticate

* fix: biometrics lockout error code mapping

* refactor: remove un-needed errors

Co-authored-by: Aiden Petersen <aiden.petersen@mattr.global>
2020-05-04 09:11:01 -04:00
Phillip Baker
94e26dd916 Bump version. 2020-05-03 10:53:12 -04:00
Phecda Su
7b3f8e5501
feat(types): improve type declaration (#136) 2020-05-01 00:04:43 -04:00
Nathan R
4c7145aa7a
Added Cancelbutton text option, new description and subtitle text when requesting fingerprint ID for better customization (#120)
* Add all android biometrics parameters
* description become title if title not set
2020-04-29 22:25:51 -04:00
MaoApp
ce0ec2c2f2
fix function call in implementation android example (#122) 2020-02-05 23:07:51 -05:00
Aishwary Raj
4a22b4dea9 pass required args (#117) 2020-01-28 11:42:28 -05:00
Frank Kotsianas
57556a4f2f
Merge pull request #115 from fkotsian/features/android-biometricx-faceid
Upgrade android to use new BiometricPrompt API
2020-01-24 18:01:37 -05:00
Frank Kotsianas
879babbd33 typo version 2020-01-24 16:17:41 -05:00
Frank Kotsianas
45b98e7646 debug auth args 2020-01-23 23:34:49 -05:00
Frank Kotsianas
256adff995 debug 2020-01-23 19:56:59 -05:00
Frank Kotsianas
90e10d0aef Add deprecation warning to README 2020-01-23 13:49:33 -05:00
Frank Kotsianas
8c811d8860 Tests caught stuff; good tests 2020-01-23 13:44:45 -05:00
Frank Kotsianas
c3283a67bd titleText -> description for consistency with iOS 2020-01-23 13:25:17 -05:00
Frank Kotsianas
af7f5e8a1a Update README with changes for Android <23 compat 2020-01-23 13:21:02 -05:00
Frank Kotsianas
dfcfce6c74 Add legacy (Samsung & MeiZi, Android < 23) support back
- ugly as all heck, but OK
2020-01-23 13:04:43 -05:00
Frank Kotsianas
09cd6ccf1e debug, update some doc
- still needs package update to switch to old package for old versions
2020-01-22 15:38:56 -05:00
Frank Kotsianas
38b39a73ac debug, document 2020-01-17 14:12:09 -05:00
Frank Kotsianas
426defd52a Cleanup and update Package spec 2020-01-15 17:50:18 -05:00
Frank Kotsianas
ee5b97bdd2 Update README 2020-01-15 17:09:27 -05:00
Frank Kotsianas
6078d0a792 Return new constant BIOMETRICS from isSensorAvailable
- unfortunately does not differentiate between FINGERPRINT and FACE_ID
   - uses default device biometric type
- attempts to pass titleText string through from React to authenticate()
Javaland method
2020-01-15 16:41:51 -05:00
Frank Kotsianas
f1963529ec Add androidx-specific error messages 2020-01-15 14:59:25 -05:00
Frank Kotsianas
937daae215 clean up RNFSModule
- remove event emitters (now handled in promise rejection)
- correctly pass promise around
- keep lifecycleListeners for onHostDestroy
- return native lib error codes to be captured & translated
2020-01-15 11:57:42 -05:00
Frank Kotsianas
e38a26fbe6 WIP: needs error msgs, investigate Listeners, otherwise, should be good
to go?

- idk, load it up on local & convert the android Biopopup modal to ios
and see if we can auth in
  - also see if we can cancel, shee
2020-01-14 18:53:36 -05:00
Frank Kotsianas
b372c404c2 Encapsulate logic for FingerprintIdentify (changes no business logic)
- switch based on Android API level
2020-01-14 16:08:46 -05:00
Phillip Baker
28a7e58194
Merge pull request #109 from petekp/patch-1
Update README with correct permission for API 28+
2019-11-17 12:39:06 -05:00
Pete
938bce2484
Update README with correct permission for API 28+ 2019-11-15 09:18:33 -08:00
dependabot[bot]
c0065278b4
Merge pull request #107 from hieuvp/dependabot/npm_and_yarn/examples/lodash-4.17.15 2019-11-05 02:13:36 +00:00
dependabot[bot]
de8a5bd7df
Bump lodash from 4.17.11 to 4.17.15 in /examples
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-02 18:57:33 +00:00
Phillip Baker
faa36ca188 3.0.2 2019-10-24 07:37:46 -04:00
SaeedZhiany
4101590a71 Update build.gradle (#101)
* Update build.gradle

Load Android Gradle Plugin conditionally and change 'compile' to 'implementation'

* Update build.gradle

revert changes `implementation` to `compile` due to breaking change.
2019-10-22 10:11:19 -04:00
Salomão Luiz de Araújo Neto
4209ba62ee Fixed types of onAttempt and isSensorAvailable (#103)
* fix types of onAttempt and isSensorAvailable

* fix the description of isSensorAvailable
2019-10-21 09:44:06 -04:00
Phillip Baker
9a698e2a13 3.0.1 2019-10-20 17:25:25 -04:00
Salomão Luiz de Araújo Neto
a1d25edca5 feat: Add types to support typescript (#100)
* feat: Add types to support typescript

* Changed the export default to correctly.
2019-10-17 10:13:56 -04:00
Phillip Baker
aa85e11a8e
Merge pull request #97 from sahil290791/master
Update README.md with linking needed for RN < 0.60
2019-09-14 19:44:40 -04:00
Sahil Prajapati
99a60de1fb
Update README.md with linking needed for RN < 0.60
As the project is supporting RN >= 0.60, [linking is automatically done](https://facebook.github.io/react-native/blog/2019/07/03/version-60#native-modules-are-now-autolinked).
2019-09-14 06:57:25 +05:30
Phillip Baker
6604f44098 Add testing on travis. 2019-09-08 17:08:29 -04:00
Phillip Baker
9f1b37b836
Merge pull request #92 from imdaniele/react_context
React context
2019-08-29 21:37:56 -04:00
imdaniele
04640c43f2 android - use mReactContext instead of getCurrentActivity 2019-08-28 21:11:38 -04:00
19 changed files with 879 additions and 162 deletions

39
.travis.yml Normal file
View File

@ -0,0 +1,39 @@
env:
- NODE_ENV='test'
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
matrix:
include:
- language: objective-c
os: osx
xcode_sdk: iphonesimulator12.2
osx_image: xcode10.2
sudo: true
before_install:
- brew install node@10
- cd examples && make install # install node modules to get podfiles
install:
- cd ios && pod install && cd ..
script:
- make test-ios
- language: android
android:
components:
- tools
- platform-tools
- build-tools-29.0.2
- android-29
sudo: true
before_install:
- nvm install --lts
install: true
script:
- cd examples && make test-android
- language: node_js
node_js: lts/*
sudo: false
install:
- cd examples && npm ci
script: react-native bundle --entry-file index.js --bundle-output main.jsbundle

203
README.md
View File

@ -25,6 +25,10 @@ It provides a **Default View** that prompts the user to place a finger to the iP
</div>
### 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.
@ -36,11 +40,24 @@ Samsung and MeiZu's Fingerprint SDK supports most devices which system versions
## Installation
`$ npm install react-native-fingerprint-scanner --save`
```sh
$ npm install react-native-fingerprint-scanner --save
```
or
```sh
$ yarn add react-native-fingerprint-scanner
```
### Automatic Configuration
For RN >= 0.60
```sh
$ cd ios && pod install
```
`$ react-native link react-native-fingerprint-scanner`
For RN < 0.60, use react-native link to add the library to your project:
```sh
$ react-native link react-native-fingerprint-scanner
```
### Manual Configuration
@ -63,19 +80,32 @@ Samsung and MeiZu's Fingerprint SDK supports most devices which system versions
```
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
Add the following permissions to their respective files:
#### Android
In your `AndroidManifest.xml`:
API level 28+ (Uses Android native BiometricPrompt) ([Reference](https://developer.android.com/reference/android/Manifest.permission#USE_BIOMETRIC))
```xml
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
```
API level 23-28 (Uses Android native FingerprintCompat) [Reference](https://developer.android.com/reference/android/Manifest.permission#USE_FINGERPRINT))
```xml
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
```
// 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
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
```
#### iOS
In your `Info.plist`:
```xml
@ -87,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.**
```
@ -113,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
@ -154,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 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 (this.requiresLegacyAuthentication()) {
this.authLegacy();
} else {
this.authCurrent();
}
}
componentWillUnmount = () => {
FingerprintScanner.release();
}
requiresLegacyAuthentication() {
return Platform.Version < 23;
}
authCurrent() {
FingerprintScanner
.authenticate({ onAttempt: this.handleAuthenticationAttempted })
.authenticate({ title: '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 (
<View style={styles.container}>
@ -213,17 +277,17 @@ class FingerprintPopup extends Component {
/>
<Text style={styles.heading}>
Fingerprint{'\n'}Authentication
Biometric{'\n'}Authentication
</Text>
<ShakingText
ref={(instance) => { 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`}
</ShakingText>
<TouchableOpacity
style={styles.buttonContainer}
onPress={handlePopupDismissed}
onPress={handlePopupDismissedLegacy}
>
<Text style={styles.buttonText}>
BACK TO MAIN
@ -234,14 +298,25 @@ class FingerprintPopup extends Component {
</View>
);
}
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
@ -251,6 +326,8 @@ Checks if Fingerprint Scanner is able to be used by now.
- Returns a `Promise<string>`
- `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
@ -284,29 +361,62 @@ componentDidMount() {
}
```
### `authenticate({ onAttempt })`: (Android)
### `authenticate({ title="Log In", subTitle, description, cancelButton="Cancel", onAttempt=() => (null) })`: (Android)
Starts Fingerprint authentication on Android.
- Returns a `Promise`
- `title: String` the title text to display in the native Android popup
- `subTitle: String` the sub title text to display in the native Android popup
- `description: String` the description text to display in the native Android popup
- `cancelButton: String` the cancel button 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({ title: '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`
@ -318,11 +428,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`
@ -330,15 +440,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

View File

@ -3,25 +3,30 @@ def safeExtGet(prop, fallback) {
}
buildscript {
repositories {
google()
jcenter()
}
// The Android Gradle plugin is only required when opening the android folder stand-alone.
// This avoids unnecessary downloads and potential conflicts when the library is included as a
// module dependency in an application project.
if (project == rootProject) {
repositories {
google()
jcenter()
}
dependencies {
classpath("com.android.tools.build:gradle:3.4.1")
dependencies {
classpath("com.android.tools.build:gradle:3.5.1")
}
}
}
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"
}
@ -36,9 +41,13 @@ repositories {
}
dependencies {
compile 'com.facebook.react:react-native:+'
implementation '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)
compile "com.wei.android.lib:fingerprintidentify:${safeExtGet("fingerprintidentify", "1.2.6")}"
implementation "com.wei.android.lib:fingerprintidentify:${safeExtGet("fingerprintidentify", "1.2.6")}"
}

View File

@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hieuvp.fingerprint">
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="com.fingerprints.service.ACCESS_FINGERPRINT_MANAGER" />
<uses-permission
android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />

View File

@ -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,19 +65,200 @@ public class ReactNativeFingerprintScannerModule extends ReactContextBaseJavaMod
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 FragmentActivity fragmentActivity, 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);
Executor executor = Executors.newSingleThreadExecutor();
biometricPrompt = new BiometricPrompt(
fragmentActivity,
executor,
authCallback
);
return biometricPrompt;
}
private void biometricAuthenticate(final String title, final String subtitle, final String description, final String cancelButton, final Promise promise) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
FragmentActivity fragmentActivity = (FragmentActivity) mReactContext.getCurrentActivity();
if(fragmentActivity == null) return;
BiometricPrompt bioPrompt = getBiometricPrompt(fragmentActivity, promise);
PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setDeviceCredentialAllowed(false)
.setConfirmationRequired(false)
.setNegativeButtonText(cancelButton)
.setDescription(description)
.setSubtitle(subtitle)
.setTitle(title)
.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 "DeviceLockedPermanent";
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 title, String subtitle, String description, String cancelButton, 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(title, subtitle, description, cancelButton, 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;
}
mReactContext.addLifecycleEventListener(this);
mFingerprintIdentify = new FingerprintIdentify(getCurrentActivity());
mFingerprintIdentify = new FingerprintIdentify(mReactContext);
mFingerprintIdentify.setSupportAndroidL(true);
mFingerprintIdentify.setExceptionListener(
new ExceptionListener() {
@Override
public void onCatchException(Throwable exception) {
mReactContext.removeLifecycleEventListener(
ReactNativeFingerprintScannerModule.this);
mReactContext.removeLifecycleEventListener(ReactNativeFingerprintScannerModule.this);
}
}
);
@ -63,7 +266,7 @@ public class ReactNativeFingerprintScannerModule extends ReactContextBaseJavaMod
return mFingerprintIdentify;
}
private String getErrorMessage() {
private String legacyGetErrorMessage() {
if (!getFingerprintIdentify().isHardwareEnable()) {
return "FingerprintScannerNotSupported";
} else if (!getFingerprintIdentify().isRegisteredFingerprint()) {
@ -71,14 +274,15 @@ public class ReactNativeFingerprintScannerModule extends ReactContextBaseJavaMod
} 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;
}
@ -88,13 +292,18 @@ public class ReactNativeFingerprintScannerModule extends ReactContextBaseJavaMod
@Override
public void onSucceed() {
promise.resolve(true);
ReactNativeFingerprintScannerModule.this.release();
}
@Override
public void onNotMatch(int availableTimes) {
mReactContext.getJSModule(RCTDeviceEventEmitter.class)
.emit("FINGERPRINT_SCANNER_AUTHENTICATION", "AuthenticationNotMatch");
if (availableTimes <= 0) {
mReactContext.getJSModule(RCTDeviceEventEmitter.class)
.emit("FINGERPRINT_SCANNER_AUTHENTICATION", "DeviceLocked");
} else {
mReactContext.getJSModule(RCTDeviceEventEmitter.class)
.emit("FINGERPRINT_SCANNER_AUTHENTICATION", "AuthenticationNotMatch");
}
}
@Override
@ -102,7 +311,7 @@ public class ReactNativeFingerprintScannerModule extends ReactContextBaseJavaMod
if(isDeviceLocked){
promise.reject("AuthenticationFailed", "DeviceLocked");
} else {
promise.reject("AuthenticationFailed", TYPE_FINGERPRINT);
promise.reject("AuthenticationFailed", TYPE_FINGERPRINT_LEGACY);
}
ReactNativeFingerprintScannerModule.this.release();
}
@ -114,21 +323,4 @@ public class ReactNativeFingerprintScannerModule extends ReactContextBaseJavaMod
}
});
}
@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);
}
}
}

16
examples/Makefile Normal file
View File

@ -0,0 +1,16 @@
all: test-ios
install:
npm ci
clean:
cd android && ./gradlew clean
test-ios: install build-ios
build-ios:
cd ios && xcodebuild clean build -scheme examples \
-workspace examples.xcworkspace \
-configuration Debug -sdk iphonesimulator \
CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""
test-android: install build-android
build-android:
cd android && chmod +x ./gradlew && TERM=dumb ./gradlew assembleDebug --stacktrace
.PHONY: all

View File

@ -5,7 +5,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- API lvl 27- (Oreo MR1 and below) -->
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<!-- API lvl 28+ (Pie and above) -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-sdk
android:minSdkVersion="16"

View File

@ -2,11 +2,11 @@
buildscript {
ext {
buildToolsVersion = "28.0.3"
buildToolsVersion = "29.0.2"
minSdkVersion = 16
compileSdkVersion = 28
targetSdkVersion = 28
supportLibVersion = "28.0.0"
compileSdkVersion = 29
targetSdkVersion = 29
supportLibVersion = "29.0.0"
}
repositories {
google()

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -2189,7 +2189,8 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -2207,11 +2208,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -2224,15 +2227,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -2335,7 +2341,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -2345,6 +2352,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -2357,17 +2365,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -2384,6 +2395,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -2456,7 +2468,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -2466,6 +2479,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -2541,7 +2555,8 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true
"bundled": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -2571,6 +2586,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -2588,6 +2604,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -2626,11 +2643,13 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true
"bundled": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true
"bundled": true,
"optional": true
}
}
},
@ -3239,9 +3258,9 @@
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash.throttle": {
"version": "4.1.1",

View File

@ -1,50 +1,81 @@
import PropTypes from 'prop-types';
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 FingerprintScanner from 'react-native-fingerprint-scanner';
import styles from './FingerprintPopup.component.styles';
import ShakingText from './ShakingText.component';
class FingerprintPopup extends Component {
// Based on https://github.com/hieuvp/react-native-fingerprint-scanner/blob/master/examples/src/FingerprintPopup.component.android.js
// - 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, 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 (
<View style={styles.container}>
@ -60,13 +91,13 @@ class FingerprintPopup extends Component {
</Text>
<ShakingText
ref={(instance) => { 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`}
</ShakingText>
<TouchableOpacity
style={styles.buttonContainer}
onPress={handlePopupDismissed}
onPress={handlePopupDismissedLegacy}
>
<Text style={styles.buttonText}>
BACK TO MAIN
@ -77,11 +108,24 @@ class FingerprintPopup extends Component {
</View>
);
}
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;

173
index.d.ts vendored Normal file
View File

@ -0,0 +1,173 @@
export type AuthenticateIOS = {
description?: string;
fallbackEnabled?: boolean;
};
export type AuthenticateAndroid = {
title?: string;
subTitle?: string;
description?: string;
cancelButton?: string;
onAttempt?: (error: FingerprintScannerError) => void;
};
export type Biometrics = 'Touch ID' | 'Face ID' | 'Biometrics';
export type Errors =
| { name: 'AuthenticationNotMatch'; message: 'No match' }
| {
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';
}
| {
name: 'UserFallback';
message: 'Authentication was canceled because the user tapped the fallback button (Enter Password)';
}
| {
name: 'SystemCancel';
message: 'Authentication was canceled by system - e.g. if another application came to foreground while the authentication dialog was up';
}
| {
name: 'PasscodeNotSet';
message: 'Authentication could not start because the passcode is not set on the device';
}
| {
name: 'FingerprintScannerNotAvailable';
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';
}
| {
name: 'FingerprintScannerUnknownError';
message: 'Could not authenticate for an unknown reason';
}
| {
name: 'FingerprintScannerNotSupported';
message: 'Device does not support Fingerprint Scanner';
}
| {
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;
export interface FingerPrintProps {
/**
### release(): (Android)
Stops fingerprint scanner listener, releases cache of internal state in native code.
- Returns a `void`
-------------------
Example
```
componentWillUnmount() {
FingerprintScanner.release();
}
```
*/
release: () => void;
/**
### isSensorAvailable(): (Android, iOS)
Checks if Fingerprint Scanner is able to be used by now.
- Returns a `Promise<Biometrics>`
- `biometryType`: *String* - The type of biometric authentication supported by the device.
- `error: FingerprintScannerError { name, message, biometric }` - The name and message of failure and the biometric type in use.
-------------
Example
```
FingerprintScanner
.isSensorAvailable()
.then(biometryType => this.setState({ biometryType }))
.catch(error => this.setState({ errorMessage: error.message }));
```
------------
*/
isSensorAvailable: () => Promise<Biometrics>;
/**
### authenticate({ description, fallbackEnabled }): (iOS)
- Returns a `Promise`
- `description: String` - the string to explain the request for user authentication.
- `fallbackEnabled: Boolean` - default to ***true***, whether to display fallback button (e.g. Enter Password).
----------------
- Example:
```
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();
AlertIOS.alert(error.message);
});
```
-----------------
### 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({
description: 'Log in with Biometrics',
onAttempt: this.handleAuthenticationAttempted,
})
.then(() => {
this.props.handlePopupDismissed();
Alert.alert('Fingerprint Authentication', 'Authenticated successfully');
})
.catch((error) => {
this.setState({ errorMessage: error.message });
this.description.shake();
});
```
-----------------
*/
authenticate: (
platformProps: AuthenticateIOS | AuthenticateAndroid
) => Promise<void>;
}
declare const FingerprintScanner: FingerPrintProps;
export default FingerprintScanner;

View File

@ -22,16 +22,26 @@ RCT_EXPORT_METHOD(isSensorAvailable: (RCTResponseSenderBlock)callback)
NSString *message;
switch (error.code) {
case LAErrorTouchIDNotAvailable:
case LAErrorBiometryNotAvailable:
code = @"FingerprintScannerNotAvailable";
message = [self getBiometryType:context];
break;
case LAErrorTouchIDNotEnrolled:
case LAErrorBiometryNotEnrolled:
code = @"FingerprintScannerNotEnrolled";
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";
message = nil;
@ -54,11 +64,12 @@ RCT_EXPORT_METHOD(authenticate: (NSString *)reason
if (!fallbackEnabled) {
context.localizedFallbackTitle = @"";
}
__auto_type policy = fallbackEnabled ? LAPolicyDeviceOwnerAuthentication : LAPolicyDeviceOwnerAuthenticationWithBiometrics;
// Device has FingerprintScanner
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
if ([context canEvaluatePolicy:policy error:&error]) {
// Attempt Authentication
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
[context evaluatePolicy:policy
localizedReason:reason
reply:^(BOOL success, NSError *error)
{
@ -87,14 +98,18 @@ RCT_EXPORT_METHOD(authenticate: (NSString *)reason
errorReason = @"PasscodeNotSet";
break;
case LAErrorTouchIDNotAvailable:
case LAErrorBiometryNotAvailable:
errorReason = @"FingerprintScannerNotAvailable";
break;
case LAErrorTouchIDNotEnrolled:
case LAErrorBiometryNotEnrolled:
errorReason = @"FingerprintScannerNotEnrolled";
break;
case LAErrorBiometryLockout:
errorReason = @"DeviceLockedPermanent";
break;
default:
errorReason = @"FingerprintScannerUnknownError";
break;
@ -115,6 +130,35 @@ RCT_EXPORT_METHOD(authenticate: (NSString *)reason
}];
} else {
if (error) {
NSString *errorReason;
switch (error.code) {
case LAErrorBiometryNotAvailable:
errorReason = @"FingerprintScannerNotAvailable";
break;
case LAErrorBiometryNotEnrolled:
errorReason = @"FingerprintScannerNotEnrolled";
break;
case LAErrorBiometryLockout:
errorReason = @"DeviceLockedPermanent";
break;
case LAErrorPasscodeNotSet:
errorReason = @"PasscodeNotSet";
break;
default:
errorReason = @"FingerprintScannerNotSupported";
break;
}
NSLog(@"Authentication failed: %@", errorReason);
callback(@[RCTJSErrorFromCodeMessageAndNSError(errorReason, errorReason, nil)]);
return;
}
// Device does not support FingerprintScanner
callback(@[RCTJSErrorFromCodeMessageAndNSError(@"FingerprintScannerNotSupported", @"FingerprintScannerNotSupported", nil)]);
return;

2
package-lock.json generated
View File

@ -1,5 +1,5 @@
{
"name": "react-native-fingerprint-scanner",
"version": "2.6.1",
"version": "5.0.0",
"lockfileVersion": 1
}

View File

@ -1,7 +1,7 @@
{
"name": "react-native-fingerprint-scanner",
"version": "3.0.0",
"description": "React Native Fingerprint Scanner for Android and iOS",
"version": "6.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"
@ -25,6 +28,7 @@
"repository": "https://github.com/hieuvp/react-native-fingerprint-scanner.git",
"author": "Hieu Van (https://github.com/hieuvp)",
"license": "MIT",
"types": "index.d.ts",
"scripts": {
"android": "cd examples && react-native run-android",
"ios": "cd examples && react-native run-ios",

View File

@ -1,24 +1,66 @@
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 = (title, subTitle, description, cancelButton, resolve, reject) => {
ReactNativeFingerprintScanner.authenticate(title, subTitle, description, cancelButton)
.then(() => {
resolve(true);
})
.catch((error) => {
// translate errors
reject(createError(error.code, error.message));
});
}
ReactNativeFingerprintScanner.authenticate()
.then(() => {
DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION');
resolve(true);
})
.catch((error) => {
DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION');
reject(createError(error.code, error.message));
});
const authLegacy = (onAttempt, resolve, reject) => {
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 ({ title, subTitle, description, cancelButton, onAttempt }) => {
return new Promise((resolve, reject) => {
if (!title) {
title = description ? description : "Log In";
description = ""
}
if (!subTitle) {
subTitle = "";
}
if (!description) {
description = "";
}
if (!cancelButton) {
cancelButton = "Cancel";
}
if (!onAttempt) {
onAttempt = nullOnAttempt;
}
if (Platform.Version < 23) {
return authLegacy(onAttempt, resolve, reject);
}
return authCurrent(title, subTitle, description, cancelButton, resolve, reject);
});
}

View File

@ -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 {

View File

@ -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();
}