From 684790a7c7756c963ac3aa3b6e4de48cc9b33df5 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Fri, 9 Dec 2022 17:32:28 -0800 Subject: [PATCH 01/23] fix failed biometric authentication not throwing error + tests --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 117 ++++++++++++++++-- .../ios/Classes/FLTLocalAuthPlugin.m | 47 +++---- 2 files changed, 124 insertions(+), 40 deletions(-) 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..33b60ea04b5d 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]]) @@ -136,17 +136,111 @@ - (void)testFailedAuthWithBiometrics { @"localizedReason" : reason, }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testFailedWithErrorCode { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @NO, + @"error" : [NSError errorWithDomain:@"error" + code:LAErrorPasscodeNotSet + userInfo:nil], + @"stickyAuth" : @NO, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testFailedWithUnknownErrorCode { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @NO, + @"error" : [NSError errorWithDomain:@"error" + code:-9999 + userInfo:nil], + @"stickyAuth" : @NO, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testHandleAuthReplyFailedWithSystemCancel { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @NO, + @"error" : [NSError errorWithDomain:@"error" + code:LAErrorSystemCancel + userInfo:nil], + @"stickyAuth" : @NO, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + + +- (void)testHandleAuthReplySucceeded { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @YES + }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); + XCTAssertTrue([result boolValue]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } + - (void)testFailedAuthWithoutBiometrics { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); @@ -163,7 +257,7 @@ - (void)testFailedAuthWithoutBiometrics { 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]]) @@ -179,8 +273,7 @@ - (void)testFailedAuthWithoutBiometrics { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -203,7 +296,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(NO, [NSError errorWithDomain:@"error" code:LAErrorUserFallback userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -220,10 +313,9 @@ - (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]); + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -245,7 +337,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(NO, [NSError errorWithDomain:@"error" code:LAErrorUserFallback userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -260,10 +352,9 @@ - (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]); + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); [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..c6433ee8f08b 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -41,6 +41,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self deviceSupportsBiometrics:result]; } else if ([@"isDeviceSupported" isEqualToString:call.method]) { result(@YES); + } else if ([@"handleAuthReplyWithSuccess" isEqualToString:call.method]) { + bool success = [call.arguments[@"success"] boolValue]; + NSError* error = call.arguments[@"error"]; + [self handleAuthReplyWithSuccess:success error:error flutterArguments:call.arguments flutterResult:result]; } else { result(FlutterMethodNotImplemented); } @@ -216,41 +220,35 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success result(@YES); } else { switch (error.code) { + case LAErrorSystemCancel: + if ([arguments[@"stickyAuth"] boolValue]) { + self->_lastCallArgs = arguments; + self->_lastResult = result; + } else { + result(@NO); + } + return; 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. - case LAErrorTouchIDNotAvailable: - case LAErrorTouchIDNotEnrolled: - case LAErrorTouchIDLockout: -#pragma clang diagnostic pop + case LAErrorAuthenticationFailed: + case LAErrorBiometryNotAvailable: + case LAErrorBiometryNotEnrolled: + case LAErrorBiometryLockout: case LAErrorUserFallback: + default: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; - case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; - return; - } } - result(@NO); } } + - (void)handleErrors:(NSError *)authError flutterArguments:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { NSString *errorCode = @"NotAvailable"; switch (authError.code) { case LAErrorPasscodeNotSet: -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when - // iOS 10 support is dropped. The values are the same, only the names have changed. - case LAErrorTouchIDNotEnrolled: -#pragma clang diagnostic pop + case LAErrorBiometryNotEnrolled: if ([arguments[@"useErrorDialogs"] boolValue]) { [self alertMessage:arguments[@"goToSettingDescriptionIOS"] firstButton:arguments[@"okButton"] @@ -260,12 +258,7 @@ - (void)handleErrors:(NSError *)authError } errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; break; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when - // iOS 10 support is dropped. The values are the same, only the names have changed. - case LAErrorTouchIDLockout: -#pragma clang diagnostic pop + case LAErrorBiometryLockout: [self alertMessage:arguments[@"lockOut"] firstButton:arguments[@"okButton"] flutterResult:result From 89ba69ccc33c37b98092a5fbfa5c71ebc23cd468 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Fri, 9 Dec 2022 17:38:16 -0800 Subject: [PATCH 02/23] fix test names --- .../example/ios/RunnerTests/FLTLocalAuthPluginTests.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 33b60ea04b5d..684ee85af81e 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 @@ -146,7 +146,7 @@ - (void)testFailedAuthWithBiometrics { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } -- (void)testFailedWithErrorCode { +- (void)testFailedWithKnownErrorCode { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; @@ -212,6 +212,7 @@ - (void)testHandleAuthReplyFailedWithSystemCancel { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); XCTAssertFalse([result boolValue]); [expectation fulfill]; }]; From 823843d5d0ce61449afe45a6dd9fdfed8b78d36a Mon Sep 17 00:00:00 2001 From: louisehsu Date: Fri, 9 Dec 2022 17:53:53 -0800 Subject: [PATCH 03/23] Revert "fix test names" This reverts commit 89ba69ccc33c37b98092a5fbfa5c71ebc23cd468. --- .../example/ios/RunnerTests/FLTLocalAuthPluginTests.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 684ee85af81e..33b60ea04b5d 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 @@ -146,7 +146,7 @@ - (void)testFailedAuthWithBiometrics { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } -- (void)testFailedWithKnownErrorCode { +- (void)testFailedWithErrorCode { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; @@ -212,7 +212,6 @@ - (void)testHandleAuthReplyFailedWithSystemCancel { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); XCTAssertFalse([result boolValue]); [expectation fulfill]; }]; From 3da994d4627582980ecbe33e2e0e87ba2e4d039e Mon Sep 17 00:00:00 2001 From: louisehsu Date: Fri, 9 Dec 2022 17:54:22 -0800 Subject: [PATCH 04/23] Revert "fix failed biometric authentication not throwing error + tests" This reverts commit 684790a7c7756c963ac3aa3b6e4de48cc9b33df5. --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 117 ++---------------- .../ios/Classes/FLTLocalAuthPlugin.m | 47 ++++--- 2 files changed, 40 insertions(+), 124 deletions(-) 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 33b60ea04b5d..50dbb1a6907b 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:LAErrorAuthenticationFailed userInfo:nil]); + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -136,111 +136,17 @@ - (void)testFailedAuthWithBiometrics { @"localizedReason" : reason, }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testFailedWithErrorCode { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @NO, - @"error" : [NSError errorWithDomain:@"error" - code:LAErrorPasscodeNotSet - userInfo:nil], - @"stickyAuth" : @NO, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testFailedWithUnknownErrorCode { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @NO, - @"error" : [NSError errorWithDomain:@"error" - code:-9999 - userInfo:nil], - @"stickyAuth" : @NO, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testHandleAuthReplyFailedWithSystemCancel { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @NO, - @"error" : [NSError errorWithDomain:@"error" - code:LAErrorSystemCancel - userInfo:nil], - @"stickyAuth" : @NO, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertFalse([result boolValue]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - - -- (void)testHandleAuthReplySucceeded { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @YES - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); + XCTAssertFalse([result boolValue]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - - (void)testFailedAuthWithoutBiometrics { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); @@ -257,7 +163,7 @@ - (void)testFailedAuthWithoutBiometrics { 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]); + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -273,7 +179,8 @@ - (void)testFailedAuthWithoutBiometrics { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertFalse([result boolValue]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -296,7 +203,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:LAErrorUserFallback userInfo:nil]); + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -313,9 +220,10 @@ - (void)testLocalizedFallbackTitle { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); + XCTAssertFalse([result boolValue]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -337,7 +245,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:LAErrorUserFallback userInfo:nil]); + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -352,9 +260,10 @@ - (void)testSkippedLocalizedFallbackTitle { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + 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 c6433ee8f08b..8f61fecfd814 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -41,10 +41,6 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self deviceSupportsBiometrics:result]; } else if ([@"isDeviceSupported" isEqualToString:call.method]) { result(@YES); - } else if ([@"handleAuthReplyWithSuccess" isEqualToString:call.method]) { - bool success = [call.arguments[@"success"] boolValue]; - NSError* error = call.arguments[@"error"]; - [self handleAuthReplyWithSuccess:success error:error flutterArguments:call.arguments flutterResult:result]; } else { result(FlutterMethodNotImplemented); } @@ -220,35 +216,41 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success result(@YES); } else { switch (error.code) { - case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; - } else { - result(@NO); - } - return; case LAErrorPasscodeNotSet: - case LAErrorAuthenticationFailed: - case LAErrorBiometryNotAvailable: - case LAErrorBiometryNotEnrolled: - case LAErrorBiometryLockout: +#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. + case LAErrorTouchIDNotAvailable: + case LAErrorTouchIDNotEnrolled: + case LAErrorTouchIDLockout: +#pragma clang diagnostic pop case LAErrorUserFallback: - default: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; + case LAErrorSystemCancel: + if ([arguments[@"stickyAuth"] boolValue]) { + self->_lastCallArgs = arguments; + self->_lastResult = result; + return; + } } + result(@NO); } } - - (void)handleErrors:(NSError *)authError flutterArguments:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { NSString *errorCode = @"NotAvailable"; switch (authError.code) { case LAErrorPasscodeNotSet: - case LAErrorBiometryNotEnrolled: +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when + // iOS 10 support is dropped. The values are the same, only the names have changed. + case LAErrorTouchIDNotEnrolled: +#pragma clang diagnostic pop if ([arguments[@"useErrorDialogs"] boolValue]) { [self alertMessage:arguments[@"goToSettingDescriptionIOS"] firstButton:arguments[@"okButton"] @@ -258,7 +260,12 @@ - (void)handleErrors:(NSError *)authError } errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; break; - case LAErrorBiometryLockout: +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when + // iOS 10 support is dropped. The values are the same, only the names have changed. + case LAErrorTouchIDLockout: +#pragma clang diagnostic pop [self alertMessage:arguments[@"lockOut"] firstButton:arguments[@"okButton"] flutterResult:result From 714907df9ffbef6de21a503591e701d6f3b86f78 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Fri, 9 Dec 2022 18:00:17 -0800 Subject: [PATCH 05/23] fix authentication not throwing error + tests --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 117 ++++++++++++++++-- .../ios/Classes/FLTLocalAuthPlugin.m | 47 +++---- 2 files changed, 124 insertions(+), 40 deletions(-) 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..33b60ea04b5d 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]]) @@ -136,17 +136,111 @@ - (void)testFailedAuthWithBiometrics { @"localizedReason" : reason, }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testFailedWithErrorCode { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @NO, + @"error" : [NSError errorWithDomain:@"error" + code:LAErrorPasscodeNotSet + userInfo:nil], + @"stickyAuth" : @NO, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testFailedWithUnknownErrorCode { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @NO, + @"error" : [NSError errorWithDomain:@"error" + code:-9999 + userInfo:nil], + @"stickyAuth" : @NO, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testHandleAuthReplyFailedWithSystemCancel { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @NO, + @"error" : [NSError errorWithDomain:@"error" + code:LAErrorSystemCancel + userInfo:nil], + @"stickyAuth" : @NO, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + + +- (void)testHandleAuthReplySucceeded { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @YES + }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); + XCTAssertTrue([result boolValue]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } + - (void)testFailedAuthWithoutBiometrics { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); @@ -163,7 +257,7 @@ - (void)testFailedAuthWithoutBiometrics { 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]]) @@ -179,8 +273,7 @@ - (void)testFailedAuthWithoutBiometrics { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -203,7 +296,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(NO, [NSError errorWithDomain:@"error" code:LAErrorUserFallback userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -220,10 +313,9 @@ - (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]); + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -245,7 +337,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(NO, [NSError errorWithDomain:@"error" code:LAErrorUserFallback userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -260,10 +352,9 @@ - (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]); + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); [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..c6433ee8f08b 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -41,6 +41,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self deviceSupportsBiometrics:result]; } else if ([@"isDeviceSupported" isEqualToString:call.method]) { result(@YES); + } else if ([@"handleAuthReplyWithSuccess" isEqualToString:call.method]) { + bool success = [call.arguments[@"success"] boolValue]; + NSError* error = call.arguments[@"error"]; + [self handleAuthReplyWithSuccess:success error:error flutterArguments:call.arguments flutterResult:result]; } else { result(FlutterMethodNotImplemented); } @@ -216,41 +220,35 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success result(@YES); } else { switch (error.code) { + case LAErrorSystemCancel: + if ([arguments[@"stickyAuth"] boolValue]) { + self->_lastCallArgs = arguments; + self->_lastResult = result; + } else { + result(@NO); + } + return; 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. - case LAErrorTouchIDNotAvailable: - case LAErrorTouchIDNotEnrolled: - case LAErrorTouchIDLockout: -#pragma clang diagnostic pop + case LAErrorAuthenticationFailed: + case LAErrorBiometryNotAvailable: + case LAErrorBiometryNotEnrolled: + case LAErrorBiometryLockout: case LAErrorUserFallback: + default: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; - case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; - return; - } } - result(@NO); } } + - (void)handleErrors:(NSError *)authError flutterArguments:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { NSString *errorCode = @"NotAvailable"; switch (authError.code) { case LAErrorPasscodeNotSet: -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when - // iOS 10 support is dropped. The values are the same, only the names have changed. - case LAErrorTouchIDNotEnrolled: -#pragma clang diagnostic pop + case LAErrorBiometryNotEnrolled: if ([arguments[@"useErrorDialogs"] boolValue]) { [self alertMessage:arguments[@"goToSettingDescriptionIOS"] firstButton:arguments[@"okButton"] @@ -260,12 +258,7 @@ - (void)handleErrors:(NSError *)authError } errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; break; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when - // iOS 10 support is dropped. The values are the same, only the names have changed. - case LAErrorTouchIDLockout: -#pragma clang diagnostic pop + case LAErrorBiometryLockout: [self alertMessage:arguments[@"lockOut"] firstButton:arguments[@"okButton"] flutterResult:result From abb8e681c414c6d396ae8ccc0bfc9a1293b1602c Mon Sep 17 00:00:00 2001 From: louisehsu Date: Fri, 9 Dec 2022 18:03:34 -0800 Subject: [PATCH 06/23] fix test name --- .../example/ios/RunnerTests/FLTLocalAuthPluginTests.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 33b60ea04b5d..684ee85af81e 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 @@ -146,7 +146,7 @@ - (void)testFailedAuthWithBiometrics { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } -- (void)testFailedWithErrorCode { +- (void)testFailedWithKnownErrorCode { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; @@ -212,6 +212,7 @@ - (void)testHandleAuthReplyFailedWithSystemCancel { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); XCTAssertFalse([result boolValue]); [expectation fulfill]; }]; From b7bbcab17230cfcc95a7eba3ef354b05cebf26a9 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Fri, 9 Dec 2022 18:07:11 -0800 Subject: [PATCH 07/23] auto format --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 918 +++++++++--------- .../ios/Classes/FLTLocalAuthPlugin.m | 398 ++++---- 2 files changed, 658 insertions(+), 658 deletions(-) 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 684ee85af81e..656145266608 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 @@ -27,543 +27,543 @@ @interface FLTLocalAuthPluginTests : XCTestCase @implementation FLTLocalAuthPluginTests - (void)setUp { - self.continueAfterFailure = NO; + self.continueAfterFailure = NO; } - (void)testSuccessfullAuthWithBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - 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(YES, nil); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(YES), - @"localizedReason" : reason, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + 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(YES, nil); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(YES), + @"localizedReason" : reason, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testSuccessfullAuthWithoutBiometrics { - 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(YES, 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]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + 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(YES, 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]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testFailedAuthWithBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - 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" : @(YES), - @"localizedReason" : reason, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + 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" : @(YES), + @"localizedReason" : reason, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testFailedWithKnownErrorCode { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @NO, - @"error" : [NSError errorWithDomain:@"error" - code:LAErrorPasscodeNotSet - userInfo:nil], - @"stickyAuth" : @NO, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @NO, + @"error" : [NSError errorWithDomain:@"error" + code:LAErrorPasscodeNotSet + userInfo:nil], + @"stickyAuth" : @NO, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testFailedWithUnknownErrorCode { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @NO, - @"error" : [NSError errorWithDomain:@"error" - code:-9999 - userInfo:nil], - @"stickyAuth" : @NO, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @NO, + @"error" : [NSError errorWithDomain:@"error" + code:-9999 + userInfo:nil], + @"stickyAuth" : @NO, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testHandleAuthReplyFailedWithSystemCancel { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @NO, - @"error" : [NSError errorWithDomain:@"error" - code:LAErrorSystemCancel - userInfo:nil], - @"stickyAuth" : @NO, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @NO, + @"error" : [NSError errorWithDomain:@"error" + code:LAErrorSystemCancel + userInfo:nil], + @"stickyAuth" : @NO, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testHandleAuthReplySucceeded { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @YES - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + arguments:@{ + @"success" : @YES + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + [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 isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + 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 isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [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:LAErrorUserFallback 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) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [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:LAErrorUserFallback 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) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testDeviceSupportsBiometrics_withEnrolledHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware_iOS11 { - if (@available(iOS 11, *)) { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { - // Write error - NSError *__autoreleasing *authError; - [invocation getArgument:&authError atIndex:3]; - *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; - // Write return value - BOOL returnValue = NO; - NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; - [invocation setReturnValue:&nsReturnValue]; + + 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:LAErrorUserFallback userInfo:nil]); + }); }; - OCMStub([mockAuthContext canEvaluatePolicy:policy - error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) - .andDo(canEvaluatePolicyHandler); - + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" arguments:@{}]; + [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]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - + OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; - } -} - -- (void)testDeviceSupportsBiometrics_withNoBiometricHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { - // Write error - NSError *__autoreleasing *authError; - [invocation getArgument:&authError atIndex:3]; - *authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; - // Write return value - BOOL returnValue = NO; - NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; - [invocation setReturnValue:&nsReturnValue]; - }; - OCMStub([mockAuthContext canEvaluatePolicy:policy - error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) - .andDo(canEvaluatePolicyHandler); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } -- (void)testGetEnrolledBiometrics_withFaceID_iOS11 { - if (@available(iOS 11, *)) { +- (void)testSkippedLocalizedFallbackTitle { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; + NSString *reason = @"a reason"; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; + + // 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:LAErrorUserFallback 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:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"face"); - [expectation fulfill]; - }]; - + OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; - } } -- (void)testGetEnrolledBiometrics_withTouchID_iOS11 { - if (@available(iOS 11, *)) { +- (void)testDeviceSupportsBiometrics_withEnrolledHardware { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; - + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" arguments:@{}]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"fingerprint"); - [expectation fulfill]; - }]; - + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; - } } -- (void)testGetEnrolledBiometrics_withTouchID_preIOS11 { - if (@available(iOS 11, *)) { - return; - } - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"fingerprint"); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; + }; + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } } -- (void)testGetEnrolledBiometrics_withoutEnrolledHardware_iOS11 { - if (@available(iOS 11, *)) { +- (void)testDeviceSupportsBiometrics_withNoBiometricHardware { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; - + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { - // Write error - NSError *__autoreleasing *authError; - [invocation getArgument:&authError atIndex:3]; - *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; - // Write return value - BOOL returnValue = NO; - NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; - [invocation setReturnValue:&nsReturnValue]; + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; }; OCMStub([mockAuthContext canEvaluatePolicy:policy error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) - .andDo(canEvaluatePolicyHandler); + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} +- (void)testGetEnrolledBiometrics_withFaceID_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"face"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} + +- (void)testGetEnrolledBiometrics_withTouchID_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"fingerprint"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} + +- (void)testGetEnrolledBiometrics_withTouchID_preIOS11 { + if (@available(iOS 11, *)) { + return; + } + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" arguments:@{}]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 0); - [expectation fulfill]; - }]; - + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"fingerprint"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; - } +} + +- (void)testGetEnrolledBiometrics_withoutEnrolledHardware_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; + }; + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 0); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } } @end 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 c6433ee8f08b..bc963a1b2e16 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -15,267 +15,267 @@ - (void)setAuthContextOverrides:(NSArray *)authContexts; @end @implementation FLTLocalAuthPlugin { - NSMutableArray *_authContextOverrides; + NSMutableArray *_authContextOverrides; } + (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth_ios" - binaryMessenger:[registrar messenger]]; - FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; - [registrar addApplicationDelegate:instance]; + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth_ios" + binaryMessenger:[registrar messenger]]; + FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; + [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addApplicationDelegate:instance]; } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"authenticate" isEqualToString:call.method]) { - bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; - if (isBiometricOnly) { - [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; + if ([@"authenticate" isEqualToString:call.method]) { + bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; + if (isBiometricOnly) { + [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; + } else { + [self authenticate:call.arguments withFlutterResult:result]; + } + } else if ([@"getEnrolledBiometrics" isEqualToString:call.method]) { + [self getEnrolledBiometrics:result]; + } else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) { + [self deviceSupportsBiometrics:result]; + } else if ([@"isDeviceSupported" isEqualToString:call.method]) { + result(@YES); + } else if ([@"handleAuthReplyWithSuccess" isEqualToString:call.method]) { + bool success = [call.arguments[@"success"] boolValue]; + NSError* error = call.arguments[@"error"]; + [self handleAuthReplyWithSuccess:success error:error flutterArguments:call.arguments flutterResult:result]; } else { - [self authenticate:call.arguments withFlutterResult:result]; + result(FlutterMethodNotImplemented); } - } else if ([@"getEnrolledBiometrics" isEqualToString:call.method]) { - [self getEnrolledBiometrics:result]; - } else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) { - [self deviceSupportsBiometrics:result]; - } else if ([@"isDeviceSupported" isEqualToString:call.method]) { - result(@YES); - } else if ([@"handleAuthReplyWithSuccess" isEqualToString:call.method]) { - bool success = [call.arguments[@"success"] boolValue]; - NSError* error = call.arguments[@"error"]; - [self handleAuthReplyWithSuccess:success error:error flutterArguments:call.arguments flutterResult:result]; - } else { - result(FlutterMethodNotImplemented); - } } #pragma mark Private Methods - (void)setAuthContextOverrides:(NSArray *)authContexts { - _authContextOverrides = [authContexts mutableCopy]; + _authContextOverrides = [authContexts mutableCopy]; } - (LAContext *)createAuthContext { - if ([_authContextOverrides count] > 0) { - LAContext *context = [_authContextOverrides firstObject]; - [_authContextOverrides removeObjectAtIndex:0]; - return context; - } - return [[LAContext alloc] init]; + if ([_authContextOverrides count] > 0) { + LAContext *context = [_authContextOverrides firstObject]; + [_authContextOverrides removeObjectAtIndex:0]; + return context; + } + return [[LAContext alloc] init]; } - (void)alertMessage:(NSString *)message firstButton:(NSString *)firstButton flutterResult:(FlutterResult)result additionalButton:(NSString *)secondButton { - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:@"" - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - result(@NO); - }]; - - [alert addAction:defaultAction]; - if (secondButton != nil) { - UIAlertAction *additionalAction = [UIAlertAction - actionWithTitle:secondButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - if (UIApplicationOpenSettingsURLString != NULL) { - NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; - if (@available(iOS 10, *)) { - [[UIApplication sharedApplication] openURL:url - options:@{} - completionHandler:NULL]; - } else { + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + result(@NO); + }]; + + [alert addAction:defaultAction]; + if (secondButton != nil) { + UIAlertAction *additionalAction = [UIAlertAction + actionWithTitle:secondButton + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + if (UIApplicationOpenSettingsURLString != NULL) { + NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; + if (@available(iOS 10, *)) { + [[UIApplication sharedApplication] openURL:url + options:@{} + completionHandler:NULL]; + } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - [[UIApplication sharedApplication] openURL:url]; + [[UIApplication sharedApplication] openURL:url]; #pragma clang diagnostic pop - } - result(@NO); - } - }]; - [alert addAction:additionalAction]; - } - [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert - animated:YES - completion:nil]; + } + result(@NO); + } + }]; + [alert addAction:additionalAction]; + } + [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert + animated:YES + completion:nil]; } - (void)deviceSupportsBiometrics:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - // Check if authentication with biometrics is possible. - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - error:&authError]) { - if (authError == nil) { - result(@YES); - return; + LAContext *context = self.createAuthContext; + NSError *authError = nil; + // Check if authentication with biometrics is possible. + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&authError]) { + if (authError == nil) { + result(@YES); + return; + } } - } - // If not, check if it is because no biometrics are enrolled (but still present). - if (authError != nil) { - if (@available(iOS 11, *)) { - if (authError.code == LAErrorBiometryNotEnrolled) { - result(@YES); - return; - } + // If not, check if it is because no biometrics are enrolled (but still present). + if (authError != nil) { + if (@available(iOS 11, *)) { + if (authError.code == LAErrorBiometryNotEnrolled) { + result(@YES); + return; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - } else if (authError.code == LAErrorTouchIDNotEnrolled) { - result(@YES); - return; + } else if (authError.code == LAErrorTouchIDNotEnrolled) { + result(@YES); + return; #pragma clang diagnostic pop + } } - } - - result(@NO); + + result(@NO); } - (void)getEnrolledBiometrics:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - NSMutableArray *biometrics = [[NSMutableArray alloc] init]; - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - error:&authError]) { - if (authError == nil) { - if (@available(iOS 11, *)) { - if (context.biometryType == LABiometryTypeFaceID) { - [biometrics addObject:@"face"]; - } else if (context.biometryType == LABiometryTypeTouchID) { - [biometrics addObject:@"fingerprint"]; + LAContext *context = self.createAuthContext; + NSError *authError = nil; + NSMutableArray *biometrics = [[NSMutableArray alloc] init]; + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&authError]) { + if (authError == nil) { + if (@available(iOS 11, *)) { + if (context.biometryType == LABiometryTypeFaceID) { + [biometrics addObject:@"face"]; + } else if (context.biometryType == LABiometryTypeTouchID) { + [biometrics addObject:@"fingerprint"]; + } + } else { + [biometrics addObject:@"fingerprint"]; + } } - } else { - [biometrics addObject:@"fingerprint"]; - } } - } - result(biometrics); + result(biometrics); } - (void)authenticateWithBiometrics:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - self.lastCallArgs = nil; - self.lastResult = nil; - context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] - ? nil - : arguments[@"localizedFallbackTitle"]; - - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - error:&authError]) { - [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; - } + LAContext *context = self.createAuthContext; + NSError *authError = nil; + self.lastCallArgs = nil; + self.lastResult = nil; + context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] + ? nil + : arguments[@"localizedFallbackTitle"]; + + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&authError]) { + [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + localizedReason:arguments[@"localizedReason"] + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + flutterArguments:arguments + flutterResult:result]; + }); + }]; + } else { + [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; + } } - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - _lastCallArgs = nil; - _lastResult = nil; - context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] - ? nil - : arguments[@"localizedFallbackTitle"]; - - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { - [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; - } + LAContext *context = self.createAuthContext; + NSError *authError = nil; + _lastCallArgs = nil; + _lastResult = nil; + context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] + ? nil + : arguments[@"localizedFallbackTitle"]; + + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { + [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication + localizedReason:arguments[@"localizedReason"] + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + flutterArguments:arguments + flutterResult:result]; + }); + }]; + } else { + [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; + } } - (void)handleAuthReplyWithSuccess:(BOOL)success error:(NSError *)error flutterArguments:(NSDictionary *)arguments flutterResult:(FlutterResult)result { - NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); - if (success) { - result(@YES); - } else { - switch (error.code) { - case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; - } else { - result(@NO); + NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); + if (success) { + result(@YES); + } else { + switch (error.code) { + case LAErrorSystemCancel: + if ([arguments[@"stickyAuth"] boolValue]) { + self->_lastCallArgs = arguments; + self->_lastResult = result; + } else { + result(@NO); + } + return; + case LAErrorPasscodeNotSet: + case LAErrorAuthenticationFailed: + case LAErrorBiometryNotAvailable: + case LAErrorBiometryNotEnrolled: + case LAErrorBiometryLockout: + case LAErrorUserFallback: + default: + [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; + return; } - return; - case LAErrorPasscodeNotSet: - case LAErrorAuthenticationFailed: - case LAErrorBiometryNotAvailable: - case LAErrorBiometryNotEnrolled: - case LAErrorBiometryLockout: - case LAErrorUserFallback: - default: - [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; - return; } - } } - (void)handleErrors:(NSError *)authError - flutterArguments:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { - NSString *errorCode = @"NotAvailable"; - switch (authError.code) { - case LAErrorPasscodeNotSet: - case LAErrorBiometryNotEnrolled: - if ([arguments[@"useErrorDialogs"] boolValue]) { - [self alertMessage:arguments[@"goToSettingDescriptionIOS"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:arguments[@"goToSetting"]]; - return; - } - errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; - break; - case LAErrorBiometryLockout: - [self alertMessage:arguments[@"lockOut"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:nil]; - return; - } - result([FlutterError errorWithCode:errorCode - message:authError.localizedDescription - details:authError.domain]); + flutterArguments:(NSDictionary *)arguments + withFlutterResult:(FlutterResult)result { + NSString *errorCode = @"NotAvailable"; + switch (authError.code) { + case LAErrorPasscodeNotSet: + case LAErrorBiometryNotEnrolled: + if ([arguments[@"useErrorDialogs"] boolValue]) { + [self alertMessage:arguments[@"goToSettingDescriptionIOS"] + firstButton:arguments[@"okButton"] + flutterResult:result + additionalButton:arguments[@"goToSetting"]]; + return; + } + errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; + break; + case LAErrorBiometryLockout: + [self alertMessage:arguments[@"lockOut"] + firstButton:arguments[@"okButton"] + flutterResult:result + additionalButton:nil]; + return; + } + result([FlutterError errorWithCode:errorCode + message:authError.localizedDescription + details:authError.domain]); } #pragma mark - AppDelegate - (void)applicationDidBecomeActive:(UIApplication *)application { - if (self.lastCallArgs != nil && self.lastResult != nil) { - [self authenticateWithBiometrics:_lastCallArgs withFlutterResult:self.lastResult]; - } + if (self.lastCallArgs != nil && self.lastResult != nil) { + [self authenticateWithBiometrics:_lastCallArgs withFlutterResult:self.lastResult]; + } } @end From cc7e32fae92f2ea378e731741fafcbf5a631544e Mon Sep 17 00:00:00 2001 From: louisehsu Date: Mon, 12 Dec 2022 14:04:57 -0800 Subject: [PATCH 08/23] cr fixes --- packages/local_auth/local_auth/CHANGELOG.md | 4 ++++ packages/local_auth/local_auth/pubspec.yaml | 2 +- .../local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index 34e26efef238..41a86c558148 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -2,6 +2,10 @@ * Updates minimum Flutter version to 2.10. +## 2.1.3 + +* Fixes biometric authentication failing silently. + ## 2.1.2 * Fixes avoid_redundant_argument_values lint warnings and minor typos. diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index 133df06d43b0..cfb42c849dfd 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -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: 2.1.2 +version: 2.1.3 environment: sdk: ">=2.14.0 <3.0.0" 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 bc963a1b2e16..79caf8fc51ba 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -42,7 +42,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"isDeviceSupported" isEqualToString:call.method]) { result(@YES); } else if ([@"handleAuthReplyWithSuccess" isEqualToString:call.method]) { - bool success = [call.arguments[@"success"] boolValue]; + BOOL success = [call.arguments[@"success"] boolValue]; NSError* error = call.arguments[@"error"]; [self handleAuthReplyWithSuccess:success error:error flutterArguments:call.arguments flutterResult:result]; } else { @@ -222,10 +222,10 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success switch (error.code) { case LAErrorSystemCancel: if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; + self->_lastCallArgs = arguments; + self->_lastResult = result; } else { - result(@NO); + result(@NO); } return; case LAErrorPasscodeNotSet: From 0bcffeb5e9f6b2089f85a9fd88c12e4a9892bdaf Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 02:34:54 -0800 Subject: [PATCH 09/23] addressed pr comments --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 116 ++++++++---------- .../ios/Classes/FLTLocalAuthPlugin.m | 18 +-- 2 files changed, 59 insertions(+), 75 deletions(-) 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 656145266608..d17c9cf0f50d 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 @@ -27,7 +27,7 @@ @interface FLTLocalAuthPluginTests : XCTestCase @implementation FLTLocalAuthPluginTests - (void)setUp { - self.continueAfterFailure = NO; + self.continueAfterFailure = NO; } - (void)testSuccessfullAuthWithBiometrics { @@ -150,72 +150,37 @@ - (void)testFailedWithKnownErrorCode { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @NO, - @"error" : [NSError errorWithDomain:@"error" - code:LAErrorPasscodeNotSet - userInfo:nil], - @"stickyAuth" : @NO, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} -- (void)testFailedWithUnknownErrorCode { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" - arguments:@{ - @"success" : @NO, - @"error" : [NSError errorWithDomain:@"error" - code:-9999 - userInfo:nil], - @"stickyAuth" : @NO, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} + const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; + NSString *reason = @"a reason"; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); -- (void)testHandleAuthReplyFailedWithSystemCancel { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + // 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:LAErrorBiometryNotEnrolled userInfo:nil]); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" arguments:@{ - @"success" : @NO, - @"error" : [NSError errorWithDomain:@"error" - code:LAErrorSystemCancel - userInfo:nil], - @"stickyAuth" : @NO, - }]; - + @"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]]); - XCTAssertFalse([result boolValue]); - [expectation fulfill]; - }]; + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -224,12 +189,31 @@ - (void)testHandleAuthReplySucceeded { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" + + 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:@{ - @"success" : @YES - }]; - + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + @"stickyAuth" : @(NO) + }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { 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 79caf8fc51ba..8d29d4f5ecff 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -41,10 +41,6 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self deviceSupportsBiometrics:result]; } else if ([@"isDeviceSupported" isEqualToString:call.method]) { result(@YES); - } else if ([@"handleAuthReplyWithSuccess" isEqualToString:call.method]) { - BOOL success = [call.arguments[@"success"] boolValue]; - NSError* error = call.arguments[@"error"]; - [self handleAuthReplyWithSuccess:success error:error flutterArguments:call.arguments flutterResult:result]; } else { result(FlutterMethodNotImplemented); } @@ -228,13 +224,17 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success result(@NO); } return; +#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. + case LAErrorTouchIDNotAvailable: + case LAErrorTouchIDNotEnrolled: + case LAErrorTouchIDLockout: + #pragma clang diagnostic pop + case LAErrorUserFallback: case LAErrorPasscodeNotSet: case LAErrorAuthenticationFailed: - case LAErrorBiometryNotAvailable: - case LAErrorBiometryNotEnrolled: - case LAErrorBiometryLockout: - case LAErrorUserFallback: - default: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; } From 5a545bd9d2a3f8baa3bbe849d1bc663a276be0a5 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 02:48:12 -0800 Subject: [PATCH 10/23] formatting --- .../local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 8d29d4f5ecff..b1c2985c522e 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -218,20 +218,20 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success switch (error.code) { case LAErrorSystemCancel: if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; + self->_lastCallArgs = arguments; + self->_lastResult = result; } else { - result(@NO); + result(@NO); } return; #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 +#pragma clang diagnostic pop case LAErrorUserFallback: case LAErrorPasscodeNotSet: case LAErrorAuthenticationFailed: From 1a053b2b0507d23c91f1e253901b928927ff7682 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 03:06:59 -0800 Subject: [PATCH 11/23] formatting --- .../local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b1c2985c522e..67600b8dcbf6 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -244,7 +244,7 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success - (void)handleErrors:(NSError *)authError flutterArguments:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { + withFlutterResult:(FlutterResult)result { NSString *errorCode = @"NotAvailable"; switch (authError.code) { case LAErrorPasscodeNotSet: From 2a6487693fb0c6ec856592f0b6be5ff09d1a75a2 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 03:15:04 -0800 Subject: [PATCH 12/23] formatting --- .../ios/Classes/FLTLocalAuthPlugin.m | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) 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 67600b8dcbf6..913f974be208 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -245,29 +245,29 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success - (void)handleErrors:(NSError *)authError flutterArguments:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - NSString *errorCode = @"NotAvailable"; - switch (authError.code) { - case LAErrorPasscodeNotSet: - case LAErrorBiometryNotEnrolled: - if ([arguments[@"useErrorDialogs"] boolValue]) { - [self alertMessage:arguments[@"goToSettingDescriptionIOS"] + NSString *errorCode = @"NotAvailable"; + switch (authError.code) { + case LAErrorPasscodeNotSet: + case LAErrorBiometryNotEnrolled: + if ([arguments[@"useErrorDialogs"] boolValue]) { + [self alertMessage:arguments[@"goToSettingDescriptionIOS"] + firstButton:arguments[@"okButton"] + flutterResult:result + additionalButton:arguments[@"goToSetting"]]; + return; + } + errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; + break; + case LAErrorBiometryLockout: + [self alertMessage:arguments[@"lockOut"] firstButton:arguments[@"okButton"] flutterResult:result - additionalButton:arguments[@"goToSetting"]]; + additionalButton:nil]; return; - } - errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; - break; - case LAErrorBiometryLockout: - [self alertMessage:arguments[@"lockOut"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:nil]; - return; - } - result([FlutterError errorWithCode:errorCode - message:authError.localizedDescription - details:authError.domain]); + } + result([FlutterError errorWithCode:errorCode + message:authError.localizedDescription + details:authError.domain]); } #pragma mark - AppDelegate From 2583cc4975861676145be738e636099786f36b7c Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 03:52:02 -0800 Subject: [PATCH 13/23] format attempt --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) 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 d17c9cf0f50d..324195b42e6b 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 @@ -27,7 +27,7 @@ @interface FLTLocalAuthPluginTests : XCTestCase @implementation FLTLocalAuthPluginTests - (void)setUp { - self.continueAfterFailure = NO; + self.continueAfterFailure = NO; } - (void)testSuccessfullAuthWithBiometrics { @@ -150,70 +150,69 @@ - (void)testFailedWithKnownErrorCode { 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:LAErrorBiometryNotEnrolled userInfo:nil]); - }); + 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:LAErrorBiometryNotEnrolled userInfo:nil]); + }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - + @"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]; - }]; + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - -- (void)testHandleAuthReplySucceeded { +- (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]); - }); + 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) - }]; - + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + @"stickyAuth" : @(NO) + }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { From 40a00cf1b45dbab12900a9a8c193e09f37b5e926 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 12:30:38 -0800 Subject: [PATCH 14/23] format attempt --- .../ios/Classes/FLTLocalAuthPlugin.m | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) 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 913f974be208..b1c2985c522e 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -244,30 +244,30 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success - (void)handleErrors:(NSError *)authError flutterArguments:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { - NSString *errorCode = @"NotAvailable"; - switch (authError.code) { - case LAErrorPasscodeNotSet: - case LAErrorBiometryNotEnrolled: - if ([arguments[@"useErrorDialogs"] boolValue]) { - [self alertMessage:arguments[@"goToSettingDescriptionIOS"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:arguments[@"goToSetting"]]; - return; - } - errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; - break; - case LAErrorBiometryLockout: - [self alertMessage:arguments[@"lockOut"] + withFlutterResult:(FlutterResult)result { + NSString *errorCode = @"NotAvailable"; + switch (authError.code) { + case LAErrorPasscodeNotSet: + case LAErrorBiometryNotEnrolled: + if ([arguments[@"useErrorDialogs"] boolValue]) { + [self alertMessage:arguments[@"goToSettingDescriptionIOS"] firstButton:arguments[@"okButton"] flutterResult:result - additionalButton:nil]; + additionalButton:arguments[@"goToSetting"]]; return; - } - result([FlutterError errorWithCode:errorCode - message:authError.localizedDescription - details:authError.domain]); + } + errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; + break; + case LAErrorBiometryLockout: + [self alertMessage:arguments[@"lockOut"] + firstButton:arguments[@"okButton"] + flutterResult:result + additionalButton:nil]; + return; + } + result([FlutterError errorWithCode:errorCode + message:authError.localizedDescription + details:authError.domain]); } #pragma mark - AppDelegate From 0ac9aee3262b00518dde5c4fb47a80d6ffb12227 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 14:06:46 -0800 Subject: [PATCH 15/23] formatting fixes --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 899 +++++++++--------- .../ios/Classes/FLTLocalAuthPlugin.m | 395 ++++---- 2 files changed, 646 insertions(+), 648 deletions(-) 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 324195b42e6b..34c67d932b5b 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 @@ -27,526 +27,525 @@ @interface FLTLocalAuthPluginTests : XCTestCase @implementation FLTLocalAuthPluginTests - (void)setUp { - self.continueAfterFailure = NO; + self.continueAfterFailure = NO; } - (void)testSuccessfullAuthWithBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - 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(YES, nil); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(YES), - @"localizedReason" : reason, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + 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(YES, nil); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(YES), + @"localizedReason" : reason, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testSuccessfullAuthWithoutBiometrics { - 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(YES, 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]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + 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(YES, 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]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testFailedAuthWithBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - 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" : @(YES), - @"localizedReason" : reason, - }]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + 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" : @(YES), + @"localizedReason" : reason, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testFailedWithKnownErrorCode { - 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:LAErrorBiometryNotEnrolled 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]; + 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:LAErrorBiometryNotEnrolled 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)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) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} + 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) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + [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 isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + 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 isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [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:LAErrorUserFallback 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) { + OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [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:LAErrorUserFallback 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) { + OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testDeviceSupportsBiometrics_withEnrolledHardware { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware_iOS11 { + if (@available(iOS 11, *)) { 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:LAErrorUserFallback userInfo:nil]); - }); + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); - + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - @"localizedFallbackTitle" : localizedFallbackTitle, - }]; - + [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" arguments:@{}]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } } -- (void)testSkippedLocalizedFallbackTitle { +- (void)testDeviceSupportsBiometrics_withNoBiometricHardware { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; + }; + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testGetEnrolledBiometrics_withFaceID_iOS11 { + if (@available(iOS 11, *)) { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; 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:LAErrorUserFallback userInfo:nil]); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - + OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"face"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } } -- (void)testDeviceSupportsBiometrics_withEnrolledHardware { +- (void)testGetEnrolledBiometrics_withTouchID_iOS11 { + if (@available(iOS 11, *)) { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; - + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" + OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" arguments:@{}]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"fingerprint"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } } -- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware_iOS11 { - if (@available(iOS 11, *)) { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { - // Write error - NSError *__autoreleasing *authError; - [invocation getArgument:&authError atIndex:3]; - *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; - // Write return value - BOOL returnValue = NO; - NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; - [invocation setReturnValue:&nsReturnValue]; - }; - OCMStub([mockAuthContext canEvaluatePolicy:policy - error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) - .andDo(canEvaluatePolicyHandler); - - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; - } +- (void)testGetEnrolledBiometrics_withTouchID_preIOS11 { + if (@available(iOS 11, *)) { + return; + } + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"fingerprint"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } -- (void)testDeviceSupportsBiometrics_withNoBiometricHardware { +- (void)testGetEnrolledBiometrics_withoutEnrolledHardware_iOS11 { + if (@available(iOS 11, *)) { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; - + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { - // Write error - NSError *__autoreleasing *authError; - [invocation getArgument:&authError atIndex:3]; - *authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; - // Write return value - BOOL returnValue = NO; - NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; - [invocation setReturnValue:&nsReturnValue]; + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; }; OCMStub([mockAuthContext canEvaluatePolicy:policy error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) - .andDo(canEvaluatePolicyHandler); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testGetEnrolledBiometrics_withFaceID_iOS11 { - if (@available(iOS 11, *)) { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"face"); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; - } -} - -- (void)testGetEnrolledBiometrics_withTouchID_iOS11 { - if (@available(iOS 11, *)) { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"fingerprint"); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; - } -} + .andDo(canEvaluatePolicyHandler); -- (void)testGetEnrolledBiometrics_withTouchID_preIOS11 { - if (@available(iOS 11, *)) { - return; - } - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" arguments:@{}]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"fingerprint"); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 0); + [expectation fulfill]; + }]; -- (void)testGetEnrolledBiometrics_withoutEnrolledHardware_iOS11 { - if (@available(iOS 11, *)) { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { - // Write error - NSError *__autoreleasing *authError; - [invocation getArgument:&authError atIndex:3]; - *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; - // Write return value - BOOL returnValue = NO; - NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; - [invocation setReturnValue:&nsReturnValue]; - }; - OCMStub([mockAuthContext canEvaluatePolicy:policy - error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) - .andDo(canEvaluatePolicyHandler); - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 0); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; - } + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } } @end 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 b1c2985c522e..d11695b7fcec 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -15,267 +15,266 @@ - (void)setAuthContextOverrides:(NSArray *)authContexts; @end @implementation FLTLocalAuthPlugin { - NSMutableArray *_authContextOverrides; + NSMutableArray *_authContextOverrides; } + (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth_ios" - binaryMessenger:[registrar messenger]]; - FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; - [registrar addApplicationDelegate:instance]; + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth_ios" + binaryMessenger:[registrar messenger]]; + FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; + [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addApplicationDelegate:instance]; } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"authenticate" isEqualToString:call.method]) { - bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; - if (isBiometricOnly) { - [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; - } else { - [self authenticate:call.arguments withFlutterResult:result]; - } - } else if ([@"getEnrolledBiometrics" isEqualToString:call.method]) { - [self getEnrolledBiometrics:result]; - } else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) { - [self deviceSupportsBiometrics:result]; - } else if ([@"isDeviceSupported" isEqualToString:call.method]) { - result(@YES); + if ([@"authenticate" isEqualToString:call.method]) { + bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; + if (isBiometricOnly) { + [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; } else { - result(FlutterMethodNotImplemented); + [self authenticate:call.arguments withFlutterResult:result]; } + } else if ([@"getEnrolledBiometrics" isEqualToString:call.method]) { + [self getEnrolledBiometrics:result]; + } else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) { + [self deviceSupportsBiometrics:result]; + } else if ([@"isDeviceSupported" isEqualToString:call.method]) { + result(@YES); + } else { + result(FlutterMethodNotImplemented); + } } #pragma mark Private Methods - (void)setAuthContextOverrides:(NSArray *)authContexts { - _authContextOverrides = [authContexts mutableCopy]; + _authContextOverrides = [authContexts mutableCopy]; } - (LAContext *)createAuthContext { - if ([_authContextOverrides count] > 0) { - LAContext *context = [_authContextOverrides firstObject]; - [_authContextOverrides removeObjectAtIndex:0]; - return context; - } - return [[LAContext alloc] init]; + if ([_authContextOverrides count] > 0) { + LAContext *context = [_authContextOverrides firstObject]; + [_authContextOverrides removeObjectAtIndex:0]; + return context; + } + return [[LAContext alloc] init]; } - (void)alertMessage:(NSString *)message firstButton:(NSString *)firstButton flutterResult:(FlutterResult)result additionalButton:(NSString *)secondButton { - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:@"" - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - result(@NO); - }]; - - [alert addAction:defaultAction]; - if (secondButton != nil) { - UIAlertAction *additionalAction = [UIAlertAction - actionWithTitle:secondButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - if (UIApplicationOpenSettingsURLString != NULL) { - NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; - if (@available(iOS 10, *)) { - [[UIApplication sharedApplication] openURL:url - options:@{} - completionHandler:NULL]; - } else { + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + result(@NO); + }]; + + [alert addAction:defaultAction]; + if (secondButton != nil) { + UIAlertAction *additionalAction = [UIAlertAction + actionWithTitle:secondButton + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + if (UIApplicationOpenSettingsURLString != NULL) { + NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; + if (@available(iOS 10, *)) { + [[UIApplication sharedApplication] openURL:url + options:@{} + completionHandler:NULL]; + } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - [[UIApplication sharedApplication] openURL:url]; + [[UIApplication sharedApplication] openURL:url]; #pragma clang diagnostic pop - } - result(@NO); - } - }]; - [alert addAction:additionalAction]; - } - [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert - animated:YES - completion:nil]; + } + result(@NO); + } + }]; + [alert addAction:additionalAction]; + } + [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert + animated:YES + completion:nil]; } - (void)deviceSupportsBiometrics:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - // Check if authentication with biometrics is possible. - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - error:&authError]) { - if (authError == nil) { - result(@YES); - return; - } + LAContext *context = self.createAuthContext; + NSError *authError = nil; + // Check if authentication with biometrics is possible. + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&authError]) { + if (authError == nil) { + result(@YES); + return; } - // If not, check if it is because no biometrics are enrolled (but still present). - if (authError != nil) { - if (@available(iOS 11, *)) { - if (authError.code == LAErrorBiometryNotEnrolled) { - result(@YES); - return; - } + } + // If not, check if it is because no biometrics are enrolled (but still present). + if (authError != nil) { + if (@available(iOS 11, *)) { + if (authError.code == LAErrorBiometryNotEnrolled) { + result(@YES); + return; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - } else if (authError.code == LAErrorTouchIDNotEnrolled) { - result(@YES); - return; + } else if (authError.code == LAErrorTouchIDNotEnrolled) { + result(@YES); + return; #pragma clang diagnostic pop - } } - - result(@NO); + } + + result(@NO); } - (void)getEnrolledBiometrics:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - NSMutableArray *biometrics = [[NSMutableArray alloc] init]; - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - error:&authError]) { - if (authError == nil) { - if (@available(iOS 11, *)) { - if (context.biometryType == LABiometryTypeFaceID) { - [biometrics addObject:@"face"]; - } else if (context.biometryType == LABiometryTypeTouchID) { - [biometrics addObject:@"fingerprint"]; - } - } else { - [biometrics addObject:@"fingerprint"]; - } + LAContext *context = self.createAuthContext; + NSError *authError = nil; + NSMutableArray *biometrics = [[NSMutableArray alloc] init]; + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&authError]) { + if (authError == nil) { + if (@available(iOS 11, *)) { + if (context.biometryType == LABiometryTypeFaceID) { + [biometrics addObject:@"face"]; + } else if (context.biometryType == LABiometryTypeTouchID) { + [biometrics addObject:@"fingerprint"]; } + } else { + [biometrics addObject:@"fingerprint"]; + } } - result(biometrics); + } + result(biometrics); } - (void)authenticateWithBiometrics:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - self.lastCallArgs = nil; - self.lastResult = nil; - context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] - ? nil - : arguments[@"localizedFallbackTitle"]; - - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - error:&authError]) { - [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; - } + LAContext *context = self.createAuthContext; + NSError *authError = nil; + self.lastCallArgs = nil; + self.lastResult = nil; + context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] + ? nil + : arguments[@"localizedFallbackTitle"]; + + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&authError]) { + [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + localizedReason:arguments[@"localizedReason"] + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + flutterArguments:arguments + flutterResult:result]; + }); + }]; + } else { + [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; + } } - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - _lastCallArgs = nil; - _lastResult = nil; - context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] - ? nil - : arguments[@"localizedFallbackTitle"]; - - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { - [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; - } + LAContext *context = self.createAuthContext; + NSError *authError = nil; + _lastCallArgs = nil; + _lastResult = nil; + context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] + ? nil + : arguments[@"localizedFallbackTitle"]; + + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { + [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication + localizedReason:arguments[@"localizedReason"] + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + flutterArguments:arguments + flutterResult:result]; + }); + }]; + } else { + [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; + } } - (void)handleAuthReplyWithSuccess:(BOOL)success error:(NSError *)error flutterArguments:(NSDictionary *)arguments flutterResult:(FlutterResult)result { - NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); - if (success) { - result(@YES); - } else { - switch (error.code) { - case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; - } else { - result(@NO); - } - return; + NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); + if (success) { + result(@YES); + } else { + switch (error.code) { + case LAErrorSystemCancel: + if ([arguments[@"stickyAuth"] boolValue]) { + self->_lastCallArgs = arguments; + self->_lastResult = result; + } else { + result(@NO); + } + return; #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. - case LAErrorTouchIDNotAvailable: - case LAErrorTouchIDNotEnrolled: - case LAErrorTouchIDLockout: + // 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 LAErrorUserFallback: + case LAErrorPasscodeNotSet: + case LAErrorAuthenticationFailed: + [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; + return; } + } } - - (void)handleErrors:(NSError *)authError - flutterArguments:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { - NSString *errorCode = @"NotAvailable"; - switch (authError.code) { - case LAErrorPasscodeNotSet: - case LAErrorBiometryNotEnrolled: - if ([arguments[@"useErrorDialogs"] boolValue]) { - [self alertMessage:arguments[@"goToSettingDescriptionIOS"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:arguments[@"goToSetting"]]; - return; - } - errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; - break; - case LAErrorBiometryLockout: - [self alertMessage:arguments[@"lockOut"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:nil]; - return; - } - result([FlutterError errorWithCode:errorCode - message:authError.localizedDescription - details:authError.domain]); + flutterArguments:(NSDictionary *)arguments + withFlutterResult:(FlutterResult)result { + NSString *errorCode = @"NotAvailable"; + switch (authError.code) { + case LAErrorPasscodeNotSet: + case LAErrorBiometryNotEnrolled: + if ([arguments[@"useErrorDialogs"] boolValue]) { + [self alertMessage:arguments[@"goToSettingDescriptionIOS"] + firstButton:arguments[@"okButton"] + flutterResult:result + additionalButton:arguments[@"goToSetting"]]; + return; + } + errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; + break; + case LAErrorBiometryLockout: + [self alertMessage:arguments[@"lockOut"] + firstButton:arguments[@"okButton"] + flutterResult:result + additionalButton:nil]; + return; + } + result([FlutterError errorWithCode:errorCode + message:authError.localizedDescription + details:authError.domain]); } #pragma mark - AppDelegate - (void)applicationDidBecomeActive:(UIApplication *)application { - if (self.lastCallArgs != nil && self.lastResult != nil) { - [self authenticateWithBiometrics:_lastCallArgs withFlutterResult:self.lastResult]; - } + if (self.lastCallArgs != nil && self.lastResult != nil) { + [self authenticateWithBiometrics:_lastCallArgs withFlutterResult:self.lastResult]; + } } @end From 699a6122ed10a4b67fccdae81d66ce0b0a696f85 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 14:28:56 -0800 Subject: [PATCH 16/23] change incorrect versionning --- packages/local_auth/local_auth/CHANGELOG.md | 4 ---- packages/local_auth/local_auth/pubspec.yaml | 2 +- packages/local_auth/local_auth_ios/CHANGELOG.md | 4 ++++ packages/local_auth/local_auth_ios/pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index 41a86c558148..34e26efef238 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -2,10 +2,6 @@ * Updates minimum Flutter version to 2.10. -## 2.1.3 - -* Fixes biometric authentication failing silently. - ## 2.1.2 * Fixes avoid_redundant_argument_values lint warnings and minor typos. diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index cfb42c849dfd..133df06d43b0 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -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: 2.1.3 +version: 2.1.2 environment: sdk: ">=2.14.0 <3.0.0" 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/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" From b483520239a6557b823e86cbb91438ebe6090603 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 15:51:52 -0800 Subject: [PATCH 17/23] fix test --- .../example/ios/RunnerTests/FLTLocalAuthPluginTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 34c67d932b5b..ecdbfa2968c2 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 @@ -218,7 +218,7 @@ - (void)testSystemCancelledWithoutStickyAuth { result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); + XCTAssertFalse([result boolValue]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; From 737bbd1cb5e9aa35230f54c5e73f1ef8c8037659 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 13 Dec 2022 16:35:12 -0800 Subject: [PATCH 18/23] add back macro --- .../ios/Classes/FLTLocalAuthPlugin.m | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 d11695b7fcec..b790c3d21a9e 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -247,7 +247,12 @@ - (void)handleErrors:(NSError *)authError NSString *errorCode = @"NotAvailable"; switch (authError.code) { case LAErrorPasscodeNotSet: - case LAErrorBiometryNotEnrolled: +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when + // iOS 10 support is dropped. The values are the same, only the names have changed. + case LAErrorTouchIDNotEnrolled: +#pragma clang diagnostic pop if ([arguments[@"useErrorDialogs"] boolValue]) { [self alertMessage:arguments[@"goToSettingDescriptionIOS"] firstButton:arguments[@"okButton"] @@ -257,7 +262,12 @@ - (void)handleErrors:(NSError *)authError } errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; break; - case LAErrorBiometryLockout: +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when + // iOS 10 support is dropped. The values are the same, only the names have changed. + case LAErrorTouchIDLockout: +#pragma clang diagnostic pop [self alertMessage:arguments[@"lockOut"] firstButton:arguments[@"okButton"] flutterResult:result From f62195a7b4d12afd402c6f8d82999099a7b42a2a Mon Sep 17 00:00:00 2001 From: louisehsu Date: Wed, 14 Dec 2022 15:50:33 -0800 Subject: [PATCH 19/23] fixed up tests, removed unnecessary assertions, replaced isAMemberOf --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 12 ++++-------- .../ios/Classes/FLTLocalAuthPlugin.m | 16 ++++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) 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 ecdbfa2968c2..e7db366dfcd1 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 @@ -140,7 +140,7 @@ - (void)testFailedAuthWithBiometrics { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + XCTAssertTrue([result isKindOfClass:[FlutterError class]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -256,7 +256,7 @@ - (void)testFailedAuthWithoutBiometrics { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); + XCTAssertTrue([result isKindOfClass:[FlutterError class]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -279,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:LAErrorUserFallback userInfo:nil]); + reply(YES, nil); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -297,8 +297,6 @@ - (void)testLocalizedFallbackTitle { [plugin handleMethodCall:call result:^(id _Nullable result) { OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -320,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:LAErrorUserFallback userInfo:nil]); + reply(YES, nil); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -336,8 +334,6 @@ - (void)testSkippedLocalizedFallbackTitle { [plugin handleMethodCall:call result:^(id _Nullable result) { OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); [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 b790c3d21a9e..4ac9b45507d9 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -216,14 +216,6 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success result(@YES); } else { switch (error.code) { - case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; - } else { - result(@NO); - } - return; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in these constants when @@ -237,6 +229,14 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success case LAErrorAuthenticationFailed: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; + case LAErrorSystemCancel: + if ([arguments[@"stickyAuth"] boolValue]) { + self->_lastCallArgs = arguments; + self->_lastResult = result; + } else { + result(@NO); + } + return; } } } From 56d49690601e27cf0945a68e4f2f7d865461467c Mon Sep 17 00:00:00 2001 From: louisehsu Date: Wed, 14 Dec 2022 16:29:06 -0800 Subject: [PATCH 20/23] add back error for unknown error codes --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 40 ++++++++++++++++++- .../ios/Classes/FLTLocalAuthPlugin.m | 1 + 2 files changed, 40 insertions(+), 1 deletion(-) 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 e7db366dfcd1..8f46bee82ad9 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 @@ -146,6 +146,44 @@ - (void)testFailedAuthWithBiometrics { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } +- (void)testFailedWithUnknownErrorCode { + 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:LAErrorTouchIDNotEnrolled 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)testFailedWithKnownErrorCode { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); @@ -162,7 +200,7 @@ - (void)testFailedWithKnownErrorCode { 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:LAErrorBiometryNotEnrolled userInfo:nil]); + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) 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 4ac9b45507d9..4d982549643d 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -238,6 +238,7 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success } return; } + [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; } } From 99e34ca0de4a58974675c79008998e7976acaaca Mon Sep 17 00:00:00 2001 From: louisehsu Date: Wed, 14 Dec 2022 16:31:12 -0800 Subject: [PATCH 21/23] tests --- .../example/ios/RunnerTests/FLTLocalAuthPluginTests.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 8f46bee82ad9..4075cb9e6550 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 @@ -162,7 +162,7 @@ - (void)testFailedWithUnknownErrorCode { 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:LAErrorTouchIDNotEnrolled userInfo:nil]); + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -200,7 +200,7 @@ - (void)testFailedWithKnownErrorCode { 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:LAErrorTouchIDNotEnrolled userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) From c70c8bc350e6abe481881af524b5cd718fb7c7e2 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Wed, 14 Dec 2022 17:58:51 -0800 Subject: [PATCH 22/23] changed enum to something thats not deprecated --- .../example/ios/RunnerTests/FLTLocalAuthPluginTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4075cb9e6550..36733626d61d 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 @@ -200,7 +200,7 @@ - (void)testFailedWithKnownErrorCode { 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:LAErrorTouchIDNotEnrolled userInfo:nil]); + reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) From 42682f24e23aa4da80e060141dcc35088640b143 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 15 Dec 2022 12:03:16 -0800 Subject: [PATCH 23/23] remove redundant test --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 38 ------------------- 1 file changed, 38 deletions(-) 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 36733626d61d..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 @@ -184,44 +184,6 @@ - (void)testFailedWithUnknownErrorCode { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } -- (void)testFailedWithKnownErrorCode { - 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)testSystemCancelledWithoutStickyAuth { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]);