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

Commit 08f8f21

Browse files
[local_auth] Fix callback thread handling (#3778)
Ensure that all auth replies, which are sent on an internal framework queue per documentation, are dispatched back to the main thread for handling, as all resulting operations (method channel callbacks, display of UI) are things that must be done on the main thread In order to test this, sets up local_auth with XCTest-based tests, and adds the ability to inject a mock LAContext. (This does not do full unit test backfill, to limit the scope of the PR.) Fixes flutter/flutter#47465
1 parent 00ec3cd commit 08f8f21

File tree

10 files changed

+472
-77
lines changed

10 files changed

+472
-77
lines changed

packages/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.3
2+
3+
* Fix crashes due to threading issues in iOS implementation.
4+
15
## 1.1.2
26

37
* Update Jetpack dependencies to latest stable versions.

packages/local_auth/example/ios/Podfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ flutter_ios_podfile_setup
2929

3030
target 'Runner' do
3131
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
32+
33+
target 'XCTests' do
34+
inherit! :search_paths
35+
36+
pod 'OCMock', '3.5'
37+
end
3238
end
3339

3440
post_install do |installer|

packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 167 additions & 27 deletions
Large diffs are not rendered by default.

packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@
3737
</BuildableReference>
3838
</MacroExpansion>
3939
<Testables>
40+
<TestableReference
41+
skipped = "NO">
42+
<BuildableReference
43+
BuildableIdentifier = "primary"
44+
BlueprintIdentifier = "3398D2CC26163948005A052F"
45+
BuildableName = "XCTests.xctest"
46+
BlueprintName = "XCTests"
47+
ReferencedContainer = "container:Runner.xcodeproj">
48+
</BuildableReference>
49+
</TestableReference>
4050
</Testables>
4151
</TestAction>
4252
<LaunchAction
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleExecutable</key>
8+
<string>$(EXECUTABLE_NAME)</string>
9+
<key>CFBundleIdentifier</key>
10+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11+
<key>CFBundleInfoDictionaryVersion</key>
12+
<string>6.0</string>
13+
<key>CFBundleName</key>
14+
<string>$(PRODUCT_NAME)</string>
15+
<key>CFBundlePackageType</key>
16+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
17+
<key>CFBundleShortVersionString</key>
18+
<string>1.0</string>
19+
<key>CFBundleVersion</key>
20+
<string>1</string>
21+
</dict>
22+
</plist>

packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
#import "FLTLocalAuthPlugin.h"
77

88
@interface FLTLocalAuthPlugin ()
9-
@property(copy, nullable) NSDictionary<NSString *, NSNumber *> *lastCallArgs;
10-
@property(nullable) FlutterResult lastResult;
9+
@property(nonatomic, copy, nullable) NSDictionary<NSString *, NSNumber *> *lastCallArgs;
10+
@property(nonatomic, nullable) FlutterResult lastResult;
11+
// For unit tests to inject dummy LAContext instances that will be used when a new context would
12+
// normally be created. Each call to createAuthContext will remove the current first element from
13+
// the array.
14+
- (void)setAuthContextOverrides:(NSArray<LAContext *> *)authContexts;
1115
@end
1216

13-
@implementation FLTLocalAuthPlugin
17+
@implementation FLTLocalAuthPlugin {
18+
NSMutableArray<LAContext *> *_authContextOverrides;
19+
}
1420

1521
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
1622
FlutterMethodChannel *channel =
@@ -40,6 +46,19 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
4046

4147
#pragma mark Private Methods
4248

49+
- (void)setAuthContextOverrides:(NSArray<LAContext *> *)authContexts {
50+
_authContextOverrides = [authContexts mutableCopy];
51+
}
52+
53+
- (LAContext *)createAuthContext {
54+
if ([_authContextOverrides count] > 0) {
55+
LAContext *context = [_authContextOverrides firstObject];
56+
[_authContextOverrides removeObjectAtIndex:0];
57+
return context;
58+
}
59+
return [[LAContext alloc] init];
60+
}
61+
4362
- (void)alertMessage:(NSString *)message
4463
firstButton:(NSString *)firstButton
4564
flutterResult:(FlutterResult)result
@@ -75,7 +94,7 @@ - (void)alertMessage:(NSString *)message
7594
}
7695

7796
- (void)getAvailableBiometrics:(FlutterResult)result {
78-
LAContext *context = [[LAContext alloc] init];
97+
LAContext *context = self.createAuthContext;
7998
NSError *authError = nil;
8099
NSMutableArray<NSString *> *biometrics = [[NSMutableArray<NSString *> alloc] init];
81100
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
@@ -96,9 +115,10 @@ - (void)getAvailableBiometrics:(FlutterResult)result {
96115
}
97116
result(biometrics);
98117
}
118+
99119
- (void)authenticateWithBiometrics:(NSDictionary *)arguments
100120
withFlutterResult:(FlutterResult)result {
101-
LAContext *context = [[LAContext alloc] init];
121+
LAContext *context = self.createAuthContext;
102122
NSError *authError = nil;
103123
self.lastCallArgs = nil;
104124
self.lastResult = nil;
@@ -109,35 +129,20 @@ - (void)authenticateWithBiometrics:(NSDictionary *)arguments
109129
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
110130
localizedReason:arguments[@"localizedReason"]
111131
reply:^(BOOL success, NSError *error) {
112-
if (success) {
113-
result(@YES);
114-
} else {
115-
switch (error.code) {
116-
case LAErrorPasscodeNotSet:
117-
case LAErrorTouchIDNotAvailable:
118-
case LAErrorTouchIDNotEnrolled:
119-
case LAErrorTouchIDLockout:
120-
[self handleErrors:error
121-
flutterArguments:arguments
122-
withFlutterResult:result];
123-
return;
124-
case LAErrorSystemCancel:
125-
if ([arguments[@"stickyAuth"] boolValue]) {
126-
self.lastCallArgs = arguments;
127-
self.lastResult = result;
128-
return;
129-
}
130-
}
131-
result(@NO);
132-
}
132+
dispatch_async(dispatch_get_main_queue(), ^{
133+
[self handleAuthReplyWithSuccess:success
134+
error:error
135+
flutterArguments:arguments
136+
flutterResult:result];
137+
});
133138
}];
134139
} else {
135140
[self handleErrors:authError flutterArguments:arguments withFlutterResult:result];
136141
}
137142
}
138143

139144
- (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result {
140-
LAContext *context = [[LAContext alloc] init];
145+
LAContext *context = self.createAuthContext;
141146
NSError *authError = nil;
142147
_lastCallArgs = nil;
143148
_lastResult = nil;
@@ -148,27 +153,12 @@ - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)
148153
[context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication
149154
localizedReason:arguments[@"localizedReason"]
150155
reply:^(BOOL success, NSError *error) {
151-
if (success) {
152-
result(@YES);
153-
} else {
154-
switch (error.code) {
155-
case LAErrorPasscodeNotSet:
156-
case LAErrorTouchIDNotAvailable:
157-
case LAErrorTouchIDNotEnrolled:
158-
case LAErrorTouchIDLockout:
159-
[self handleErrors:error
160-
flutterArguments:arguments
161-
withFlutterResult:result];
162-
return;
163-
case LAErrorSystemCancel:
164-
if ([arguments[@"stickyAuth"] boolValue]) {
165-
self->_lastCallArgs = arguments;
166-
self->_lastResult = result;
167-
return;
168-
}
169-
}
170-
result(@NO);
171-
}
156+
dispatch_async(dispatch_get_main_queue(), ^{
157+
[self handleAuthReplyWithSuccess:success
158+
error:error
159+
flutterArguments:arguments
160+
flutterResult:result];
161+
});
172162
}];
173163
} else {
174164
[self handleErrors:authError flutterArguments:arguments withFlutterResult:result];
@@ -178,6 +168,32 @@ - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)
178168
}
179169
}
180170

171+
- (void)handleAuthReplyWithSuccess:(BOOL)success
172+
error:(NSError *)error
173+
flutterArguments:(NSDictionary *)arguments
174+
flutterResult:(FlutterResult)result {
175+
NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread.");
176+
if (success) {
177+
result(@YES);
178+
} else {
179+
switch (error.code) {
180+
case LAErrorPasscodeNotSet:
181+
case LAErrorTouchIDNotAvailable:
182+
case LAErrorTouchIDNotEnrolled:
183+
case LAErrorTouchIDLockout:
184+
[self handleErrors:error flutterArguments:arguments withFlutterResult:result];
185+
return;
186+
case LAErrorSystemCancel:
187+
if ([arguments[@"stickyAuth"] boolValue]) {
188+
self->_lastCallArgs = arguments;
189+
self->_lastResult = result;
190+
return;
191+
}
192+
}
193+
result(@NO);
194+
}
195+
}
196+
181197
- (void)handleErrors:(NSError *)authError
182198
flutterArguments:(NSDictionary *)arguments
183199
withFlutterResult:(FlutterResult)result {

0 commit comments

Comments
 (0)