Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 9318efc

Browse files
authored
[local_auth] support localizedFallbackTitle in IOSAuthMessages (#3806)
Add support localizedFallbackTitle in IOSAuthMessages.
1 parent eb330b0 commit 9318efc

File tree

6 files changed

+228
-51
lines changed

6 files changed

+228
-51
lines changed

packages/local_auth/local_auth/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.1.11
2+
3+
* Adds support `localizedFallbackTitle` in authenticateWithBiometrics on iOS.
4+
15
## 1.1.10
26

37
* Removes dependency on `meta`.

packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,87 @@ - (void)testFailedAuthWithoutBiometrics {
186186
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
187187
}
188188

189+
- (void)testLocalizedFallbackTitle {
190+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
191+
id mockAuthContext = OCMClassMock([LAContext class]);
192+
plugin.authContextOverrides = @[ mockAuthContext ];
193+
194+
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
195+
NSString *reason = @"a reason";
196+
NSString *localizedFallbackTitle = @"a title";
197+
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
198+
199+
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
200+
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
201+
// a background thread.
202+
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
203+
void (^reply)(BOOL, NSError *);
204+
[invocation getArgument:&reply atIndex:4];
205+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
206+
reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]);
207+
});
208+
};
209+
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
210+
.andDo(backgroundThreadReplyCaller);
211+
212+
FlutterMethodCall *call =
213+
[FlutterMethodCall methodCallWithMethodName:@"authenticate"
214+
arguments:@{
215+
@"biometricOnly" : @(NO),
216+
@"localizedReason" : reason,
217+
@"localizedFallbackTitle" : localizedFallbackTitle,
218+
}];
219+
220+
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
221+
[plugin handleMethodCall:call
222+
result:^(id _Nullable result) {
223+
XCTAssertTrue([NSThread isMainThread]);
224+
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
225+
OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]);
226+
XCTAssertFalse([result boolValue]);
227+
[expectation fulfill];
228+
}];
229+
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
230+
}
231+
232+
- (void)testSkippedLocalizedFallbackTitle {
233+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
234+
id mockAuthContext = OCMClassMock([LAContext class]);
235+
plugin.authContextOverrides = @[ mockAuthContext ];
236+
237+
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
238+
NSString *reason = @"a reason";
239+
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
240+
241+
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
242+
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
243+
// a background thread.
244+
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
245+
void (^reply)(BOOL, NSError *);
246+
[invocation getArgument:&reply atIndex:4];
247+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
248+
reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]);
249+
});
250+
};
251+
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
252+
.andDo(backgroundThreadReplyCaller);
253+
254+
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
255+
arguments:@{
256+
@"biometricOnly" : @(NO),
257+
@"localizedReason" : reason,
258+
}];
259+
260+
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
261+
[plugin handleMethodCall:call
262+
result:^(id _Nullable result) {
263+
XCTAssertTrue([NSThread isMainThread]);
264+
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
265+
OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]);
266+
XCTAssertFalse([result boolValue]);
267+
[expectation fulfill];
268+
}];
269+
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
270+
}
271+
189272
@end

packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ - (void)authenticateWithBiometrics:(NSDictionary *)arguments
122122
NSError *authError = nil;
123123
self.lastCallArgs = nil;
124124
self.lastResult = nil;
125-
context.localizedFallbackTitle = @"";
125+
context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null]
126+
? nil
127+
: arguments[@"localizedFallbackTitle"];
126128

127129
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
128130
error:&authError]) {
@@ -146,7 +148,9 @@ - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)
146148
NSError *authError = nil;
147149
_lastCallArgs = nil;
148150
_lastResult = nil;
149-
context.localizedFallbackTitle = @"";
151+
context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null]
152+
? nil
153+
: arguments[@"localizedFallbackTitle"];
150154

151155
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) {
152156
[context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication
@@ -176,6 +180,7 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success
176180
case LAErrorPasscodeNotSet:
177181
case LAErrorTouchIDNotAvailable:
178182
case LAErrorTouchIDNotEnrolled:
183+
case LAErrorUserFallback:
179184
case LAErrorTouchIDLockout:
180185
[self handleErrors:error flutterArguments:arguments withFlutterResult:result];
181186
return;

packages/local_auth/local_auth/lib/auth_strings.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,14 @@ class IOSAuthMessages {
6868
this.goToSettingsButton,
6969
this.goToSettingsDescription,
7070
this.cancelButton,
71+
this.localizedFallbackTitle,
7172
});
7273

7374
final String? lockOut;
7475
final String? goToSettingsButton;
7576
final String? goToSettingsDescription;
7677
final String? cancelButton;
78+
final String? localizedFallbackTitle;
7779

7880
Map<String, String> get args {
7981
return <String, String>{
@@ -82,6 +84,8 @@ class IOSAuthMessages {
8284
'goToSettingDescriptionIOS':
8385
goToSettingsDescription ?? iOSGoToSettingsDescription,
8486
'okButton': cancelButton ?? iOSOkButton,
87+
if (localizedFallbackTitle != null)
88+
'localizedFallbackTitle': localizedFallbackTitle!,
8589
};
8690
}
8791
}

packages/local_auth/local_auth/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for Android and iOS devices to allow local
33
authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern.
44
repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22
6-
version: 1.1.10
6+
version: 1.1.11
77

88
environment:
99
sdk: ">=2.14.0 <3.0.0"

packages/local_auth/local_auth/test/local_auth_test.dart

Lines changed: 129 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,28 @@ void main() {
4040
expect(
4141
log,
4242
<Matcher>[
43-
isMethodCall('authenticate',
44-
arguments: <String, dynamic>{
45-
'localizedReason': 'Needs secure',
46-
'useErrorDialogs': true,
47-
'stickyAuth': false,
48-
'sensitiveTransaction': true,
49-
'biometricOnly': true,
50-
}..addAll(const AndroidAuthMessages().args)),
43+
isMethodCall(
44+
'authenticate',
45+
arguments: <String, dynamic>{
46+
'localizedReason': 'Needs secure',
47+
'useErrorDialogs': true,
48+
'stickyAuth': false,
49+
'sensitiveTransaction': true,
50+
'biometricOnly': true,
51+
'biometricHint': androidBiometricHint,
52+
'biometricNotRecognized': androidBiometricNotRecognized,
53+
'biometricSuccess': androidBiometricSuccess,
54+
'biometricRequired': androidBiometricRequiredTitle,
55+
'cancelButton': androidCancelButton,
56+
'deviceCredentialsRequired':
57+
androidDeviceCredentialsRequiredTitle,
58+
'deviceCredentialsSetupDescription':
59+
androidDeviceCredentialsSetupDescription,
60+
'goToSetting': goToSettings,
61+
'goToSettingDescription': androidGoToSettingsDescription,
62+
'signInTitle': androidSignInTitle,
63+
},
64+
),
5165
],
5266
);
5367
});
@@ -61,14 +75,45 @@ void main() {
6175
expect(
6276
log,
6377
<Matcher>[
64-
isMethodCall('authenticate',
65-
arguments: <String, dynamic>{
66-
'localizedReason': 'Needs secure',
67-
'useErrorDialogs': true,
68-
'stickyAuth': false,
69-
'sensitiveTransaction': true,
70-
'biometricOnly': true,
71-
}..addAll(const IOSAuthMessages().args)),
78+
isMethodCall('authenticate', arguments: <String, dynamic>{
79+
'localizedReason': 'Needs secure',
80+
'useErrorDialogs': true,
81+
'stickyAuth': false,
82+
'sensitiveTransaction': true,
83+
'biometricOnly': true,
84+
'lockOut': iOSLockOut,
85+
'goToSetting': goToSettings,
86+
'goToSettingDescriptionIOS': iOSGoToSettingsDescription,
87+
'okButton': iOSOkButton,
88+
}),
89+
],
90+
);
91+
});
92+
93+
test('authenticate with `localizedFallbackTitle` on iOS.', () async {
94+
const IOSAuthMessages iosAuthMessages =
95+
IOSAuthMessages(localizedFallbackTitle: 'Enter PIN');
96+
setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios'));
97+
await localAuthentication.authenticate(
98+
localizedReason: 'Needs secure',
99+
biometricOnly: true,
100+
iOSAuthStrings: iosAuthMessages,
101+
);
102+
expect(
103+
log,
104+
<Matcher>[
105+
isMethodCall('authenticate', arguments: <String, dynamic>{
106+
'localizedReason': 'Needs secure',
107+
'useErrorDialogs': true,
108+
'stickyAuth': false,
109+
'sensitiveTransaction': true,
110+
'biometricOnly': true,
111+
'lockOut': iOSLockOut,
112+
'goToSetting': goToSettings,
113+
'goToSettingDescriptionIOS': iOSGoToSettingsDescription,
114+
'okButton': iOSOkButton,
115+
'localizedFallbackTitle': 'Enter PIN',
116+
}),
72117
],
73118
);
74119
});
@@ -95,14 +140,25 @@ void main() {
95140
expect(
96141
log,
97142
<Matcher>[
98-
isMethodCall('authenticate',
99-
arguments: <String, dynamic>{
100-
'localizedReason': 'Insecure',
101-
'useErrorDialogs': false,
102-
'stickyAuth': false,
103-
'sensitiveTransaction': false,
104-
'biometricOnly': true,
105-
}..addAll(const AndroidAuthMessages().args)),
143+
isMethodCall('authenticate', arguments: <String, dynamic>{
144+
'localizedReason': 'Insecure',
145+
'useErrorDialogs': false,
146+
'stickyAuth': false,
147+
'sensitiveTransaction': false,
148+
'biometricOnly': true,
149+
'biometricHint': androidBiometricHint,
150+
'biometricNotRecognized': androidBiometricNotRecognized,
151+
'biometricSuccess': androidBiometricSuccess,
152+
'biometricRequired': androidBiometricRequiredTitle,
153+
'cancelButton': androidCancelButton,
154+
'deviceCredentialsRequired':
155+
androidDeviceCredentialsRequiredTitle,
156+
'deviceCredentialsSetupDescription':
157+
androidDeviceCredentialsSetupDescription,
158+
'goToSetting': goToSettings,
159+
'goToSettingDescription': androidGoToSettingsDescription,
160+
'signInTitle': androidSignInTitle,
161+
}),
106162
],
107163
);
108164
});
@@ -117,14 +173,25 @@ void main() {
117173
expect(
118174
log,
119175
<Matcher>[
120-
isMethodCall('authenticate',
121-
arguments: <String, dynamic>{
122-
'localizedReason': 'Needs secure',
123-
'useErrorDialogs': true,
124-
'stickyAuth': false,
125-
'sensitiveTransaction': true,
126-
'biometricOnly': false,
127-
}..addAll(const AndroidAuthMessages().args)),
176+
isMethodCall('authenticate', arguments: <String, dynamic>{
177+
'localizedReason': 'Needs secure',
178+
'useErrorDialogs': true,
179+
'stickyAuth': false,
180+
'sensitiveTransaction': true,
181+
'biometricOnly': false,
182+
'biometricHint': androidBiometricHint,
183+
'biometricNotRecognized': androidBiometricNotRecognized,
184+
'biometricSuccess': androidBiometricSuccess,
185+
'biometricRequired': androidBiometricRequiredTitle,
186+
'cancelButton': androidCancelButton,
187+
'deviceCredentialsRequired':
188+
androidDeviceCredentialsRequiredTitle,
189+
'deviceCredentialsSetupDescription':
190+
androidDeviceCredentialsSetupDescription,
191+
'goToSetting': goToSettings,
192+
'goToSettingDescription': androidGoToSettingsDescription,
193+
'signInTitle': androidSignInTitle,
194+
}),
128195
],
129196
);
130197
});
@@ -137,14 +204,17 @@ void main() {
137204
expect(
138205
log,
139206
<Matcher>[
140-
isMethodCall('authenticate',
141-
arguments: <String, dynamic>{
142-
'localizedReason': 'Needs secure',
143-
'useErrorDialogs': true,
144-
'stickyAuth': false,
145-
'sensitiveTransaction': true,
146-
'biometricOnly': false,
147-
}..addAll(const IOSAuthMessages().args)),
207+
isMethodCall('authenticate', arguments: <String, dynamic>{
208+
'localizedReason': 'Needs secure',
209+
'useErrorDialogs': true,
210+
'stickyAuth': false,
211+
'sensitiveTransaction': true,
212+
'biometricOnly': false,
213+
'lockOut': iOSLockOut,
214+
'goToSetting': goToSettings,
215+
'goToSettingDescriptionIOS': iOSGoToSettingsDescription,
216+
'okButton': iOSOkButton,
217+
}),
148218
],
149219
);
150220
});
@@ -159,14 +229,25 @@ void main() {
159229
expect(
160230
log,
161231
<Matcher>[
162-
isMethodCall('authenticate',
163-
arguments: <String, dynamic>{
164-
'localizedReason': 'Insecure',
165-
'useErrorDialogs': false,
166-
'stickyAuth': false,
167-
'sensitiveTransaction': false,
168-
'biometricOnly': false,
169-
}..addAll(const AndroidAuthMessages().args)),
232+
isMethodCall('authenticate', arguments: <String, dynamic>{
233+
'localizedReason': 'Insecure',
234+
'useErrorDialogs': false,
235+
'stickyAuth': false,
236+
'sensitiveTransaction': false,
237+
'biometricOnly': false,
238+
'biometricHint': androidBiometricHint,
239+
'biometricNotRecognized': androidBiometricNotRecognized,
240+
'biometricSuccess': androidBiometricSuccess,
241+
'biometricRequired': androidBiometricRequiredTitle,
242+
'cancelButton': androidCancelButton,
243+
'deviceCredentialsRequired':
244+
androidDeviceCredentialsRequiredTitle,
245+
'deviceCredentialsSetupDescription':
246+
androidDeviceCredentialsSetupDescription,
247+
'goToSetting': goToSettings,
248+
'goToSettingDescription': androidGoToSettingsDescription,
249+
'signInTitle': androidSignInTitle,
250+
}),
170251
],
171252
);
172253
});

0 commit comments

Comments
 (0)