diff --git a/packages/local_auth/local_auth_darwin/CHANGELOG.md b/packages/local_auth/local_auth_darwin/CHANGELOG.md index 67a409017df..a98cdea5b17 100644 --- a/packages/local_auth/local_auth_darwin/CHANGELOG.md +++ b/packages/local_auth/local_auth_darwin/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.6.0 + +* Provides more specific error codes on iOS for authentication failures. + * `LockedOut` is now returned for biometric lockout. + * `UserCancelled` is now returned when the user cancels the prompt. + * `UserFallback` is now returned when the user selects the fallback option. + ## 1.5.0 * Converts implementation to Swift. diff --git a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift index 287d13b8193..096b54ec1ee 100644 --- a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift +++ b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift @@ -266,6 +266,99 @@ class LocalAuthPluginTests: XCTestCase { self.waitForExpectations(timeout: timeout) } + @MainActor + func testFailedAuthWithErrorUserCancelled() { + let stubAuthContext = StubAuthContext() + let alertFactory = StubAlertFactory() + let viewProvider = StubViewProvider() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: alertFactory, + viewProvider: viewProvider) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError( + domain: "LocalAuthentication", code: LAError.userCancel.rawValue) + + let expectation = expectation(description: "Result is called for user cancel") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + XCTAssertTrue(Thread.isMainThread) + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .errorUserCancelled) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorUserFallback() { + let stubAuthContext = StubAuthContext() + let alertFactory = StubAlertFactory() + let viewProvider = StubViewProvider() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: alertFactory, + viewProvider: viewProvider) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError( + domain: "LocalAuthentication", code: LAError.userFallback.rawValue) + + let expectation = expectation(description: "Result is called for user fallback") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + XCTAssertTrue(Thread.isMainThread) + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .errorUserFallback) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorBiometricNotAvailable() { + let stubAuthContext = StubAuthContext() + let alertFactory = StubAlertFactory() + let viewProvider = StubViewProvider() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: alertFactory, + viewProvider: viewProvider) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryNotAvailable.rawValue) + + let expectation = expectation(description: "Result is called for biometric not available") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + XCTAssertTrue(Thread.isMainThread) + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .errorBiometricNotAvailable) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + @MainActor func testFailedWithUnknownErrorCode() { let stubAuthContext = StubAuthContext() diff --git a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/LocalAuthPlugin.swift b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/LocalAuthPlugin.swift index 4e43ebf37d0..e42cc3ec7bf 100644 --- a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/LocalAuthPlugin.swift +++ b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/LocalAuthPlugin.swift @@ -381,6 +381,12 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch return } result = errorCode == .passcodeNotSet ? .errorPasscodeNotSet : .errorNotEnrolled + case .userCancel: + result = .errorUserCancelled + case .userFallback: + result = .errorUserFallback + case .biometryNotAvailable: + result = .errorBiometricNotAvailable case .biometryLockout: DispatchQueue.main.async { [weak self] in self?.showAlert( diff --git a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/messages.g.swift b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/messages.g.swift index de92c9c4886..a0706af4fe1 100644 --- a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/messages.g.swift +++ b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/messages.g.swift @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// Autogenerated from Pigeon (v25.5.0), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -141,6 +141,12 @@ enum AuthResult: Int { case errorNotEnrolled = 3 /// No passcode is set. case errorPasscodeNotSet = 4 + /// The user cancelled the authentication. + case errorUserCancelled = 5 + /// The user tapped the "Enter Password" fallback. + case errorUserFallback = 6 + /// The user biometrics is disabled. + case errorBiometricNotAvailable = 7 } /// Pigeon equivalent of the subset of BiometricType used by iOS. diff --git a/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart b/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart index 935d274efa6..a3be6ab5e26 100644 --- a/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart +++ b/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart @@ -75,6 +75,21 @@ class LocalAuthDarwin extends LocalAuthPlatform { code: 'PasscodeNotSet', message: resultDetails.errorMessage, details: resultDetails.errorDetails); + case AuthResult.errorUserCancelled: + throw PlatformException( + code: 'UserCancelled', + message: resultDetails.errorMessage, + details: resultDetails.errorDetails); + case AuthResult.errorBiometricNotAvailable: + throw PlatformException( + code: 'BiometricNotAvailable', + message: resultDetails.errorMessage, + details: resultDetails.errorDetails); + case AuthResult.errorUserFallback: + throw PlatformException( + code: 'UserFallback', + message: resultDetails.errorMessage, + details: resultDetails.errorDetails); } } diff --git a/packages/local_auth/local_auth_darwin/lib/src/messages.g.dart b/packages/local_auth/local_auth_darwin/lib/src/messages.g.dart index ba9584ae61c..fa3a4987aeb 100644 --- a/packages/local_auth/local_auth_darwin/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_darwin/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// Autogenerated from Pigeon (v25.5.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -49,6 +49,15 @@ enum AuthResult { /// No passcode is set. errorPasscodeNotSet, + + /// The user cancelled the authentication. + errorUserCancelled, + + /// The user tapped the "Enter Password" fallback. + errorUserFallback, + + /// The user biometrics is disabled. + errorBiometricNotAvailable, } /// Pigeon equivalent of the subset of BiometricType used by iOS. diff --git a/packages/local_auth/local_auth_darwin/pigeons/messages.dart b/packages/local_auth/local_auth_darwin/pigeons/messages.dart index 78fad5509e9..9c89bfd3682 100644 --- a/packages/local_auth/local_auth_darwin/pigeons/messages.dart +++ b/packages/local_auth/local_auth_darwin/pigeons/messages.dart @@ -49,6 +49,15 @@ enum AuthResult { /// No passcode is set. errorPasscodeNotSet, + + /// The user cancelled the authentication. + errorUserCancelled, + + /// The user tapped the "Enter Password" fallback. + errorUserFallback, + + /// The user biometrics is disabled. + errorBiometricNotAvailable, } class AuthOptions { diff --git a/packages/local_auth/local_auth_darwin/pubspec.yaml b/packages/local_auth/local_auth_darwin/pubspec.yaml index 8448351337f..3c00fc879ab 100644 --- a/packages/local_auth/local_auth_darwin/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_darwin description: iOS implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_darwin issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.5.0 +version: 1.6.0 environment: sdk: ^3.6.0 @@ -32,7 +32,7 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.4.4 - pigeon: ^25.3.2 + pigeon: ^26.0.0 topics: - authentication diff --git a/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.dart b/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.dart index 756205f1b68..5ab2dac7aad 100644 --- a/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.dart +++ b/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.dart @@ -372,6 +372,71 @@ void main() { errorDetails))); }); + test('converts errorUserCancelled to PlatformException', () async { + const String errorMessage = 'The user cancelled authentication.'; + const String errorDetails = 'com.apple.LocalAuthentication'; + when(api.authenticate(any, any)).thenAnswer((_) async => + AuthResultDetails( + result: AuthResult.errorUserCancelled, + errorMessage: errorMessage, + errorDetails: errorDetails)); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', authMessages: []), + throwsA(isA() + .having( + (PlatformException e) => e.code, 'code', 'UserCancelled') + .having( + (PlatformException e) => e.message, 'message', errorMessage) + .having((PlatformException e) => e.details, 'details', + errorDetails))); + }); + + test('converts errorUserFallback to PlatformException', () async { + const String errorMessage = 'The user chose to use the fallback.'; + const String errorDetails = 'com.apple.LocalAuthentication'; + when(api.authenticate(any, any)).thenAnswer((_) async => + AuthResultDetails( + result: AuthResult.errorUserFallback, + errorMessage: errorMessage, + errorDetails: errorDetails)); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', authMessages: []), + throwsA(isA() + .having((PlatformException e) => e.code, 'code', 'UserFallback') + .having( + (PlatformException e) => e.message, 'message', errorMessage) + .having((PlatformException e) => e.details, 'details', + errorDetails))); + }); + + test('converts errorBiometricNotAvailable to PlatformException', + () async { + const String errorMessage = + 'Biometrics are not available on this device.'; + const String errorDetails = 'com.apple.LocalAuthentication'; + when(api.authenticate(any, any)).thenAnswer((_) async => + AuthResultDetails( + result: AuthResult.errorBiometricNotAvailable, + errorMessage: errorMessage, + errorDetails: errorDetails)); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', authMessages: []), + throwsA(isA() + // The code here should match what you defined in your Dart switch statement. + .having((PlatformException e) => e.code, 'code', + 'BiometricNotAvailable') + .having( + (PlatformException e) => e.message, 'message', errorMessage) + .having((PlatformException e) => e.details, 'details', + errorDetails))); + }); + test('converts errorPasscodeNotSet to legacy PlatformException', () async { const String errorMessage = 'a message';