Skip to content

Commit 2bb8b1a

Browse files
authored
Merge pull request #22 from smallcase/device-passcode-biometrics-fallback
feat(device-passcode-biometrics-fallback): add device passcode as fallback incase biometrics aren't enabled
2 parents 0b4a6bc + 32b171d commit 2bb8b1a

File tree

6 files changed

+185
-63
lines changed

6 files changed

+185
-63
lines changed

.github/pull_request_template.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
### Description
2+
3+
Provide a brief description of the problem this PR solves and the solution implemented
4+
5+
### Problem
6+
7+
Describe the issue, bug, or enhancement you're addressing
8+
9+
### Solution
10+
11+
Explain how you've implemented the fix or feature
12+
13+
### Manual Testing Checklist
14+
15+
- [ ] Tested on iOS device/simulator
16+
- [ ] Tested on Android device/emulator
17+
18+
### Screen Recordings
19+
20+
Please attach screen recordings demonstrating the functionality on both platforms
21+
22+
### Breaking Changes
23+
24+
Document any breaking changes to the API
25+
26+
### Related Issues
27+
28+
Link to any related issues
29+
30+
### Additional Notes
31+
32+
Any additional information that reviewers should know
33+
34+
---
35+
36+
**Note**: Please ensure all screen recordings are uploaded and accessible to reviewers. If you're unable to provide screen recordings, please explain why and provide alternative testing evidence.

README.md

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
React Native Simple Biometrics is a straightforward and minimalistic React Native package designed to provide developers with an API for implementing user authentication using on-device biometrics. This library facilitates the quick verification of the app's user, ensuring that sensitive information is only accessible to authorized individuals, such as the phone owner or a trustee.
88

9-
![demo](./demo.gif?raw=true "demo")
9+
![demo](./demo.gif?raw=true 'demo')
1010

1111
## Installation
1212

@@ -18,7 +18,7 @@ $ yarn add react-native-simple-biometrics
1818

1919
## Minimum Requirements
2020

21-
- iOS target: `8.0`
21+
- iOS target: `10.0`
2222
- Android minSdkVersion: `21`
2323

2424
## iOS Permission
@@ -36,21 +36,55 @@ When you call the `authenticate` function, iOS users will be automatically promp
3636

3737
React Native Simple Biometrics offers two main methods:
3838

39-
1. `canAuthenticate()`: Checks whether the device supports biometric authentication. Returns `true` if the hardware is available or if permission for Face ID (iOS) was granted.
39+
1. `canAuthenticate(options?: Options)`: Checks whether the device supports biometric authentication. Returns `true` if the hardware is available or if permission for Face ID (iOS) was granted.
4040

41-
2. `requestBioAuth(promptTitle: string, promptMessage: string)`: Initiates the biometric authentication process, displaying a user-friendly prompt with the specified title and message. This function can be used for user authentication.
41+
Parameters
42+
43+
- `options` (optional): An object containing configuration options
44+
- `allowDeviceCredentials` (boolean, default: true): Whether to allow device credentials (passcode/password) as a fallback when biometric authentication is not available
45+
46+
Return Value
47+
48+
Returns a Promise<boolean> that resolves to:
49+
50+
- true if authentication is possible with the specified options
51+
- false if authentication is not possible
52+
53+
2. `requestBioAuth(promptTitle: string, promptMessage: string, options?: Options)`: Initiates the biometric authentication process, displaying a user-friendly prompt with the specified title and message. This function can be used for user authentication.
54+
55+
Required Parameters
56+
57+
- `promptTitle` (string): The title displayed in the authentication dialog
58+
Must be a non-empty string
59+
Throws an error if not provided or empty
60+
- `promptMessage` (string): The subtitle/reason for requesting authentication
61+
Must be a non-empty string
62+
Throws an error if not provided or empty
63+
Displays in the authentication dialog to explain why authentication is needed
64+
65+
Optional Parameters
66+
67+
- `options` (object, optional): Configuration options
68+
- `allowDeviceCredentials` (boolean, default: true): Whether to allow device credentials (passcode/password) as a fallback when biometric authentication is not available
69+
70+
Return Value
71+
72+
Returns a Promise<boolean> that:
73+
74+
- Resolves to true when authentication is successful
75+
- Rejects with an error when authentication fails or is cancelled
4276

4377
Here's a code snippet demonstrating how to use these methods:
4478

4579
```javascript
46-
import RNBiometrics from "react-native-simple-biometrics";
80+
import RNBiometrics from 'react-native-simple-biometrics';
4781

48-
// Check if biometric authentication is available
82+
// Check if biometric authentication is available, will fallback to device passcode by default if not
4983
const can = await RNBiometrics.canAuthenticate();
5084

5185
if (can) {
5286
try {
53-
await RNBiometrics.requestBioAuth("prompt-title", "prompt-message");
87+
await RNBiometrics.requestBioAuth('prompt-title', 'prompt-message');
5488
// Code to execute when authenticated
5589
// ...
5690
} catch (error) {
@@ -62,4 +96,4 @@ if (can) {
6296

6397
## Credits
6498

65-
React Native Simple Biometrics is a simplified version of [react-native-biometrics](https://www.npmjs.com/package/react-native-biometrics). If you require advanced features such as key generation, signatures, and more, consider using react-native-biometrics.
99+
React Native Simple Biometrics is a simplified version of [react-native-biometrics](https://www.npmjs.com/package/react-native-biometrics). If you require advanced features such as key generation, signatures, and more, consider using react-native-biometrics.

android/src/main/java/com/reactnativesimplebiometrics/SimpleBiometricsModule.java

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.reactnativesimplebiometrics;
22

33
import android.app.Activity;
4+
45
import androidx.annotation.NonNull;
56

67
import java.util.concurrent.Executor;
8+
79
import com.facebook.react.bridge.Promise;
810
import com.facebook.react.bridge.ReactMethod;
911
import com.facebook.react.bridge.UiThreadUtil;
@@ -16,16 +18,10 @@
1618
import androidx.core.content.ContextCompat;
1719
import androidx.fragment.app.FragmentActivity;
1820

19-
20-
2121
@ReactModule(name = SimpleBiometricsModule.NAME)
2222
public class SimpleBiometricsModule extends ReactContextBaseJavaModule {
2323
public static final String NAME = "SimpleBiometrics";
2424

25-
static final int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG
26-
| BiometricManager.Authenticators.BIOMETRIC_WEAK
27-
| BiometricManager.Authenticators.DEVICE_CREDENTIAL;
28-
2925
public SimpleBiometricsModule(ReactApplicationContext reactContext) {
3026
super(reactContext);
3127
}
@@ -36,12 +32,27 @@ public String getName() {
3632
return NAME;
3733
}
3834

35+
/**
36+
* Helper to choose allowed authenticators depending on API level and JS param.
37+
*/
38+
private int getAllowedAuthenticators(boolean allowDeviceCredentials) {
39+
if (allowDeviceCredentials) {
40+
return BiometricManager.Authenticators.BIOMETRIC_STRONG |
41+
BiometricManager.Authenticators.BIOMETRIC_WEAK |
42+
BiometricManager.Authenticators.DEVICE_CREDENTIAL;
43+
}
44+
// Default to biometrics only
45+
return BiometricManager.Authenticators.BIOMETRIC_STRONG |
46+
BiometricManager.Authenticators.BIOMETRIC_WEAK;
47+
}
48+
3949
@ReactMethod
40-
public void canAuthenticate(Promise promise) {
50+
public void canAuthenticate(boolean allowDeviceCredentials, Promise promise) {
4151
try {
4252
ReactApplicationContext context = getReactApplicationContext();
4353
BiometricManager biometricManager = BiometricManager.from(context);
4454

55+
int authenticators = getAllowedAuthenticators(allowDeviceCredentials);
4556
int res = biometricManager.canAuthenticate(authenticators);
4657
boolean can = res == BiometricManager.BIOMETRIC_SUCCESS;
4758

@@ -52,7 +63,8 @@ public void canAuthenticate(Promise promise) {
5263
}
5364

5465
@ReactMethod
55-
public void requestBioAuth(final String title, final String subtitle, final Promise promise) {
66+
public void requestBioAuth(final String title, final String subtitle, final boolean allowDeviceCredentials,
67+
final Promise promise) {
5668
UiThreadUtil.runOnUiThread(
5769
new Runnable() {
5870
@Override
@@ -69,15 +81,18 @@ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString
6981
}
7082

7183
@Override
72-
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
84+
public void onAuthenticationSucceeded(
85+
@NonNull BiometricPrompt.AuthenticationResult result) {
7386
super.onAuthenticationSucceeded(result);
7487
promise.resolve(true);
7588
}
7689
};
7790

7891
if (activity != null) {
79-
BiometricPrompt prompt = new BiometricPrompt((FragmentActivity) activity, mainExecutor, authenticationCallback);
92+
BiometricPrompt prompt = new BiometricPrompt((FragmentActivity) activity, mainExecutor,
93+
authenticationCallback);
8094

95+
int authenticators = getAllowedAuthenticators(allowDeviceCredentials);
8196
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
8297
.setAllowedAuthenticators(authenticators)
8398
.setTitle(title)
@@ -92,8 +107,7 @@ public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationRes
92107
promise.reject(e);
93108
}
94109
}
95-
}
96-
);
110+
});
97111

98112
}
99113
}

ios/SimpleBiometrics.mm

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,58 +10,80 @@
1010
@implementation SimpleBiometrics
1111
RCT_EXPORT_MODULE()
1212

13-
RCT_REMAP_METHOD(canAuthenticate,
14-
canAuthenticateWithResolver:(RCTPromiseResolveBlock)resolve
15-
rejecter:(RCTPromiseRejectBlock)reject)
16-
{
17-
LAContext *context = [[LAContext alloc] init];
18-
NSError *la_error = nil;
19-
BOOL canEvaluatePolicy = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&la_error];
20-
21-
if (canEvaluatePolicy) {
22-
resolve(@(YES));
23-
} else {
24-
resolve(@(NO));
25-
}
13+
- (LAPolicy)getLocalAuthPolicy:(BOOL)allowDeviceCredentials {
14+
if (allowDeviceCredentials) {
15+
// LAPolicyDeviceOwnerAuthentication allows authentication using
16+
// biometrics (Face ID/Touch ID) or device passcode.
17+
// If biometry is available, enrolled, and not disabled, the system
18+
// uses that first. When these options aren’t available, the system
19+
// prompts the user for the device passcode or user’s password.
20+
return LAPolicyDeviceOwnerAuthentication;
21+
} else {
22+
// LAPolicyDeviceOwnerAuthenticationWithBiometrics policy evaluation
23+
// fails if Touch ID or Face ID is unavailable or not enrolled.
24+
return LAPolicyDeviceOwnerAuthenticationWithBiometrics;
25+
}
2626
}
2727

28-
RCT_REMAP_METHOD(requestBioAuth,
29-
title:(NSString *)title
30-
subtitle:(NSString *)subtitle
31-
requestBioAuthWithResolver:(RCTPromiseResolveBlock)resolve
32-
rejecter:(RCTPromiseRejectBlock)reject)
33-
{
28+
RCT_REMAP_METHOD(canAuthenticate, allowDeviceCredentials
29+
: (BOOL)allowDeviceCredentials canAuthenticateWithResolver
30+
: (RCTPromiseResolveBlock)resolve rejecter
31+
: (RCTPromiseRejectBlock)reject) {
32+
LAContext *context = [[LAContext alloc] init];
33+
NSError *la_error = nil;
3434

35-
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
35+
LAPolicy localAuthPolicy = [self getLocalAuthPolicy:allowDeviceCredentials];
36+
37+
BOOL canEvaluatePolicy = [context canEvaluatePolicy:localAuthPolicy
38+
error:&la_error];
39+
40+
if (canEvaluatePolicy) {
41+
resolve(@(YES));
42+
} else {
43+
resolve(@(NO));
44+
}
45+
}
46+
47+
RCT_REMAP_METHOD(requestBioAuth, title
48+
: (NSString *)title subtitle
49+
: (NSString *)subtitle allowDeviceCredentials
50+
: (BOOL)allowDeviceCredentials requestBioAuthWithResolver
51+
: (RCTPromiseResolveBlock)resolve rejecter
52+
: (RCTPromiseRejectBlock)reject) {
53+
54+
dispatch_async(
55+
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
3656
NSString *promptMessage = subtitle;
3757

3858
LAContext *context = [[LAContext alloc] init];
3959
context.localizedFallbackTitle = nil;
4060

41-
LAPolicy localAuthPolicy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
42-
if (![[UIDevice currentDevice].systemVersion hasPrefix:@"8."]) {
43-
localAuthPolicy = LAPolicyDeviceOwnerAuthentication;
44-
}
45-
46-
[context evaluatePolicy:localAuthPolicy localizedReason:promptMessage reply:^(BOOL success, NSError *biometricError) {
47-
if (success) {
48-
resolve( @(YES));
61+
LAPolicy localAuthPolicy =
62+
[self getLocalAuthPolicy:allowDeviceCredentials];
4963

50-
} else {
51-
NSString *message = [NSString stringWithFormat:@"%@", biometricError.localizedDescription];
52-
reject(@"biometric_error", message, nil);
53-
}
54-
}];
55-
});
64+
[context evaluatePolicy:localAuthPolicy
65+
localizedReason:promptMessage
66+
reply:^(BOOL success, NSError *biometricError) {
67+
if (success) {
68+
resolve(@(YES));
5669

70+
} else {
71+
NSString *message = [NSString
72+
stringWithFormat:@"%@",
73+
biometricError
74+
.localizedDescription];
75+
reject(@"biometric_error", message, nil);
76+
}
77+
}];
78+
});
5779
}
5880

5981
// Don't compile this code when we build for the old architecture.
6082
#ifdef RCT_NEW_ARCH_ENABLED
6183
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
62-
(const facebook::react::ObjCTurboModule::InitParams &)params
63-
{
64-
return std::make_shared<facebook::react::NativeSimpleBiometricsSpecJSI>(params);
84+
(const facebook::react::ObjCTurboModule::InitParams &)params {
85+
return std::make_shared<facebook::react::NativeSimpleBiometricsSpecJSI>(
86+
params);
6587
}
6688
#endif
6789

ios/SimpleBiometrics.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@
176176
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
177177
GCC_WARN_UNUSED_FUNCTION = YES;
178178
GCC_WARN_UNUSED_VARIABLE = YES;
179-
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
179+
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
180180
MTL_ENABLE_DEBUG_INFO = YES;
181181
ONLY_ACTIVE_ARCH = YES;
182182
SDKROOT = iphoneos;
@@ -220,7 +220,7 @@
220220
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
221221
GCC_WARN_UNUSED_FUNCTION = YES;
222222
GCC_WARN_UNUSED_VARIABLE = YES;
223-
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
223+
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
224224
MTL_ENABLE_DEBUG_INFO = NO;
225225
SDKROOT = iphoneos;
226226
VALIDATE_PRODUCT = YES;

0 commit comments

Comments
 (0)