Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions packages/local_auth/local_auth_darwin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
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 @@ -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.6.0

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