Compare commits
44 Commits
test-travi
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce64467368 | ||
|
|
5988a07c31 | ||
|
|
910e563bb5 | ||
|
|
0d9f8219cb | ||
|
|
4cea388974 | ||
|
|
7e30ab9828 | ||
|
|
770465db16 | ||
|
|
94e26dd916 | ||
|
|
7b3f8e5501 | ||
|
|
4c7145aa7a | ||
|
|
ce0ec2c2f2 | ||
|
|
4a22b4dea9 | ||
|
|
57556a4f2f | ||
|
|
879babbd33 | ||
|
|
45b98e7646 | ||
|
|
256adff995 | ||
|
|
90e10d0aef | ||
|
|
8c811d8860 | ||
|
|
c3283a67bd | ||
|
|
af7f5e8a1a | ||
|
|
dfcfce6c74 | ||
|
|
09cd6ccf1e | ||
|
|
38b39a73ac | ||
|
|
426defd52a | ||
|
|
ee5b97bdd2 | ||
|
|
6078d0a792 | ||
|
|
f1963529ec | ||
|
|
937daae215 | ||
|
|
e38a26fbe6 | ||
|
|
b372c404c2 | ||
|
|
28a7e58194 | ||
|
|
938bce2484 | ||
|
|
c0065278b4 | ||
|
|
de8a5bd7df | ||
|
|
faa36ca188 | ||
|
|
4101590a71 | ||
|
|
4209ba62ee | ||
|
|
9a698e2a13 | ||
|
|
a1d25edca5 | ||
|
|
aa85e11a8e | ||
|
|
99a60de1fb | ||
|
|
6604f44098 | ||
|
|
9f1b37b836 | ||
|
|
04640c43f2 |
@ -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
|
||||
|
||||
203
README.md
203
README.md
@ -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
|
||||
|
||||
|
||||
@ -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")}"
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
47
examples/package-lock.json
generated
47
examples/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
173
index.d.ts
vendored
Normal 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;
|
||||
@ -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
2
package-lock.json
generated
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "react-native-fingerprint-scanner",
|
||||
"version": "3.0.0",
|
||||
"version": "5.0.0",
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@ -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",
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user