diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md index e67f2a4e2ef1..eb95e2f1ed8e 100644 --- a/packages/local_auth/local_auth_ios/CHANGELOG.md +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.11 + +* Fixes issue where failed authentication was failing silently + ## 1.0.10 * Updates imports for `prefer_relative_imports`. diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index 50dbb1a6907b..51c94ccc39e7 100644 --- a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -124,7 +124,7 @@ - (void)testFailedAuthWithBiometrics { 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]); + reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -140,14 +140,13 @@ - (void)testFailedAuthWithBiometrics { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); + XCTAssertTrue([result isKindOfClass:[FlutterError class]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } -- (void)testFailedAuthWithoutBiometrics { +- (void)testFailedWithUnknownErrorCode { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; @@ -175,6 +174,45 @@ - (void)testFailedAuthWithoutBiometrics { @"localizedReason" : reason, }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testSystemCancelledWithoutStickyAuth { + 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:LAErrorSystemCancel userInfo:nil]); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + @"stickyAuth" : @(NO) + }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { @@ -186,6 +224,44 @@ - (void)testFailedAuthWithoutBiometrics { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } +- (void)testFailedAuthWithoutBiometrics { + 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:LAErrorAuthenticationFailed 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:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + - (void)testLocalizedFallbackTitle { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); @@ -203,7 +279,7 @@ - (void)testLocalizedFallbackTitle { 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]); + reply(YES, nil); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -220,10 +296,7 @@ - (void)testLocalizedFallbackTitle { 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]; @@ -245,7 +318,7 @@ - (void)testSkippedLocalizedFallbackTitle { 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]); + reply(YES, nil); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -260,10 +333,7 @@ - (void)testSkippedLocalizedFallbackTitle { 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]; diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 8f61fecfd814..4d982549643d 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -216,26 +216,29 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success result(@YES); } else { switch (error.code) { - case LAErrorPasscodeNotSet: #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in these constants when - // iOS 10 support is dropped. The values are the same, only the names have changed. + // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in these constants when + // iOS 10 support is dropped. The values are the same, only the names have changed. case LAErrorTouchIDNotAvailable: case LAErrorTouchIDNotEnrolled: case LAErrorTouchIDLockout: #pragma clang diagnostic pop case LAErrorUserFallback: + case LAErrorPasscodeNotSet: + case LAErrorAuthenticationFailed: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; case LAErrorSystemCancel: if ([arguments[@"stickyAuth"] boolValue]) { self->_lastCallArgs = arguments; self->_lastResult = result; - return; + } else { + result(@NO); } + return; } - result(@NO); + [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; } } diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index 9cdeef963c34..d6cab0fe97bc 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_ios description: iOS implementation of the local_auth plugin. repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.10 +version: 1.0.11 environment: sdk: ">=2.14.0 <3.0.0"