Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS Only] Support Passcode Fallback Natively #101

Merged
merged 9 commits into from
Oct 27, 2018
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ There's excellent documentation on how to do this in the [React Native Docs](htt

iOS and Android differ slightly in their TouchID authentication.

On Android you can customize the title and color of the pop-up by passing in the **optional config object** with a color and title key to the `authenticate` method. Even if you pass in the config object, iOS **does not** allow you change the color nor the title of the pop-up.
On Android you can customize the title and color of the pop-up by passing in the **optional config object** with a color and title key to the `authenticate` method. Even if you pass in the config object, iOS **does not** allow you change the color nor the title of the pop-up. iOS does support `passcodeFallback` as an option, which when set to `true` will allow users to use their device pin - useful for people with Face / Touch ID disabled.

Error handling is also different between the platforms, with iOS currently providing much more descriptive error codes.

Expand Down Expand Up @@ -151,6 +151,7 @@ __Arguments__
- `cancelText` - **Android** - cancel button text
- `fallbackLabel` - **iOS** - by default specified 'Show Password' label. If set to empty string label is invisible.
- `unifiedErrors` - return unified error messages (see below) (default = false)
- `passcodeFallback` - **iOS** - by default set to false. If set to true, will allow use of keypad passcode.


__Examples__
Expand All @@ -161,7 +162,8 @@ const optionalConfigObject = {
sensorDescription: "Touch sensor", // Android
cancelText: "Cancel", // Android
fallbackLabel: "Show Passcode", // iOS (if empty, then label is hidden)
unifiedErrors: false // use unified error messages (default false)
unifiedErrors: false, // use unified error messages (default false)
passcodeFallback: false // iOS
}

TouchID.authenticate('to demo this react-native component', optionalConfigObject)
Expand Down
3 changes: 2 additions & 1 deletion TouchID.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export default {
authenticate(reason, config) {
const DEFAULT_CONFIG = {
fallbackLabel: null,
unifiedErrors: false
unifiedErrors: false,
passcodeFallback: false
};
const authReason = reason ? reason : ' ';
const authConfig = Object.assign({}, DEFAULT_CONFIG, config);
Expand Down
130 changes: 79 additions & 51 deletions TouchID.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ @implementation TouchID

if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
callback(@[[NSNull null], [self getBiometryType:context]]);
// Device does not support TouchID
} else {

} else if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&error]) {
callback(@[[NSNull null], [self getBiometryType:context]]);
}
// Device does not support FaceID / TouchID / Pin
else {
callback(@[RCTMakeError(@"RCTTouchIDNotSupported", nil, nil)]);
return;
}
Expand All @@ -24,6 +28,7 @@ @implementation TouchID
options:(NSDictionary *)options
callback: (RCTResponseSenderBlock)callback)
{
NSNumber *passcodeFallback = [NSNumber numberWithBool:false];
LAContext *context = [[LAContext alloc] init];
NSError *error;

Expand All @@ -32,75 +37,98 @@ @implementation TouchID
context.localizedFallbackTitle = fallbackLabel;
}

if (RCTNilIfNull([options objectForKey:@"passcodeFallback"]) != nil) {
passcodeFallback = [RCTConvert NSNumber:options[@"passcodeFallback"]];
}

// Device has TouchID
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
// Attempt Authentification
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:reason
reply:^(BOOL success, NSError *error)
{
if (success) { // Authentication Successful
callback(@[[NSNull null], @"Authenticated with Touch ID."]);
} else if (error) { // Authentication Error
NSString *errorReason;

switch (error.code) {
case LAErrorAuthenticationFailed:
errorReason = @"LAErrorAuthenticationFailed";
break;

case LAErrorUserCancel:
errorReason = @"LAErrorUserCancel";
break;

case LAErrorUserFallback:
errorReason = @"LAErrorUserFallback";
break;

case LAErrorSystemCancel:
errorReason = @"LAErrorSystemCancel";
break;

case LAErrorPasscodeNotSet:
errorReason = @"LAErrorPasscodeNotSet";
break;

case LAErrorTouchIDNotAvailable:
errorReason = @"LAErrorTouchIDNotAvailable";
break;

case LAErrorTouchIDNotEnrolled:
errorReason = @"LAErrorTouchIDNotEnrolled";
break;

default:
errorReason = @"RCTTouchIDUnknownError";
break;
}

NSLog(@"Authentication failed: %@", errorReason);
callback(@[RCTMakeError(errorReason, nil, nil)]);
} else { // Authentication Failure
callback(@[RCTMakeError(@"LAErrorAuthenticationFailed", nil, nil)]);
}
[self handleAttemptToUseDeviceIDWithSuccess:success error:error callback:callback];
}];

// Device does not support TouchID
} else {
// Device does not support TouchID but user wishes to use passcode fallback
} else if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&error] && [passcodeFallback boolValue]) {
// Attempt Authentification
[context evaluatePolicy:LAPolicyDeviceOwnerAuthentication
localizedReason:reason
reply:^(BOOL success, NSError *error)
{
[self handleAttemptToUseDeviceIDWithSuccess:success error:error callback:callback];
}];
}
else {
callback(@[RCTMakeError(@"RCTTouchIDNotSupported", nil, nil)]);
return;
}
}

- (void)handleAttemptToUseDeviceIDWithSuccess:(BOOL)success error:(NSError *)error callback:(RCTResponseSenderBlock)callback {
if (success) { // Authentication Successful
callback(@[[NSNull null], @"Authenticated with Touch ID."]);
} else if (error) { // Authentication Error
NSString *errorReason;

switch (error.code) {
case LAErrorAuthenticationFailed:
errorReason = @"LAErrorAuthenticationFailed";
break;

case LAErrorUserCancel:
errorReason = @"LAErrorUserCancel";
break;

case LAErrorUserFallback:
errorReason = @"LAErrorUserFallback";
break;

case LAErrorSystemCancel:
errorReason = @"LAErrorSystemCancel";
break;

case LAErrorPasscodeNotSet:
errorReason = @"LAErrorPasscodeNotSet";
break;

case LAErrorTouchIDNotAvailable:
errorReason = @"LAErrorTouchIDNotAvailable";
break;

case LAErrorTouchIDNotEnrolled:
errorReason = @"LAErrorTouchIDNotEnrolled";
break;

default:
errorReason = @"RCTTouchIDUnknownError";
break;
}

NSLog(@"Authentication failed: %@", errorReason);
callback(@[RCTMakeError(errorReason, nil, nil)]);
} else { // Authentication Failure
callback(@[RCTMakeError(@"LAErrorAuthenticationFailed", nil, nil)]);
}
}

- (NSString *)getBiometryType:(LAContext *)context
{
if (@available(iOS 11, *)) {
return (context.biometryType == LABiometryTypeFaceID) ? @"FaceID" : @"TouchID";
if (context.biometryType == LABiometryTypeFaceID) {
return @"FaceID";
}
else if (context.biometryType == LABiometryTypeTouchID) {
return @"TouchID";
}
else if (context.biometryType == LABiometryNone) {
return @"None";
}
}

return @"TouchID";
}

@end