[local_auth] support localizedFallbackTitle in IOSAuthMessages (#3806)
Add support localizedFallbackTitle in IOSAuthMessages.
diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md
index d17eb4a..9387540 100644
--- a/packages/local_auth/local_auth/CHANGELOG.md
+++ b/packages/local_auth/local_auth/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.1.11
+
+* Adds support `localizedFallbackTitle` in authenticateWithBiometrics on iOS.
+
## 1.1.10
* Removes dependency on `meta`.
diff --git a/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m
index dc409da..3572524 100644
--- a/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m
+++ b/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m
@@ -186,4 +186,87 @@
[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
diff --git a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m
index c2dc9db..70113ef 100644
--- a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m
+++ b/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m
@@ -122,7 +122,9 @@
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]) {
@@ -146,7 +148,9 @@
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
@@ -176,6 +180,7 @@
case LAErrorPasscodeNotSet:
case LAErrorTouchIDNotAvailable:
case LAErrorTouchIDNotEnrolled:
+ case LAErrorUserFallback:
case LAErrorTouchIDLockout:
[self handleErrors:error flutterArguments:arguments withFlutterResult:result];
return;
diff --git a/packages/local_auth/local_auth/lib/auth_strings.dart b/packages/local_auth/local_auth/lib/auth_strings.dart
index 537340b..3e34659 100644
--- a/packages/local_auth/local_auth/lib/auth_strings.dart
+++ b/packages/local_auth/local_auth/lib/auth_strings.dart
@@ -68,12 +68,14 @@
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>{
@@ -82,6 +84,8 @@
'goToSettingDescriptionIOS':
goToSettingsDescription ?? iOSGoToSettingsDescription,
'okButton': cancelButton ?? iOSOkButton,
+ if (localizedFallbackTitle != null)
+ 'localizedFallbackTitle': localizedFallbackTitle!,
};
}
}
diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml
index cd9d0d9..78c79f4 100644
--- a/packages/local_auth/local_auth/pubspec.yaml
+++ b/packages/local_auth/local_auth/pubspec.yaml
@@ -3,7 +3,7 @@
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"
diff --git a/packages/local_auth/local_auth/test/local_auth_test.dart b/packages/local_auth/local_auth/test/local_auth_test.dart
index 758b9ce..3de9758 100644
--- a/packages/local_auth/local_auth/test/local_auth_test.dart
+++ b/packages/local_auth/local_auth/test/local_auth_test.dart
@@ -40,14 +40,28 @@
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,
+ },
+ ),
],
);
});
@@ -61,14 +75,45 @@
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',
+ }),
],
);
});
@@ -95,14 +140,25 @@
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,
+ }),
],
);
});
@@ -117,14 +173,25 @@
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,
+ }),
],
);
});
@@ -137,14 +204,17 @@
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,
+ }),
],
);
});
@@ -159,14 +229,25 @@
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,
+ }),
],
);
});