[local_auth] Fix failed biometric authentication not throwing error (#6821)

* fix failed biometric authentication not throwing error + tests

* fix test names

* Revert "fix test names"

This reverts commit 89ba69ccc33c37b98092a5fbfa5c71ebc23cd468.

* Revert "fix failed biometric authentication not throwing error + tests"

This reverts commit 684790a7c7756c963ac3aa3b6e4de48cc9b33df5.

* fix authentication not throwing error + tests

* fix test name

* auto format

* cr fixes

* addressed pr comments

* formatting

* formatting

* formatting

* format attempt

* format attempt

* formatting fixes

* change incorrect versionning

* fix test

* add back macro

* fixed up tests, removed unnecessary assertions, replaced isAMemberOf

* add back error for unknown error codes

* tests

* changed enum to something thats not deprecated

* remove redundant test
diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md
index e67f2a4..eb95e2f 100644
--- a/packages/local_auth/local_auth_ios/CHANGELOG.md
+++ b/packages/local_auth/local_auth_ios/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.0.11
+
+* Fixes issue where failed authentication was failing silently
+
 ## 1.0.10
 
 * Updates imports for `prefer_relative_imports`.
diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m
index 50dbb1a..51c94cc 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 (^reply)(BOOL, NSError *);
     [invocation getArgument:&reply atIndex:4];
     dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
-      reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]);
+      reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]);
     });
   };
   OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
@@ -140,6 +140,83 @@
   [plugin handleMethodCall:call
                     result:^(id _Nullable result) {
                       XCTAssertTrue([NSThread isMainThread]);
+                      XCTAssertTrue([result isKindOfClass:[FlutterError class]]);
+                      [expectation fulfill];
+                    }];
+  [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:99 userInfo:nil]);
+    });
+  };
+  OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
+      .andDo(backgroundThreadReplyCaller);
+
+  FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
+                                                              arguments:@{
+                                                                @"biometricOnly" : @(NO),
+                                                                @"localizedReason" : reason,
+                                                              }];
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
+  [plugin handleMethodCall:call
+                    result:^(id _Nullable result) {
+                      XCTAssertTrue([NSThread isMainThread]);
+                      XCTAssertTrue([result isKindOfClass:[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]]);
                       XCTAssertFalse([result boolValue]);
                       [expectation fulfill];
@@ -163,7 +240,7 @@
     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 +256,7 @@
   [plugin handleMethodCall:call
                     result:^(id _Nullable result) {
                       XCTAssertTrue([NSThread isMainThread]);
-                      XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
-                      XCTAssertFalse([result boolValue]);
+                      XCTAssertTrue([result isKindOfClass:[FlutterError class]]);
                       [expectation fulfill];
                     }];
   [self waitForExpectationsWithTimeout:kTimeout handler:nil];
@@ -203,7 +279,7 @@
     void (^reply)(BOOL, NSError *);
     [invocation getArgument:&reply atIndex:4];
     dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
-      reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]);
+      reply(YES, nil);
     });
   };
   OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
@@ -220,10 +296,7 @@
   XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
   [plugin handleMethodCall:call
                     result:^(id _Nullable result) {
-                      XCTAssertTrue([NSThread isMainThread]);
-                      XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
                       OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]);
-                      XCTAssertFalse([result boolValue]);
                       [expectation fulfill];
                     }];
   [self waitForExpectationsWithTimeout:kTimeout handler:nil];
@@ -245,7 +318,7 @@
     void (^reply)(BOOL, NSError *);
     [invocation getArgument:&reply atIndex:4];
     dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
-      reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]);
+      reply(YES, nil);
     });
   };
   OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
@@ -260,10 +333,7 @@
   XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
   [plugin handleMethodCall:call
                     result:^(id _Nullable result) {
-                      XCTAssertTrue([NSThread isMainThread]);
-                      XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
                       OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]);
-                      XCTAssertFalse([result boolValue]);
                       [expectation fulfill];
                     }];
   [self waitForExpectationsWithTimeout:kTimeout handler:nil];
diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m
index 8f61fec..4d98254 100644
--- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m
+++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m
@@ -216,26 +216,29 @@
     result(@YES);
   } else {
     switch (error.code) {
-      case LAErrorPasscodeNotSet:
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
-      // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in these constants when
-      // iOS 10 support is dropped. The values are the same, only the names have changed.
+        // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in these constants when
+        // iOS 10 support is dropped. The values are the same, only the names have changed.
       case LAErrorTouchIDNotAvailable:
       case LAErrorTouchIDNotEnrolled:
       case LAErrorTouchIDLockout:
 #pragma clang diagnostic pop
       case LAErrorUserFallback:
+      case LAErrorPasscodeNotSet:
+      case LAErrorAuthenticationFailed:
         [self handleErrors:error flutterArguments:arguments withFlutterResult:result];
         return;
       case LAErrorSystemCancel:
         if ([arguments[@"stickyAuth"] boolValue]) {
           self->_lastCallArgs = arguments;
           self->_lastResult = result;
-          return;
+        } else {
+          result(@NO);
         }
+        return;
     }
-    result(@NO);
+    [self handleErrors:error flutterArguments:arguments withFlutterResult:result];
   }
 }
 
diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml
index 9cdeef9..d6cab0f 100644
--- a/packages/local_auth/local_auth_ios/pubspec.yaml
+++ b/packages/local_auth/local_auth_ios/pubspec.yaml
@@ -2,7 +2,7 @@
 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"