Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/local_auth/local_auth_darwin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 1.5.1

* Provides more specific error codes on iOS for authentication failures.
* Previously, several distinct errors (like user cancellation or biometric lockout) would return a generic `NotAvailable` exception.
* These failures now return specific codes. You may need to update your error handling logic to check for these new codes:
* `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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 60;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -255,7 +255,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -423,9 +423,10 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = S8QB4VV633;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = RunnerTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -454,9 +455,10 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = S8QB4VV633;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = RunnerTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -582,6 +584,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = S8QB4VV633;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
Expand All @@ -606,6 +609,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = S8QB4VV633;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
Expand Down Expand Up @@ -658,7 +662,7 @@
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
11 changes: 10 additions & 1 deletion packages/local_auth/local_auth_darwin/lib/src/messages.g.dart
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions packages/local_auth/local_auth_darwin/pigeons/messages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions packages/local_auth/local_auth_darwin/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.5.1

environment:
sdk: ^3.6.0
Expand Down Expand Up @@ -32,7 +32,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.4.4
pigeon: ^25.3.2
pigeon: ^26.0.0

topics:
- authentication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: <AuthMessages>[]),
throwsA(isA<PlatformException>()
.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: <AuthMessages>[]),
throwsA(isA<PlatformException>()
.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: <AuthMessages>[]),
throwsA(isA<PlatformException>()
// 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';
Expand Down