[local_auth] Fix callback thread handling (#3778)

Ensure that all auth replies, which are sent on an internal framework queue per documentation, are dispatched back to the main thread for handling, as all resulting operations (method channel callbacks, display of UI) are things that must be done on the main thread

In order to test this, sets up local_auth with XCTest-based tests, and adds the ability to inject a mock LAContext. (This does not do full unit test backfill, to limit the scope of the PR.)

Fixes flutter/flutter#47465
diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md
index 7e86672..429e217 100644
--- a/packages/local_auth/CHANGELOG.md
+++ b/packages/local_auth/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.1.3
+
+* Fix crashes due to threading issues in iOS implementation.
+
 ## 1.1.2
 
 * Update Jetpack dependencies to latest stable versions.
diff --git a/packages/local_auth/example/ios/Podfile b/packages/local_auth/example/ios/Podfile
index f7d6a5e..6549735 100644
--- a/packages/local_auth/example/ios/Podfile
+++ b/packages/local_auth/example/ios/Podfile
@@ -29,6 +29,12 @@
 
 target 'Runner' do
   flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+
+  target 'XCTests' do
+    inherit! :search_paths
+
+    pod 'OCMock', '3.5'
+  end
 end
 
 post_install do |installer|
diff --git a/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj
index 8960fe4..708c643 100644
--- a/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj
@@ -9,18 +9,26 @@
 /* Begin PBXBuildFile section */
 		0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */; };
 		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		3398D2E426164AD8005A052F /* FLTLocalAuthPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */; };
 		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
-		3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
-		3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
-		9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
-		9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
 		97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+		D6C28B8B9E1BDEC22D03304F /* libPods-XCTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4EB178B442E18480B8054307 /* libPods-XCTests.a */; };
 /* End PBXBuildFile section */
 
+/* Begin PBXContainerItemProxy section */
+		3398D2D226163948005A052F /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+			remoteInfo = Runner;
+		};
+/* End PBXContainerItemProxy section */
+
 /* Begin PBXCopyFilesBuildPhase section */
 		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
 			isa = PBXCopyFilesBuildPhase;
@@ -28,8 +36,6 @@
 			dstPath = "";
 			dstSubfolderSpec = 10;
 			files = (
-				3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
-				9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
@@ -39,15 +45,20 @@
 /* Begin PBXFileReference section */
 		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
 		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+		3398D2CD26163948005A052F /* XCTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XCTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		3398D2D126163948005A052F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		3398D2DC261649CD005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; };
+		3398D2DF26164A03005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; };
+		3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLTLocalAuthPluginTests.m; path = ../../../ios/Tests/FLTLocalAuthPluginTests.m; sourceTree = "<group>"; };
 		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
-		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
+		4EB178B442E18480B8054307 /* libPods-XCTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-XCTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
 		7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
 		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+		81D8AFFB31AECDACBC5B11F8 /* Pods-XCTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-XCTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-XCTests/Pods-XCTests.debug.xcconfig"; sourceTree = "<group>"; };
 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
 		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
-		9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
 		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
 		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
@@ -56,15 +67,22 @@
 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		EB36DF6C3F25E00DF4175422 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+		F6BEBFD3433B1712765D62F7 /* Pods-XCTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-XCTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-XCTests/Pods-XCTests.release.xcconfig"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
+		3398D2CA26163948005A052F /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				D6C28B8B9E1BDEC22D03304F /* libPods-XCTests.a in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		97C146EB1CF9000F007C117D /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
-				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
 				0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -72,12 +90,19 @@
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+		3398D2CE26163948005A052F /* XCTests */ = {
+			isa = PBXGroup;
+			children = (
+				3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */,
+				3398D2D126163948005A052F /* Info.plist */,
+			);
+			path = XCTests;
+			sourceTree = "<group>";
+		};
 		9740EEB11CF90186004384FC /* Flutter */ = {
 			isa = PBXGroup;
 			children = (
-				3B80C3931E831B6300D905FE /* App.framework */,
 				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
-				9740EEBA1CF902C7004384FC /* Flutter.framework */,
 				9740EEB21CF90195004384FC /* Debug.xcconfig */,
 				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
 				9740EEB31CF90195004384FC /* Generated.xcconfig */,
@@ -90,6 +115,7 @@
 			children = (
 				9740EEB11CF90186004384FC /* Flutter */,
 				97C146F01CF9000F007C117D /* Runner */,
+				3398D2CE26163948005A052F /* XCTests */,
 				97C146EF1CF9000F007C117D /* Products */,
 				F8CC53B854B121315C7319D2 /* Pods */,
 				E2D5FA899A019BD3E0DB0917 /* Frameworks */,
@@ -100,6 +126,7 @@
 			isa = PBXGroup;
 			children = (
 				97C146EE1CF9000F007C117D /* Runner.app */,
+				3398D2CD26163948005A052F /* XCTests.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -131,7 +158,10 @@
 		E2D5FA899A019BD3E0DB0917 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				3398D2DF26164A03005A052F /* liblocal_auth.a */,
+				3398D2DC261649CD005A052F /* liblocal_auth.a */,
 				9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */,
+				4EB178B442E18480B8054307 /* libPods-XCTests.a */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -141,6 +171,8 @@
 			children = (
 				EB36DF6C3F25E00DF4175422 /* Pods-Runner.debug.xcconfig */,
 				658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */,
+				81D8AFFB31AECDACBC5B11F8 /* Pods-XCTests.debug.xcconfig */,
+				F6BEBFD3433B1712765D62F7 /* Pods-XCTests.release.xcconfig */,
 			);
 			name = Pods;
 			sourceTree = "<group>";
@@ -148,6 +180,25 @@
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
+		3398D2CC26163948005A052F /* XCTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "XCTests" */;
+			buildPhases = (
+				B5AF6C7A6759E6F38749E537 /* [CP] Check Pods Manifest.lock */,
+				3398D2C926163948005A052F /* Sources */,
+				3398D2CA26163948005A052F /* Frameworks */,
+				3398D2CB26163948005A052F /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				3398D2D326163948005A052F /* PBXTargetDependency */,
+			);
+			name = XCTests;
+			productName = XCTests;
+			productReference = 3398D2CD26163948005A052F /* XCTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 		97C146ED1CF9000F007C117D /* Runner */ = {
 			isa = PBXNativeTarget;
 			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -159,7 +210,6 @@
 				97C146EC1CF9000F007C117D /* Resources */,
 				9705A1C41CF9048500538489 /* Embed Frameworks */,
 				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
-				16CF73924D0A9C13B2100A83 /* [CP] Embed Pods Frameworks */,
 			);
 			buildRules = (
 			);
@@ -179,6 +229,11 @@
 				LastUpgradeCheck = 1100;
 				ORGANIZATIONNAME = "The Flutter Authors";
 				TargetAttributes = {
+					3398D2CC26163948005A052F = {
+						CreatedOnToolsVersion = 12.4;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 97C146ED1CF9000F007C117D;
+					};
 					97C146ED1CF9000F007C117D = {
 						CreatedOnToolsVersion = 7.3.1;
 					};
@@ -198,11 +253,19 @@
 			projectRoot = "";
 			targets = (
 				97C146ED1CF9000F007C117D /* Runner */,
+				3398D2CC26163948005A052F /* XCTests */,
 			);
 		};
 /* End PBXProject section */
 
 /* Begin PBXResourcesBuildPhase section */
+		3398D2CB26163948005A052F /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		97C146EC1CF9000F007C117D /* Resources */ = {
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -217,21 +280,6 @@
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXShellScriptBuildPhase section */
-		16CF73924D0A9C13B2100A83 /* [CP] Embed Pods Frameworks */ = {
-			isa = PBXShellScriptBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			inputPaths = (
-			);
-			name = "[CP] Embed Pods Frameworks";
-			outputPaths = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-			shellPath = /bin/sh;
-			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
-			showEnvVarsInLog = 0;
-		};
 		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
@@ -244,7 +292,7 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
-			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
 		};
 		9740EEB61CF901F6004384FC /* Run Script */ = {
 			isa = PBXShellScriptBuildPhase;
@@ -278,9 +326,39 @@
 			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
 			showEnvVarsInLog = 0;
 		};
+		B5AF6C7A6759E6F38749E537 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-XCTests-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
 /* End PBXShellScriptBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
+		3398D2C926163948005A052F /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				3398D2E426164AD8005A052F /* FLTLocalAuthPluginTests.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		97C146EA1CF9000F007C117D /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -293,6 +371,14 @@
 		};
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXTargetDependency section */
+		3398D2D326163948005A052F /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 97C146ED1CF9000F007C117D /* Runner */;
+			targetProxy = 3398D2D226163948005A052F /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
 /* Begin PBXVariantGroup section */
 		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
 			isa = PBXVariantGroup;
@@ -313,9 +399,55 @@
 /* End PBXVariantGroup section */
 
 /* Begin XCBuildConfiguration section */
+		3398D2D526163948005A052F /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 81D8AFFB31AECDACBC5B11F8 /* Pods-XCTests.debug.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = XCTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.XCTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Debug;
+		};
+		3398D2D626163948005A052F /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = F6BEBFD3433B1712765D62F7 /* Pods-XCTests.release.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = XCTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.XCTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Release;
+		};
 		97C147031CF9000F007C117D /* Debug */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
@@ -372,7 +504,6 @@
 		};
 		97C147041CF9000F007C117D /* Release */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
@@ -466,6 +597,15 @@
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
+		3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "XCTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				3398D2D526163948005A052F /* Debug */,
+				3398D2D626163948005A052F /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (
diff --git a/packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
index 1d526a1..919434a 100644
--- a/packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ b/packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -2,6 +2,6 @@
 <Workspace
    version = "1.0">
    <FileRef
-      location = "group:Runner.xcodeproj">
+      location = "self:">
    </FileRef>
 </Workspace>
diff --git a/packages/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 3bb3697..5b12c3a 100644
--- a/packages/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -37,6 +37,16 @@
          </BuildableReference>
       </MacroExpansion>
       <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "3398D2CC26163948005A052F"
+               BuildableName = "XCTests.xctest"
+               BlueprintName = "XCTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
       </Testables>
    </TestAction>
    <LaunchAction
diff --git a/packages/local_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/local_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/local_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/local_auth/example/ios/XCTests/Info.plist b/packages/local_auth/example/ios/XCTests/Info.plist
new file mode 100644
index 0000000..64d65ca
--- /dev/null
+++ b/packages/local_auth/example/ios/XCTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m
index 40a14b9..a00c7ee 100644
--- a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m
+++ b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m
@@ -6,11 +6,17 @@
 #import "FLTLocalAuthPlugin.h"
 
 @interface FLTLocalAuthPlugin ()
-@property(copy, nullable) NSDictionary<NSString *, NSNumber *> *lastCallArgs;
-@property(nullable) FlutterResult lastResult;
+@property(nonatomic, copy, nullable) NSDictionary<NSString *, NSNumber *> *lastCallArgs;
+@property(nonatomic, nullable) FlutterResult lastResult;
+// For unit tests to inject dummy LAContext instances that will be used when a new context would
+// normally be created. Each call to createAuthContext will remove the current first element from
+// the array.
+- (void)setAuthContextOverrides:(NSArray<LAContext *> *)authContexts;
 @end
 
-@implementation FLTLocalAuthPlugin
+@implementation FLTLocalAuthPlugin {
+  NSMutableArray<LAContext *> *_authContextOverrides;
+}
 
 + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
   FlutterMethodChannel *channel =
@@ -40,6 +46,19 @@
 
 #pragma mark Private Methods
 
+- (void)setAuthContextOverrides:(NSArray<LAContext *> *)authContexts {
+  _authContextOverrides = [authContexts mutableCopy];
+}
+
+- (LAContext *)createAuthContext {
+  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
@@ -75,7 +94,7 @@
 }
 
 - (void)getAvailableBiometrics:(FlutterResult)result {
-  LAContext *context = [[LAContext alloc] init];
+  LAContext *context = self.createAuthContext;
   NSError *authError = nil;
   NSMutableArray<NSString *> *biometrics = [[NSMutableArray<NSString *> alloc] init];
   if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
@@ -96,9 +115,10 @@
   }
   result(biometrics);
 }
+
 - (void)authenticateWithBiometrics:(NSDictionary *)arguments
                  withFlutterResult:(FlutterResult)result {
-  LAContext *context = [[LAContext alloc] init];
+  LAContext *context = self.createAuthContext;
   NSError *authError = nil;
   self.lastCallArgs = nil;
   self.lastResult = nil;
@@ -109,27 +129,12 @@
     [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
             localizedReason:arguments[@"localizedReason"]
                       reply:^(BOOL success, NSError *error) {
-                        if (success) {
-                          result(@YES);
-                        } else {
-                          switch (error.code) {
-                            case LAErrorPasscodeNotSet:
-                            case LAErrorTouchIDNotAvailable:
-                            case LAErrorTouchIDNotEnrolled:
-                            case LAErrorTouchIDLockout:
-                              [self handleErrors:error
-                                   flutterArguments:arguments
-                                  withFlutterResult:result];
-                              return;
-                            case LAErrorSystemCancel:
-                              if ([arguments[@"stickyAuth"] boolValue]) {
-                                self.lastCallArgs = arguments;
-                                self.lastResult = result;
-                                return;
-                              }
-                          }
-                          result(@NO);
-                        }
+                        dispatch_async(dispatch_get_main_queue(), ^{
+                          [self handleAuthReplyWithSuccess:success
+                                                     error:error
+                                          flutterArguments:arguments
+                                             flutterResult:result];
+                        });
                       }];
   } else {
     [self handleErrors:authError flutterArguments:arguments withFlutterResult:result];
@@ -137,7 +142,7 @@
 }
 
 - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result {
-  LAContext *context = [[LAContext alloc] init];
+  LAContext *context = self.createAuthContext;
   NSError *authError = nil;
   _lastCallArgs = nil;
   _lastResult = nil;
@@ -148,27 +153,12 @@
       [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication
               localizedReason:arguments[@"localizedReason"]
                         reply:^(BOOL success, NSError *error) {
-                          if (success) {
-                            result(@YES);
-                          } else {
-                            switch (error.code) {
-                              case LAErrorPasscodeNotSet:
-                              case LAErrorTouchIDNotAvailable:
-                              case LAErrorTouchIDNotEnrolled:
-                              case LAErrorTouchIDLockout:
-                                [self handleErrors:error
-                                     flutterArguments:arguments
-                                    withFlutterResult:result];
-                                return;
-                              case LAErrorSystemCancel:
-                                if ([arguments[@"stickyAuth"] boolValue]) {
-                                  self->_lastCallArgs = arguments;
-                                  self->_lastResult = result;
-                                  return;
-                                }
-                            }
-                            result(@NO);
-                          }
+                          dispatch_async(dispatch_get_main_queue(), ^{
+                            [self handleAuthReplyWithSuccess:success
+                                                       error:error
+                                            flutterArguments:arguments
+                                               flutterResult:result];
+                          });
                         }];
     } else {
       [self handleErrors:authError flutterArguments:arguments withFlutterResult:result];
@@ -178,6 +168,32 @@
   }
 }
 
+- (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 LAErrorPasscodeNotSet:
+      case LAErrorTouchIDNotAvailable:
+      case LAErrorTouchIDNotEnrolled:
+      case LAErrorTouchIDLockout:
+        [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 {
diff --git a/packages/local_auth/ios/Tests/FLTLocalAuthPluginTests.m b/packages/local_auth/ios/Tests/FLTLocalAuthPluginTests.m
new file mode 100644
index 0000000..97e78e2
--- /dev/null
+++ b/packages/local_auth/ios/Tests/FLTLocalAuthPluginTests.m
@@ -0,0 +1,189 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@import LocalAuthentication;
+@import XCTest;
+
+#import <OCMock/OCMock.h>
+
+#if __has_include(<local_auth/FLTLocalAuthPlugin.h>)
+#import <local_auth/FLTLocalAuthPlugin.h>
+#else
+@import local_auth;
+#endif
+
+// Private API needed for tests.
+@interface FLTLocalAuthPlugin (Test)
+- (void)setAuthContextOverrides:(NSArray<LAContext*>*)authContexts;
+@end
+
+// Set a long timeout to avoid flake due to slow CI.
+static const NSTimeInterval kTimeout = 30.0;
+
+@interface FLTLocalAuthPluginTests : XCTestCase
+@end
+
+@implementation FLTLocalAuthPluginTests
+
+- (void)setUp {
+  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];
+}
+
+- (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];
+}
+
+- (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:99 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 isKindOfClass:[NSNumber class]]);
+                      XCTAssertFalse([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: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:[NSNumber class]]);
+                      XCTAssertFalse([result boolValue]);
+                      [expectation fulfill];
+                    }];
+  [self waitForExpectationsWithTimeout:kTimeout handler:nil];
+}
+
+@end
diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml
index e9d406d..5e2dbb4 100644
--- a/packages/local_auth/pubspec.yaml
+++ b/packages/local_auth/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Flutter plugin for Android and iOS devices to allow local
   authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern.
 homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth
-version: 1.1.2
+version: 1.1.3
 
 flutter:
   plugin: