Skip to content

Commit

Permalink
[local_auth] support localizedFallbackTitle in IOSAuthMessages (flutt…
Browse files Browse the repository at this point in the history
…er#3806)

Add support localizedFallbackTitle in IOSAuthMessages.
  • Loading branch information
furaiev committed Feb 16, 2022
1 parent eb330b0 commit 9318efc
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 51 deletions.
4 changes: 4 additions & 0 deletions packages/local_auth/local_auth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.1.11

* Adds support `localizedFallbackTitle` in authenticateWithBiometrics on iOS.

## 1.1.10

* Removes dependency on `meta`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,87 @@ - (void)testFailedAuthWithoutBiometrics {
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testLocalizedFallbackTitle {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
NSString *reason = @"a reason";
NSString *localizedFallbackTitle = @"a title";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);

// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);

FlutterMethodCall *call =
[FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(NO),
@"localizedReason" : reason,
@"localizedFallbackTitle" : localizedFallbackTitle,
}];

XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]);
XCTAssertFalse([result boolValue]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testSkippedLocalizedFallbackTitle {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
NSString *reason = @"a reason";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);

// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(NO),
@"localizedReason" : reason,
}];

XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]);
XCTAssertFalse([result boolValue]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ - (void)authenticateWithBiometrics:(NSDictionary *)arguments
NSError *authError = nil;
self.lastCallArgs = nil;
self.lastResult = nil;
context.localizedFallbackTitle = @"";
context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null]
? nil
: arguments[@"localizedFallbackTitle"];

if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
error:&authError]) {
Expand All @@ -146,7 +148,9 @@ - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)
NSError *authError = nil;
_lastCallArgs = nil;
_lastResult = nil;
context.localizedFallbackTitle = @"";
context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null]
? nil
: arguments[@"localizedFallbackTitle"];

if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) {
[context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication
Expand Down Expand Up @@ -176,6 +180,7 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success
case LAErrorPasscodeNotSet:
case LAErrorTouchIDNotAvailable:
case LAErrorTouchIDNotEnrolled:
case LAErrorUserFallback:
case LAErrorTouchIDLockout:
[self handleErrors:error flutterArguments:arguments withFlutterResult:result];
return;
Expand Down
4 changes: 4 additions & 0 deletions packages/local_auth/local_auth/lib/auth_strings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,14 @@ class IOSAuthMessages {
this.goToSettingsButton,
this.goToSettingsDescription,
this.cancelButton,
this.localizedFallbackTitle,
});

final String? lockOut;
final String? goToSettingsButton;
final String? goToSettingsDescription;
final String? cancelButton;
final String? localizedFallbackTitle;

Map<String, String> get args {
return <String, String>{
Expand All @@ -82,6 +84,8 @@ class IOSAuthMessages {
'goToSettingDescriptionIOS':
goToSettingsDescription ?? iOSGoToSettingsDescription,
'okButton': cancelButton ?? iOSOkButton,
if (localizedFallbackTitle != null)
'localizedFallbackTitle': localizedFallbackTitle!,
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/local_auth/local_auth/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for Android and iOS devices to allow local
authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern.
repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22
version: 1.1.10
version: 1.1.11

environment:
sdk: ">=2.14.0 <3.0.0"
Expand Down
177 changes: 129 additions & 48 deletions packages/local_auth/local_auth/test/local_auth_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,28 @@ void main() {
expect(
log,
<Matcher>[
isMethodCall('authenticate',
arguments: <String, dynamic>{
'localizedReason': 'Needs secure',
'useErrorDialogs': true,
'stickyAuth': false,
'sensitiveTransaction': true,
'biometricOnly': true,
}..addAll(const AndroidAuthMessages().args)),
isMethodCall(
'authenticate',
arguments: <String, dynamic>{
'localizedReason': 'Needs secure',
'useErrorDialogs': true,
'stickyAuth': false,
'sensitiveTransaction': true,
'biometricOnly': true,
'biometricHint': androidBiometricHint,
'biometricNotRecognized': androidBiometricNotRecognized,
'biometricSuccess': androidBiometricSuccess,
'biometricRequired': androidBiometricRequiredTitle,
'cancelButton': androidCancelButton,
'deviceCredentialsRequired':
androidDeviceCredentialsRequiredTitle,
'deviceCredentialsSetupDescription':
androidDeviceCredentialsSetupDescription,
'goToSetting': goToSettings,
'goToSettingDescription': androidGoToSettingsDescription,
'signInTitle': androidSignInTitle,
},
),
],
);
});
Expand All @@ -61,14 +75,45 @@ void main() {
expect(
log,
<Matcher>[
isMethodCall('authenticate',
arguments: <String, dynamic>{
'localizedReason': 'Needs secure',
'useErrorDialogs': true,
'stickyAuth': false,
'sensitiveTransaction': true,
'biometricOnly': true,
}..addAll(const IOSAuthMessages().args)),
isMethodCall('authenticate', arguments: <String, dynamic>{
'localizedReason': 'Needs secure',
'useErrorDialogs': true,
'stickyAuth': false,
'sensitiveTransaction': true,
'biometricOnly': true,
'lockOut': iOSLockOut,
'goToSetting': goToSettings,
'goToSettingDescriptionIOS': iOSGoToSettingsDescription,
'okButton': iOSOkButton,
}),
],
);
});

test('authenticate with `localizedFallbackTitle` on iOS.', () async {
const IOSAuthMessages iosAuthMessages =
IOSAuthMessages(localizedFallbackTitle: 'Enter PIN');
setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios'));
await localAuthentication.authenticate(
localizedReason: 'Needs secure',
biometricOnly: true,
iOSAuthStrings: iosAuthMessages,
);
expect(
log,
<Matcher>[
isMethodCall('authenticate', arguments: <String, dynamic>{
'localizedReason': 'Needs secure',
'useErrorDialogs': true,
'stickyAuth': false,
'sensitiveTransaction': true,
'biometricOnly': true,
'lockOut': iOSLockOut,
'goToSetting': goToSettings,
'goToSettingDescriptionIOS': iOSGoToSettingsDescription,
'okButton': iOSOkButton,
'localizedFallbackTitle': 'Enter PIN',
}),
],
);
});
Expand All @@ -95,14 +140,25 @@ void main() {
expect(
log,
<Matcher>[
isMethodCall('authenticate',
arguments: <String, dynamic>{
'localizedReason': 'Insecure',
'useErrorDialogs': false,
'stickyAuth': false,
'sensitiveTransaction': false,
'biometricOnly': true,
}..addAll(const AndroidAuthMessages().args)),
isMethodCall('authenticate', arguments: <String, dynamic>{
'localizedReason': 'Insecure',
'useErrorDialogs': false,
'stickyAuth': false,
'sensitiveTransaction': false,
'biometricOnly': true,
'biometricHint': androidBiometricHint,
'biometricNotRecognized': androidBiometricNotRecognized,
'biometricSuccess': androidBiometricSuccess,
'biometricRequired': androidBiometricRequiredTitle,
'cancelButton': androidCancelButton,
'deviceCredentialsRequired':
androidDeviceCredentialsRequiredTitle,
'deviceCredentialsSetupDescription':
androidDeviceCredentialsSetupDescription,
'goToSetting': goToSettings,
'goToSettingDescription': androidGoToSettingsDescription,
'signInTitle': androidSignInTitle,
}),
],
);
});
Expand All @@ -117,14 +173,25 @@ void main() {
expect(
log,
<Matcher>[
isMethodCall('authenticate',
arguments: <String, dynamic>{
'localizedReason': 'Needs secure',
'useErrorDialogs': true,
'stickyAuth': false,
'sensitiveTransaction': true,
'biometricOnly': false,
}..addAll(const AndroidAuthMessages().args)),
isMethodCall('authenticate', arguments: <String, dynamic>{
'localizedReason': 'Needs secure',
'useErrorDialogs': true,
'stickyAuth': false,
'sensitiveTransaction': true,
'biometricOnly': false,
'biometricHint': androidBiometricHint,
'biometricNotRecognized': androidBiometricNotRecognized,
'biometricSuccess': androidBiometricSuccess,
'biometricRequired': androidBiometricRequiredTitle,
'cancelButton': androidCancelButton,
'deviceCredentialsRequired':
androidDeviceCredentialsRequiredTitle,
'deviceCredentialsSetupDescription':
androidDeviceCredentialsSetupDescription,
'goToSetting': goToSettings,
'goToSettingDescription': androidGoToSettingsDescription,
'signInTitle': androidSignInTitle,
}),
],
);
});
Expand All @@ -137,14 +204,17 @@ void main() {
expect(
log,
<Matcher>[
isMethodCall('authenticate',
arguments: <String, dynamic>{
'localizedReason': 'Needs secure',
'useErrorDialogs': true,
'stickyAuth': false,
'sensitiveTransaction': true,
'biometricOnly': false,
}..addAll(const IOSAuthMessages().args)),
isMethodCall('authenticate', arguments: <String, dynamic>{
'localizedReason': 'Needs secure',
'useErrorDialogs': true,
'stickyAuth': false,
'sensitiveTransaction': true,
'biometricOnly': false,
'lockOut': iOSLockOut,
'goToSetting': goToSettings,
'goToSettingDescriptionIOS': iOSGoToSettingsDescription,
'okButton': iOSOkButton,
}),
],
);
});
Expand All @@ -159,14 +229,25 @@ void main() {
expect(
log,
<Matcher>[
isMethodCall('authenticate',
arguments: <String, dynamic>{
'localizedReason': 'Insecure',
'useErrorDialogs': false,
'stickyAuth': false,
'sensitiveTransaction': false,
'biometricOnly': false,
}..addAll(const AndroidAuthMessages().args)),
isMethodCall('authenticate', arguments: <String, dynamic>{
'localizedReason': 'Insecure',
'useErrorDialogs': false,
'stickyAuth': false,
'sensitiveTransaction': false,
'biometricOnly': false,
'biometricHint': androidBiometricHint,
'biometricNotRecognized': androidBiometricNotRecognized,
'biometricSuccess': androidBiometricSuccess,
'biometricRequired': androidBiometricRequiredTitle,
'cancelButton': androidCancelButton,
'deviceCredentialsRequired':
androidDeviceCredentialsRequiredTitle,
'deviceCredentialsSetupDescription':
androidDeviceCredentialsSetupDescription,
'goToSetting': goToSettings,
'goToSettingDescription': androidGoToSettingsDescription,
'signInTitle': androidSignInTitle,
}),
],
);
});
Expand Down

0 comments on commit 9318efc

Please sign in to comment.