From b53743a56b863ea7e0720cbbb6247d469b3ada33 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 26 Aug 2025 13:03:28 -0400 Subject: [PATCH 01/19] Bind to local platform interface package --- packages/local_auth/local_auth/example/pubspec.yaml | 4 ++++ packages/local_auth/local_auth/pubspec.yaml | 4 ++++ packages/local_auth/local_auth_android/example/pubspec.yaml | 4 ++++ packages/local_auth/local_auth_android/pubspec.yaml | 4 ++++ packages/local_auth/local_auth_darwin/example/pubspec.yaml | 4 ++++ packages/local_auth/local_auth_darwin/pubspec.yaml | 4 ++++ packages/local_auth/local_auth_windows/example/pubspec.yaml | 4 ++++ packages/local_auth/local_auth_windows/pubspec.yaml | 4 ++++ 8 files changed, 32 insertions(+) diff --git a/packages/local_auth/local_auth/example/pubspec.yaml b/packages/local_auth/local_auth/example/pubspec.yaml index 48d1809f2e9..e1bab96dd5d 100644 --- a/packages/local_auth/local_auth/example/pubspec.yaml +++ b/packages/local_auth/local_auth/example/pubspec.yaml @@ -28,3 +28,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index eda0fdd07a8..f32f89f86c9 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -39,3 +39,7 @@ topics: - authentication - biometrics - local-auth +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml index 3a10bee4329..2644f661216 100644 --- a/packages/local_auth/local_auth_android/example/pubspec.yaml +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -26,3 +26,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index 2a1565e61c0..badf49ed45a 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -35,3 +35,7 @@ topics: - authentication - biometrics - local-auth +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_darwin/example/pubspec.yaml b/packages/local_auth/local_auth_darwin/example/pubspec.yaml index 95c7f923b6b..4e90fdc2614 100644 --- a/packages/local_auth/local_auth_darwin/example/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/example/pubspec.yaml @@ -26,3 +26,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_darwin/pubspec.yaml b/packages/local_auth/local_auth_darwin/pubspec.yaml index a0596f945cd..5af84f8704b 100644 --- a/packages/local_auth/local_auth_darwin/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/pubspec.yaml @@ -38,3 +38,7 @@ topics: - authentication - biometrics - local-auth +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml index bef084fb595..ea37698ad85 100644 --- a/packages/local_auth/local_auth_windows/example/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -26,3 +26,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index 6401846b5b0..1c6ddf1d8d1 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -30,3 +30,7 @@ topics: - authentication - biometrics - local-auth +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} From dff4dec6abd31ad818fff2500fa244a47fe8d394 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 26 Aug 2025 14:27:50 -0400 Subject: [PATCH 02/19] Add structured exception class --- .../CHANGELOG.md | 4 +- .../lib/types/auth_exception.dart | 124 ++++++++++++++++++ .../lib/types/types.dart | 1 + .../pubspec.yaml | 2 +- 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart diff --git a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md index 9391da4be9f..98be5f7598d 100644 --- a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md +++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 1.1.0 +* Adds `LocalAuthException` to allow for consistent, structured exceptions + across platform implementations. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.0.10 diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart new file mode 100644 index 00000000000..d6d148a0250 --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart @@ -0,0 +1,124 @@ +// 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 'package:flutter/foundation.dart'; + +/// An exception thrown by the plugin when there is authenication failure, or +/// some other error. +@immutable +class LocalAuthException implements Exception { + /// Crceates a new exception with the given information. + const LocalAuthException({ + required this.code, + this.description, + this.details, + }); + + /// The type of failure. + final LocalAuthExceptionCode code; + + /// A human-readable description of the failure. + final String? description; + + /// Any additional details about the failure. + final Object? details; + + @override + String toString() => 'LocalAuthException(code $code, $description, $details)'; +} + +/// Types of [LocalAuthException]s, as indicated by [LocalAuthException.code]. +/// +/// Adding new values to this enum in the future will *not* be considered a +/// breaking change, so clients should not assume they can exhaustively match +/// exception codes. Clients should always include a default or other fallback. +enum LocalAuthExceptionCode { + /// An authentication operation is already in progress, and has not completed. + /// + /// A new authentication cannot be started while the Future for a previous + /// authentication is still outstanding. + authInProgress, + + /// UI needs to be displayed, but could not be. + /// + /// For example, this can be returned on Android if a call tries to show UI + /// when no Activity is available. + uiUnavailable, + + /// The operation was canceled by the user. + userCanceled, + + /// The operation was canceled due to a device-specific timeout. + timeout, + + /// The operation was canceled by a system event. + /// + /// For example, on mobile this may be returned if the application is + /// backgrounded during authentication. + systemCanceled, + + /// The device has no credentials configured. + /// + /// For example, on mobile this would be returned if the device has no + /// enrolled biometrics and no fallback authentication mechanism set such as + /// a passcode, pin, or pattern. + noCredentialsSet, + + /// The device is capable of biometric authentication, but no biometrics are + /// enrolled. + noBiometricsEnrolled, + + /// The device does not have biometric hardware. + noBiometricHardware, + + /// The device has, or can have, biometric hardware, but none is currently + /// available. + /// + /// Examples include: + /// - Hardware that is currently in use by another application. + /// - Devices that have previously paired with bluetooth biometric hardware, + /// but are not currently paired to it. + /// + /// Devices that could have removable hardware attached may return either this + /// or [noBiometricHardware] depending on the platform implementation. + /// Platforms should generally only return this code if the system provides + /// information indicating that the device has previously had such hardware. + biometricHardwareTemporarilyUnavailable, + + /// Authentication has temporarily been locked out, and should be re-attempted + /// later. + /// + /// For example, devices may return this error after too many failed + /// authentication attempts. + temporaryLockout, + + /// Biometric authentication has been locked until some other authentication + /// has succeeded. + /// + /// Applications that do not require biometric authentication should generally + /// handle this error by re-attempting authentication with fallback to + /// non-biometrics allowed. Applications that require biometrics should + /// prompt users to resolve the lockout. + biometricLockout, + + /// The user indicated via system-provided UI that they want to use a fallback + /// authentication option instead of biometrics. + /// + /// Whether this can be returned depends on the platform implementation and + /// the authentication configuration options. Applications should generally + /// handle this error by offering the user an alternate authentication option. + userRequestedFallback, + + /// The authentication attempt failed due to some device-level error. + /// + /// The [LocalAuthException.description] should contain more details about the + /// error. + deviceError, + + /// The authentication attempt failed due to some unknown or unexpected error. + /// + /// The [LocalAuthException.description] should contain more details about the + /// error. + unknownError, +} diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/types.dart b/packages/local_auth/local_auth_platform_interface/lib/types/types.dart index ea43b942cff..c8710b663a2 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/types.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/types.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'auth_exception.dart'; export 'auth_messages.dart'; export 'auth_options.dart'; export 'biometric_type.dart'; diff --git a/packages/local_auth/local_auth_platform_interface/pubspec.yaml b/packages/local_auth/local_auth_platform_interface/pubspec.yaml index 4b19c7d05e0..ca5f3aa4476 100644 --- a/packages/local_auth/local_auth_platform_interface/pubspec.yaml +++ b/packages/local_auth/local_auth_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/local_auth/lo issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.0.10 +version: 1.1.0 environment: sdk: ^3.7.0 From 0f54e0f723dab97186ad5a2d0a18f48141f0c060 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 26 Aug 2025 15:23:04 -0400 Subject: [PATCH 03/19] Update Windows --- .../local_auth_windows/CHANGELOG.md | 3 +- .../local_auth_windows/example/lib/main.dart | 27 ++-- .../lib/local_auth_windows.dart | 29 +++- .../lib/src/messages.g.dart | 127 +++++++++++----- .../local_auth_windows/pigeons/messages.dart | 29 +++- .../local_auth_windows/pubspec.yaml | 4 +- .../test/local_auth_test.dart | 137 ++++++++++++++++-- .../local_auth_windows/windows/local_auth.h | 7 +- .../windows/local_auth_plugin.cpp | 28 ++-- .../local_auth_windows/windows/messages.g.cpp | 38 ++++- .../local_auth_windows/windows/messages.g.h | 33 +++-- .../windows/test/local_auth_plugin_test.cpp | 78 +++++++++- 12 files changed, 426 insertions(+), 114 deletions(-) diff --git a/packages/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md index cab0a630bc7..fff29ae14f2 100644 --- a/packages/local_auth/local_auth_windows/CHANGELOG.md +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.0 +* Switches to `LocalAuthException` for error reporting. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.0.11 diff --git a/packages/local_auth/local_auth_windows/example/lib/main.dart b/packages/local_auth/local_auth_windows/example/lib/main.dart index b8f9e119e02..5546c572b00 100644 --- a/packages/local_auth/local_auth_windows/example/lib/main.dart +++ b/packages/local_auth/local_auth_windows/example/lib/main.dart @@ -94,11 +94,18 @@ class _MyAppState extends State { setState(() { _isAuthenticating = false; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - $e'; + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected error - ${e.message}'; }); return; } @@ -111,11 +118,6 @@ class _MyAppState extends State { ); } - Future _cancelAuthentication() async { - await LocalAuthPlatform.instance.stopAuthentication(); - setState(() => _isAuthenticating = false); - } - @override Widget build(BuildContext context) { return MaterialApp( @@ -149,18 +151,7 @@ class _MyAppState extends State { ), const Divider(height: 100), Text('Current State: $_authorized\n'), - if (_isAuthenticating) - ElevatedButton( - onPressed: _cancelAuthentication, - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Cancel Authentication'), - Icon(Icons.cancel), - ], - ), - ) - else + if (!_isAuthenticating) Column( children: [ ElevatedButton( diff --git a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart index de19b1548c4..8af926ac2e9 100644 --- a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart +++ b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart @@ -39,7 +39,34 @@ class LocalAuthWindows extends LocalAuthPlatform { ); } - return _api.authenticate(localizedReason); + return switch (await _api.authenticate(localizedReason)) { + AuthResult.success => true, + AuthResult.failure => false, + AuthResult.noHardware => + throw const LocalAuthException( + code: LocalAuthExceptionCode.noBiometricHardware, + ), + AuthResult.notEnrolled => + throw const LocalAuthException( + code: LocalAuthExceptionCode.noBiometricsEnrolled, + ), + AuthResult.deviceBusy => + throw const LocalAuthException( + code: LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable, + ), + AuthResult.disabledByPolicy => + // This error is niche enough that it doesn't warrant a specific + // mapping, so just use unknownError with a description. + throw const LocalAuthException( + code: LocalAuthExceptionCode.unknownError, + description: 'Group policy has disabled the authentication device.', + ), + AuthResult.unavailable => + throw const LocalAuthException( + code: LocalAuthExceptionCode.unknownError, + description: 'Authentication failed with an unsupported result code.', + ), + }; } @override diff --git a/packages/local_auth/local_auth_windows/lib/src/messages.g.dart b/packages/local_auth/local_auth_windows/lib/src/messages.g.dart index 1b511f686fb..16dc3e1d8bc 100644 --- a/packages/local_auth/local_auth_windows/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_windows/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 (v21.0.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), 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 @@ -18,8 +18,55 @@ PlatformException _createConnectionError(String channelName) { ); } +/// Possible outcomes of an authentication attempt. +enum AuthResult { + /// The user authenticated successfully. + success, + + /// The user failed to successfully authenticate. + failure, + + /// No biometric hardware is available. + noHardware, + + /// No biometrics are enrolled. + notEnrolled, + + /// The biometric hardware is currently in use. + deviceBusy, + + /// Device poilcy does not allow using the authentication system. + disabledByPolicy, + + /// Authentication is unavailable for an unknown reason. + unavailable, +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is AuthResult) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : AuthResult.values[value]; + default: + return super.readValueOfType(type, buffer); + } + } } class LocalAuthApi { @@ -29,77 +76,77 @@ class LocalAuthApi { LocalAuthApi({ BinaryMessenger? binaryMessenger, String messageChannelSuffix = '', - }) : __pigeon_binaryMessenger = binaryMessenger, - __pigeon_messageChannelSuffix = + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? __pigeon_binaryMessenger; + final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - final String __pigeon_messageChannelSuffix; + final String pigeonVar_messageChannelSuffix; /// Returns true if this device supports authentication. Future isDeviceSupported() async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.local_auth_windows.LocalAuthApi.isDeviceSupported$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = + final String pigeonVar_channelName = + 'dev.flutter.pigeon.local_auth_windows.LocalAuthApi.isDeviceSupported$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send(null) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as bool?)!; } } /// Attempts to authenticate the user with the provided [localizedReason] as /// the user-facing explanation for the authorization request. - /// - /// Returns true if authorization succeeds, false if it is attempted but is - /// not successful, and an error if authorization could not be attempted. - Future authenticate(String localizedReason) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.local_auth_windows.LocalAuthApi.authenticate$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = + Future authenticate(String localizedReason) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.local_auth_windows.LocalAuthApi.authenticate$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send([localizedReason]) - as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [localizedReason], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as AuthResult?)!; } } } diff --git a/packages/local_auth/local_auth_windows/pigeons/messages.dart b/packages/local_auth/local_auth_windows/pigeons/messages.dart index d3ac41192dc..1261657f411 100644 --- a/packages/local_auth/local_auth_windows/pigeons/messages.dart +++ b/packages/local_auth/local_auth_windows/pigeons/messages.dart @@ -13,6 +13,30 @@ import 'package:pigeon/pigeon.dart'; copyrightHeader: 'pigeons/copyright.txt', ), ) +/// Possible outcomes of an authentication attempt. +enum AuthResult { + /// The user authenticated successfully. + success, + + /// The user failed to successfully authenticate. + failure, + + /// No biometric hardware is available. + noHardware, + + /// No biometrics are enrolled. + notEnrolled, + + /// The biometric hardware is currently in use. + deviceBusy, + + /// Device poilcy does not allow using the authentication system. + disabledByPolicy, + + /// Authentication is unavailable for an unknown reason. + unavailable, +} + @HostApi() abstract class LocalAuthApi { /// Returns true if this device supports authentication. @@ -21,9 +45,6 @@ abstract class LocalAuthApi { /// Attempts to authenticate the user with the provided [localizedReason] as /// the user-facing explanation for the authorization request. - /// - /// Returns true if authorization succeeds, false if it is attempted but is - /// not successful, and an error if authorization could not be attempted. @async - bool authenticate(String localizedReason); + AuthResult authenticate(String localizedReason); } diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index 1c6ddf1d8d1..b4e6b715e8b 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_windows description: Windows implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.11 +version: 2.0.0 environment: sdk: ^3.7.0 @@ -24,7 +24,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pigeon: ^21.0.0 + pigeon: ^26.0.1 topics: - authentication diff --git a/packages/local_auth/local_auth_windows/test/local_auth_test.dart b/packages/local_auth/local_auth_windows/test/local_auth_test.dart index 80bc78576c7..13b65496bd1 100644 --- a/packages/local_auth/local_auth_windows/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_windows/test/local_auth_test.dart @@ -2,7 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/src/services/binary_messenger.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; import 'package:local_auth_windows/local_auth_windows.dart'; import 'package:local_auth_windows/src/messages.g.dart'; @@ -17,7 +19,7 @@ void main() { }); test('authenticate handles success', () async { - api.returnValue = true; + api.authReturnValue = AuthResult.success; final bool result = await plugin.authenticate( authMessages: [const WindowsAuthMessages()], @@ -29,7 +31,7 @@ void main() { }); test('authenticate handles failure', () async { - api.returnValue = false; + api.authReturnValue = AuthResult.failure; final bool result = await plugin.authenticate( authMessages: [const WindowsAuthMessages()], @@ -40,6 +42,104 @@ void main() { expect(api.passedReason, 'My localized reason'); }); + test('authenticate handles no hardware', () async { + api.authReturnValue = AuthResult.noHardware; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricHardware, + ), + ), + ); + }); + + test('authenticate handles not enrolled', () async { + api.authReturnValue = AuthResult.notEnrolled; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricsEnrolled, + ), + ), + ); + }); + + test('authenticate handles busy', () async { + api.authReturnValue = AuthResult.deviceBusy; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable, + ), + ), + ); + }); + + test('authenticate handles disabled by policy', () async { + api.authReturnValue = AuthResult.disabledByPolicy; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + // Currently there is no specific error code for this case; it can + // be added if there is user demand for it. + LocalAuthExceptionCode.unknownError, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + contains('Group policy has disabled the authentication device'), + ), + ), + ); + }); + + test('authenticate handles generic error', () async { + api.authReturnValue = AuthResult.unavailable; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.unknownError, + ), + ), + ); + }); + test('authenticate throws for biometricOnly', () async { expect( plugin.authenticate( @@ -52,7 +152,7 @@ void main() { }); test('isDeviceSupported handles supported', () async { - api.returnValue = true; + api.supportedReturnValue = true; final bool result = await plugin.isDeviceSupported(); @@ -60,7 +160,7 @@ void main() { }); test('isDeviceSupported handles unsupported', () async { - api.returnValue = false; + api.supportedReturnValue = false; final bool result = await plugin.isDeviceSupported(); @@ -68,7 +168,7 @@ void main() { }); test('deviceSupportsBiometrics handles supported', () async { - api.returnValue = true; + api.supportedReturnValue = true; final bool result = await plugin.deviceSupportsBiometrics(); @@ -76,7 +176,7 @@ void main() { }); test('deviceSupportsBiometrics handles unsupported', () async { - api.returnValue = false; + api.supportedReturnValue = false; final bool result = await plugin.deviceSupportsBiometrics(); @@ -86,7 +186,7 @@ void main() { test( 'getEnrolledBiometrics returns expected values when supported', () async { - api.returnValue = true; + api.supportedReturnValue = true; final List result = await plugin.getEnrolledBiometrics(); @@ -98,7 +198,7 @@ void main() { ); test('getEnrolledBiometrics returns nothing when unsupported', () async { - api.returnValue = false; + api.supportedReturnValue = false; final List result = await plugin.getEnrolledBiometrics(); @@ -114,20 +214,31 @@ void main() { } class _FakeLocalAuthApi implements LocalAuthApi { - /// The return value for [isDeviceSupported] and [authenticate]. - bool returnValue = false; + /// The return value for [authenticate]. + AuthResult authReturnValue = AuthResult.success; + + /// The return value for [isDeviceSupported]. + bool supportedReturnValue = false; /// The argument that was passed to [authenticate]. String? passedReason; @override - Future authenticate(String localizedReason) async { + Future authenticate(String localizedReason) async { passedReason = localizedReason; - return returnValue; + return authReturnValue; } @override Future isDeviceSupported() async { - return returnValue; + return supportedReturnValue; } + + @override + // ignore: non_constant_identifier_names + BinaryMessenger? get pigeonVar_binaryMessenger => null; + + @override + // ignore: non_constant_identifier_names + String get pigeonVar_messageChannelSuffix => ''; } diff --git a/packages/local_auth/local_auth_windows/windows/local_auth.h b/packages/local_auth/local_auth_windows/windows/local_auth.h index 9cdc6efbcd1..73e2c3b17be 100644 --- a/packages/local_auth/local_auth_windows/windows/local_auth.h +++ b/packages/local_auth/local_auth_windows/windows/local_auth.h @@ -67,8 +67,9 @@ class LocalAuthPlugin : public flutter::Plugin, public LocalAuthApi { // LocalAuthApi: void IsDeviceSupported( std::function reply)> result) override; - void Authenticate(const std::string& localized_reason, - std::function reply)> result) override; + void Authenticate( + const std::string& localized_reason, + std::function reply)> result) override; private: std::unique_ptr user_consent_verifier_; @@ -76,7 +77,7 @@ class LocalAuthPlugin : public flutter::Plugin, public LocalAuthApi { // Starts authentication process. winrt::fire_and_forget AuthenticateCoroutine( const std::string& localized_reason, - std::function reply)> result); + std::function reply)> result); // Returns whether the system supports Windows Hello. winrt::fire_and_forget IsDeviceSupportedCoroutine( diff --git a/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp b/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp index cb8d4cf0b5f..592baa668b6 100644 --- a/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp +++ b/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp @@ -120,14 +120,14 @@ void LocalAuthPlugin::IsDeviceSupported( void LocalAuthPlugin::Authenticate( const std::string& localized_reason, - std::function reply)> result) { + std::function reply)> result) { AuthenticateCoroutine(localized_reason, std::move(result)); } // Starts authentication process. winrt::fire_and_forget LocalAuthPlugin::AuthenticateCoroutine( const std::string& localized_reason, - std::function reply)> result) { + std::function reply)> result) { std::wstring reason = Utf16FromUtf8(localized_reason); winrt::Windows::Security::Credentials::UI::UserConsentVerifierAvailability @@ -137,19 +137,27 @@ winrt::fire_and_forget LocalAuthPlugin::AuthenticateCoroutine( if (ucv_availability == winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::DeviceNotPresent) { - result(FlutterError("NoHardware", "No biometric hardware found")); + result(AuthResult::kNoHardware); co_return; } else if (ucv_availability == winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::NotConfiguredForUser) { - result( - FlutterError("NotEnrolled", "No biometrics enrolled on this device.")); + result(AuthResult::kNotEnrolled); + co_return; + } else if (ucv_availability == + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DeviceBusy) { + result(AuthResult::kDeviceBusy); + co_return; + } else if (ucv_availability == + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DisabledByPolicy) { + result(AuthResult::kDisabledByPolicy); co_return; } else if (ucv_availability != winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::Available) { - result( - FlutterError("NotAvailable", "Required security features not enabled")); + result(AuthResult::kUnavailable); co_return; } @@ -160,9 +168,11 @@ winrt::fire_and_forget LocalAuthPlugin::AuthenticateCoroutine( reason); result(consent_result == winrt::Windows::Security::Credentials::UI:: - UserConsentVerificationResult::Verified); + UserConsentVerificationResult::Verified + ? AuthResult::kSuccess + : AuthResult::kFailure); } catch (...) { - result(false); + result(AuthResult::kFailure); } } diff --git a/packages/local_auth/local_auth_windows/windows/messages.g.cpp b/packages/local_auth/local_auth_windows/windows/messages.g.cpp index ce0f578edfc..b0212b92c26 100644 --- a/packages/local_auth/local_auth_windows/windows/messages.g.cpp +++ b/packages/local_auth/local_auth_windows/windows/messages.g.cpp @@ -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 (v21.0.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #undef _HAS_EXCEPTIONS @@ -31,22 +31,44 @@ FlutterError CreateConnectionError(const std::string channel_name) { EncodableValue("")); } -PigeonCodecSerializer::PigeonCodecSerializer() {} +PigeonInternalCodecSerializer::PigeonInternalCodecSerializer() {} -EncodableValue PigeonCodecSerializer::ReadValueOfType( +EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( uint8_t type, flutter::ByteStreamReader* stream) const { - return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + switch (type) { + case 129: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = + encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() + ? EncodableValue() + : CustomEncodableValue( + static_cast(enum_arg_value)); + } + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } } -void PigeonCodecSerializer::WriteValue( +void PigeonInternalCodecSerializer::WriteValue( const EncodableValue& value, flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = + std::get_if(&value)) { + if (custom_value->type() == typeid(AuthResult)) { + stream->WriteByte(129); + WriteValue(EncodableValue(static_cast( + std::any_cast(*custom_value))), + stream); + return; + } + } flutter::StandardCodecSerializer::WriteValue(value, stream); } /// The codec used by LocalAuthApi. const flutter::StandardMessageCodec& LocalAuthApi::GetCodec() { return flutter::StandardMessageCodec::GetInstance( - &PigeonCodecSerializer::GetInstance()); + &PigeonInternalCodecSerializer::GetInstance()); } // Sets up an instance of `LocalAuthApi` to handle messages through the @@ -112,14 +134,14 @@ void LocalAuthApi::SetUp(flutter::BinaryMessenger* binary_messenger, const auto& localized_reason_arg = std::get(encodable_localized_reason_arg); api->Authenticate( - localized_reason_arg, [reply](ErrorOr&& output) { + localized_reason_arg, [reply](ErrorOr&& output) { if (output.has_error()) { reply(WrapError(output.error())); return; } EncodableList wrapped; wrapped.push_back( - EncodableValue(std::move(output).TakeValue())); + CustomEncodableValue(std::move(output).TakeValue())); reply(EncodableValue(std::move(wrapped))); }); } catch (const std::exception& exception) { diff --git a/packages/local_auth/local_auth_windows/windows/messages.g.h b/packages/local_auth/local_auth_windows/windows/messages.g.h index 60d35feec39..7ba94942815 100644 --- a/packages/local_auth/local_auth_windows/windows/messages.g.h +++ b/packages/local_auth/local_auth_windows/windows/messages.g.h @@ -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 (v21.0.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #ifndef PIGEON_MESSAGES_G_H_ @@ -58,11 +58,29 @@ class ErrorOr { std::variant v_; }; -class PigeonCodecSerializer : public flutter::StandardCodecSerializer { +// Possible outcomes of an authentication attempt. +enum class AuthResult { + // The user authenticated successfully. + kSuccess = 0, + // The user failed to successfully authenticate. + kFailure = 1, + // No biometric hardware is available. + kNoHardware = 2, + // No biometrics are enrolled. + kNotEnrolled = 3, + // The biometric hardware is currently in use. + kDeviceBusy = 4, + // Device poilcy does not allow using the authentication system. + kDisabledByPolicy = 5, + // Authentication is unavailable for an unknown reason. + kUnavailable = 6 +}; + +class PigeonInternalCodecSerializer : public flutter::StandardCodecSerializer { public: - PigeonCodecSerializer(); - inline static PigeonCodecSerializer& GetInstance() { - static PigeonCodecSerializer sInstance; + PigeonInternalCodecSerializer(); + inline static PigeonInternalCodecSerializer& GetInstance() { + static PigeonInternalCodecSerializer sInstance; return sInstance; } @@ -86,12 +104,9 @@ class LocalAuthApi { std::function reply)> result) = 0; // Attempts to authenticate the user with the provided [localizedReason] as // the user-facing explanation for the authorization request. - // - // Returns true if authorization succeeds, false if it is attempted but is - // not successful, and an error if authorization could not be attempted. virtual void Authenticate( const std::string& localized_reason, - std::function reply)> result) = 0; + std::function reply)> result) = 0; // The codec used by LocalAuthApi. static const flutter::StandardMessageCodec& GetCodec(); diff --git a/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp b/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp index 6b1b0ed79c3..f07e147c5f5 100644 --- a/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp +++ b/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp @@ -94,12 +94,12 @@ TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenAuthorized) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - ErrorOr result(false); + ErrorOr result(AuthResult::kUnavailable); plugin.Authenticate("My Reason", - [&result](ErrorOr reply) { result = reply; }); + [&result](ErrorOr reply) { result = reply; }); EXPECT_FALSE(result.has_error()); - EXPECT_TRUE(result.value()); + EXPECT_EQ(result.value(), AuthResult::kSuccess); } TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenNotAuthorized) { @@ -127,12 +127,78 @@ TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenNotAuthorized) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - ErrorOr result(true); + ErrorOr result(AuthResult::kUnavailable); plugin.Authenticate("My Reason", - [&result](ErrorOr reply) { result = reply; }); + [&result](ErrorOr reply) { result = reply; }); EXPECT_FALSE(result.has_error()); - EXPECT_FALSE(result.value()); + EXPECT_EQ(result.value(), AuthResult::kFailure); +} + +TEST(LocalAuthPlugin, AuthenticateHandlerReportsNoHardware) { + std::unique_ptr mockConsentVerifier = + std::make_unique(); + + EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) + .Times(1) + .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability> { + co_return winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DeviceNotPresent; + }); + + LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(AuthResult::kUnavailable); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); + + EXPECT_FALSE(result.has_error()); + EXPECT_EQ(result.value(), AuthResult::kNoHardware); +} + +TEST(LocalAuthPlugin, AuthenticateHandlerReportsBusy) { + std::unique_ptr mockConsentVerifier = + std::make_unique(); + + EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) + .Times(1) + .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability> { + co_return winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DeviceBusy; + }); + + LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(AuthResult::kUnavailable); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); + + EXPECT_FALSE(result.has_error()); + EXPECT_EQ(result.value(), AuthResult::kDeviceBusy); +} + +TEST(LocalAuthPlugin, AuthenticateHandlerReportsDisabledByPolicy) { + std::unique_ptr mockConsentVerifier = + std::make_unique(); + + EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) + .Times(1) + .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability> { + co_return winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DisabledByPolicy; + }); + + LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(AuthResult::kUnavailable); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); + + EXPECT_FALSE(result.has_error()); + EXPECT_EQ(result.value(), AuthResult::kDisabledByPolicy); } } // namespace test From 9f9c1486a8662fc6b84e0381d123fbf2d604c26f Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 3 Sep 2025 11:24:40 -0400 Subject: [PATCH 04/19] Clarify platform interface docs --- .../lib/local_auth_platform_interface.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart index ec400ddafa3..5aa5ba9d374 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart @@ -40,7 +40,13 @@ abstract class LocalAuthPlatform extends PlatformInterface { /// Authenticates the user with biometrics available on the device while also /// allowing the user to use device authentication - pin, pattern, passcode. /// - /// Returns true if the user successfully authenticated, false otherwise. + /// Returns true if the user successfully authenticated. Returns false if + /// the authentication completes, but the user failed the challenge with no + /// further effects. Platform implementations should throw a + /// [LocalAuthException] for any other outcome, such as errors, cancelation, + /// or lockout. This may mean that for some platforms, the implementation will + /// never return false (e.g., if the only standard outcomes are success, + /// cancelation, or temporary lockout due to too many retries). /// /// [localizedReason] is the message to show to user while prompting them /// for authentication. This is typically along the lines of: 'Please scan @@ -50,11 +56,6 @@ abstract class LocalAuthPlatform extends PlatformInterface { /// customize messages in the dialogs. /// /// Provide [options] for configuring further authentication related options. - /// - /// Throws a [PlatformException] if there were technical problems with local - /// authentication (e.g. lack of relevant hardware). This might throw - /// [PlatformException] with error code [otherOperatingSystem] on the iOS - /// simulator. Future authenticate({ required String localizedReason, required Iterable authMessages, From a623537d220e28b6fd05d704ded6fc08b97c95aa Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 27 Aug 2025 10:09:30 -0400 Subject: [PATCH 05/19] Android implementation --- packages/local_auth/local_auth/CHANGELOG.md | 2 + .../local_auth/lib/error_codes.dart | 29 -- .../local_auth_android/CHANGELOG.md | 3 +- .../localauth/AuthenticationHelper.java | 55 ++- .../plugins/localauth/LocalAuthPlugin.java | 33 +- .../flutter/plugins/localauth/Messages.java | 156 ++++++- .../localauth/AuthenticationHelperTest.java | 173 +++++++- .../plugins/localauth/LocalAuthTest.java | 9 +- .../lib/local_auth_android.dart | 108 ++--- .../lib/src/messages.g.dart | 88 +++- .../local_auth_android/pigeons/messages.dart | 63 ++- .../local_auth_android/pubspec.yaml | 2 +- .../test/local_auth_test.dart | 379 +++++++++++++----- .../test/local_auth_test.mocks.dart | 13 +- 14 files changed, 834 insertions(+), 279 deletions(-) delete mode 100644 packages/local_auth/local_auth/lib/error_codes.dart diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index 9dd19517d5c..80d87dda378 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,5 +1,7 @@ ## NEXT +* **BREAKING CHANGES:** + * * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. * Updates README to indicate that Andoid SDK <21 is no longer supported. diff --git a/packages/local_auth/local_auth/lib/error_codes.dart b/packages/local_auth/local_auth/lib/error_codes.dart deleted file mode 100644 index 8959bf29770..00000000000 --- a/packages/local_auth/local_auth/lib/error_codes.dart +++ /dev/null @@ -1,29 +0,0 @@ -// 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. - -// Exception codes for `PlatformException` returned by -// `authenticate`. - -/// Indicates that the user has not yet configured a passcode (iOS) or -/// PIN/pattern/password (Android) on the device. -const String passcodeNotSet = 'PasscodeNotSet'; - -/// Indicates the user has not enrolled any biometrics on the device. -const String notEnrolled = 'NotEnrolled'; - -/// Indicates the device does not have hardware support for biometrics. -const String notAvailable = 'NotAvailable'; - -/// Indicates the device operating system is unsupported. -const String otherOperatingSystem = 'OtherOperatingSystem'; - -/// Indicates the API is temporarily locked out due to too many attempts. -const String lockedOut = 'LockedOut'; - -/// Indicates the API is locked out more persistently than [lockedOut]. -/// Strong authentication like PIN/Pattern/Password is required to unlock. -const String permanentlyLockedOut = 'PermanentlyLockedOut'; - -/// Indicates that the biometricOnly parameter can't be true on Windows -const String biometricOnlyNotSupported = 'biometricOnlyNotSupported'; diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index 29be4e8936e..8d94402c231 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.0 +* Switches to `LocalAuthException` for error reporting. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.0.51 diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java index fcb32c546b1..a1c926492ba 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java @@ -25,6 +25,8 @@ import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; +import io.flutter.plugins.localauth.Messages.AuthResult; +import io.flutter.plugins.localauth.Messages.AuthResultCode; import java.util.concurrent.Executor; /** @@ -38,10 +40,9 @@ class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback /** The callback that handles the result of this authentication process. */ interface AuthCompletionHandler { /** Called when authentication attempt is complete. */ - void complete(Messages.AuthResult authResult); + void complete(AuthResult authResult); } - // This is null when not using v2 embedding; private final Lifecycle lifecycle; private final FragmentActivity activity; private final AuthCompletionHandler completionHandler; @@ -120,7 +121,14 @@ private void stop() { @SuppressLint("SwitchIntDef") @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + AuthResultCode code; switch (errorCode) { + case BiometricPrompt.ERROR_USER_CANCELED: + code = AuthResultCode.USER_CANCELED; + break; + case BiometricPrompt.ERROR_NEGATIVE_BUTTON: + code = AuthResultCode.NEGATIVE_BUTTON; + break; case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL: if (useErrorDialogs) { showGoToSettingsDialog( @@ -128,50 +136,65 @@ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString strings.getDeviceCredentialsSetupDescription()); return; } - completionHandler.complete(Messages.AuthResult.ERROR_NOT_AVAILABLE); + code = AuthResultCode.NO_CREDENTIALS; break; - case BiometricPrompt.ERROR_NO_SPACE: case BiometricPrompt.ERROR_NO_BIOMETRICS: if (useErrorDialogs) { showGoToSettingsDialog( strings.getBiometricRequiredTitle(), strings.getGoToSettingsDescription()); return; } - completionHandler.complete(Messages.AuthResult.ERROR_NOT_ENROLLED); + code = AuthResultCode.NOT_ENROLLED; break; case BiometricPrompt.ERROR_HW_UNAVAILABLE: + code = AuthResultCode.HARDWARE_UNAVAILABLE; + break; case BiometricPrompt.ERROR_HW_NOT_PRESENT: - completionHandler.complete(Messages.AuthResult.ERROR_NOT_AVAILABLE); + code = AuthResultCode.NO_HARDWARE; break; case BiometricPrompt.ERROR_LOCKOUT: - completionHandler.complete(Messages.AuthResult.ERROR_LOCKED_OUT_TEMPORARILY); + code = AuthResultCode.LOCKED_OUT_TEMPORARILY; break; case BiometricPrompt.ERROR_LOCKOUT_PERMANENT: - completionHandler.complete(Messages.AuthResult.ERROR_LOCKED_OUT_PERMANENTLY); + code = AuthResultCode.LOCKED_OUT_PERMANENTLY; break; case BiometricPrompt.ERROR_CANCELED: // If we are doing sticky auth and the activity has been paused, // ignore this error. We will start listening again when resumed. if (activityPaused && isAuthSticky) { return; - } else { - completionHandler.complete(Messages.AuthResult.FAILURE); } + code = AuthResultCode.SYSTEM_CANCELED; + break; + case BiometricPrompt.ERROR_TIMEOUT: + code = AuthResultCode.TIMEOUT; + break; + case BiometricPrompt.ERROR_NO_SPACE: + code = AuthResultCode.NO_SPACE; + break; + case BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED: + code = AuthResultCode.SECURITY_UPDATE_REQUIRED; break; default: - completionHandler.complete(Messages.AuthResult.FAILURE); + code = AuthResultCode.UNKNOWN_ERROR; + break; } + completionHandler.complete( + new AuthResult.Builder().setCode(code).setErrorMessage(errString.toString()).build()); stop(); } @Override public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { - completionHandler.complete(Messages.AuthResult.SUCCESS); + completionHandler.complete(new AuthResult.Builder().setCode(AuthResultCode.SUCCESS).build()); stop(); } @Override - public void onAuthenticationFailed() {} + public void onAuthenticationFailed() { + // No-op; this is called for incremental failures. Wait for a final + // resolution via the success or error callbacks. + } /** * If the activity is paused, we keep track because biometric dialog simply returns "User @@ -216,13 +239,15 @@ private void showGoToSettingsDialog(String title, String descriptionText) { Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom); OnClickListener goToSettingHandler = (dialog, which) -> { - completionHandler.complete(Messages.AuthResult.FAILURE); + completionHandler.complete( + new AuthResult.Builder().setCode(AuthResultCode.LAUNCHED_SETTINGS).build()); stop(); activity.startActivity(new Intent(Settings.ACTION_SECURITY_SETTINGS)); }; OnClickListener cancelHandler = (dialog, which) -> { - completionHandler.complete(Messages.AuthResult.FAILURE); + completionHandler.complete( + new AuthResult.Builder().setCode(AuthResultCode.NEGATIVE_BUTTON).build()); stop(); }; new AlertDialog.Builder(context) diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index 09af7c6656c..094b0b9db86 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -4,13 +4,11 @@ package io.flutter.plugins.localauth; -import static android.app.Activity.RESULT_OK; import static android.content.Context.KEYGUARD_SERVICE; import android.app.Activity; import android.app.KeyguardManager; import android.content.Context; -import android.content.Intent; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -21,11 +19,11 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; -import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; import io.flutter.plugins.localauth.Messages.AuthClassification; import io.flutter.plugins.localauth.Messages.AuthOptions; import io.flutter.plugins.localauth.Messages.AuthResult; +import io.flutter.plugins.localauth.Messages.AuthResultCode; import io.flutter.plugins.localauth.Messages.AuthStrings; import io.flutter.plugins.localauth.Messages.LocalAuthApi; import io.flutter.plugins.localauth.Messages.Result; @@ -39,32 +37,14 @@ *

Instantiate this in an add to app scenario to gracefully handle activity and context changes. */ public class LocalAuthPlugin implements FlutterPlugin, ActivityAware, LocalAuthApi { - private static final int LOCK_REQUEST_CODE = 221; private Activity activity; private AuthenticationHelper authHelper; @VisibleForTesting final AtomicBoolean authInProgress = new AtomicBoolean(false); - // These are null when not using v2 embedding. private Lifecycle lifecycle; private BiometricManager biometricManager; private KeyguardManager keyguardManager; - Result lockRequestResult; - private final PluginRegistry.ActivityResultListener resultListener = - new PluginRegistry.ActivityResultListener() { - @Override - public boolean onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == LOCK_REQUEST_CODE) { - if (resultCode == RESULT_OK && lockRequestResult != null) { - onAuthenticationCompleted(lockRequestResult, AuthResult.SUCCESS); - } else { - onAuthenticationCompleted(lockRequestResult, AuthResult.FAILURE); - } - lockRequestResult = null; - } - return false; - } - }; /** * Default constructor for LocalAuthPlugin. @@ -112,22 +92,23 @@ public void authenticate( @NonNull AuthStrings strings, @NonNull Result result) { if (authInProgress.get()) { - result.success(AuthResult.ERROR_ALREADY_IN_PROGRESS); + result.success(new AuthResult.Builder().setCode(AuthResultCode.ALREADY_IN_PROGRESS).build()); return; } if (activity == null || activity.isFinishing()) { - result.success(AuthResult.ERROR_NO_ACTIVITY); + result.success(new AuthResult.Builder().setCode(AuthResultCode.NO_ACTIVITY).build()); return; } if (!(activity instanceof FragmentActivity)) { - result.success(AuthResult.ERROR_NOT_FRAGMENT_ACTIVITY); + result.success( + new AuthResult.Builder().setCode(AuthResultCode.NOT_FRAGMENT_ACTIVITY).build()); return; } if (!isDeviceSupported()) { - result.success(AuthResult.ERROR_NOT_AVAILABLE); + result.success(new AuthResult.Builder().setCode(AuthResultCode.NO_CREDENTIALS).build()); return; } @@ -221,7 +202,6 @@ private void setServicesFromActivity(Activity activity) { @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { - binding.addActivityResultListener(resultListener); setServicesFromActivity(binding.getActivity()); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); } @@ -234,7 +214,6 @@ public void onDetachedFromActivityForConfigChanges() { @Override public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { - binding.addActivityResultListener(resultListener); setServicesFromActivity(binding.getActivity()); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); } diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java index e04a999a109..9fdf10f5e6e 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java @@ -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 (v22.4.0), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.localauth; @@ -66,29 +66,51 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { @interface CanIgnoreReturnValue {} /** Possible outcomes of an authentication attempt. */ - public enum AuthResult { + public enum AuthResultCode { /** The user authenticated successfully. */ SUCCESS(0), - /** The user failed to successfully authenticate. */ - FAILURE(1), + /** The user launched the settings dialog. */ + LAUNCHED_SETTINGS(1), + /** The user pressed the negative button, which corresponds to [AuthStrings.cancelButton]. */ + NEGATIVE_BUTTON(2), + /** + * The user canceled authentication without pressing the negative button. + * + *

This may be triggered by a swipe or a back button, for example. + */ + USER_CANCELED(3), + /** Authentication was caneceled by the system. */ + SYSTEM_CANCELED(4), + /** Authentication timed out. */ + TIMEOUT(5), /** An authentication was already in progress. */ - ERROR_ALREADY_IN_PROGRESS(2), + ALREADY_IN_PROGRESS(6), /** There is no foreground activity. */ - ERROR_NO_ACTIVITY(3), + NO_ACTIVITY(7), /** The foreground activity is not a FragmentActivity. */ - ERROR_NOT_FRAGMENT_ACTIVITY(4), - /** The authentication system was not available. */ - ERROR_NOT_AVAILABLE(5), + NOT_FRAGMENT_ACTIVITY(8), + /** The device does not have any credentials available. */ + NO_CREDENTIALS(9), + /** No biometric hardware is present. */ + NO_HARDWARE(10), + /** The biometric is temporarily unavailable. */ + HARDWARE_UNAVAILABLE(11), /** No biometrics are enrolled. */ - ERROR_NOT_ENROLLED(6), + NOT_ENROLLED(12), /** The user is locked out temporarily due to too many failed attempts. */ - ERROR_LOCKED_OUT_TEMPORARILY(7), + LOCKED_OUT_TEMPORARILY(13), /** The user is locked out until they log in another way due to too many failed attempts. */ - ERROR_LOCKED_OUT_PERMANENTLY(8); + LOCKED_OUT_PERMANENTLY(14), + /** The device does not have enough storage to complete authentication. */ + NO_SPACE(15), + /** The hardware is unavailable until a security update is performed. */ + SECURITY_UPDATE_REQUIRED(16), + /** Some unrecognized error case was encountered */ + UNKNOWN_ERROR(17); final int index; - AuthResult(final int index) { + AuthResultCode(final int index) { this.index = index; } } @@ -424,6 +446,101 @@ ArrayList toList() { } } + /** + * The results of an authentication request. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class AuthResult { + /** The specific result returned from the SDK. */ + private @NonNull AuthResultCode code; + + public @NonNull AuthResultCode getCode() { + return code; + } + + public void setCode(@NonNull AuthResultCode setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"code\" is null."); + } + this.code = setterArg; + } + + /** The error message associated with the result, if any. */ + private @Nullable String errorMessage; + + public @Nullable String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(@Nullable String setterArg) { + this.errorMessage = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + AuthResult() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AuthResult that = (AuthResult) o; + return code.equals(that.code) && Objects.equals(errorMessage, that.errorMessage); + } + + @Override + public int hashCode() { + return Objects.hash(code, errorMessage); + } + + public static final class Builder { + + private @Nullable AuthResultCode code; + + @CanIgnoreReturnValue + public @NonNull Builder setCode(@NonNull AuthResultCode setterArg) { + this.code = setterArg; + return this; + } + + private @Nullable String errorMessage; + + @CanIgnoreReturnValue + public @NonNull Builder setErrorMessage(@Nullable String setterArg) { + this.errorMessage = setterArg; + return this; + } + + public @NonNull AuthResult build() { + AuthResult pigeonReturn = new AuthResult(); + pigeonReturn.setCode(code); + pigeonReturn.setErrorMessage(errorMessage); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(code); + toListResult.add(errorMessage); + return toListResult; + } + + static @NonNull AuthResult fromList(@NonNull ArrayList pigeonVar_list) { + AuthResult pigeonResult = new AuthResult(); + Object code = pigeonVar_list.get(0); + pigeonResult.setCode((AuthResultCode) code); + Object errorMessage = pigeonVar_list.get(1); + pigeonResult.setErrorMessage((String) errorMessage); + return pigeonResult; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class AuthOptions { private @NonNull Boolean biometricOnly; @@ -580,7 +697,7 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 129: { Object value = readValue(buffer); - return value == null ? null : AuthResult.values()[((Long) value).intValue()]; + return value == null ? null : AuthResultCode.values()[((Long) value).intValue()]; } case (byte) 130: { @@ -590,6 +707,8 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 131: return AuthStrings.fromList((ArrayList) readValue(buffer)); case (byte) 132: + return AuthResult.fromList((ArrayList) readValue(buffer)); + case (byte) 133: return AuthOptions.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -598,17 +717,20 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { @Override protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof AuthResult) { + if (value instanceof AuthResultCode) { stream.write(129); - writeValue(stream, value == null ? null : ((AuthResult) value).index); + writeValue(stream, value == null ? null : ((AuthResultCode) value).index); } else if (value instanceof AuthClassification) { stream.write(130); writeValue(stream, value == null ? null : ((AuthClassification) value).index); } else if (value instanceof AuthStrings) { stream.write(131); writeValue(stream, ((AuthStrings) value).toList()); - } else if (value instanceof AuthOptions) { + } else if (value instanceof AuthResult) { stream.write(132); + writeValue(stream, ((AuthResult) value).toList()); + } else if (value instanceof AuthOptions) { + stream.write(133); writeValue(stream, ((AuthOptions) value).toList()); } else { super.writeValue(stream, value); diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java index d23532e0076..9c5d4fcf3ef 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java @@ -15,6 +15,7 @@ import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; import io.flutter.plugins.localauth.Messages.AuthOptions; import io.flutter.plugins.localauth.Messages.AuthResult; +import io.flutter.plugins.localauth.Messages.AuthResultCode; import io.flutter.plugins.localauth.Messages.AuthStrings; import org.junit.Test; import org.junit.runner.RunWith; @@ -48,7 +49,51 @@ public class AuthenticationHelperTest { .build(); @Test - public void onAuthenticationError_withoutDialogs_returnsNotAvailableForNoCredential() { + public void onAuthenticationError_returnsUserCanceled() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_USER_CANCELED, ""); + + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.USER_CANCELED) + .setErrorMessage("") + .build()); + } + + @Test + public void onAuthenticationError_returnsNegativeButton() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_NEGATIVE_BUTTON, ""); + + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.NEGATIVE_BUTTON) + .setErrorMessage("") + .build()); + } + + @Test + public void onAuthenticationError_withoutDialogs_returnsNoCredential() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -61,7 +106,12 @@ public void onAuthenticationError_withoutDialogs_returnsNotAvailableForNoCredent helper.onAuthenticationError(BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL, ""); - verify(handler).complete(AuthResult.ERROR_NOT_AVAILABLE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.NO_CREDENTIALS) + .setErrorMessage("") + .build()); } @Test @@ -78,11 +128,16 @@ public void onAuthenticationError_withoutDialogs_returnsNotEnrolledForNoBiometri helper.onAuthenticationError(BiometricPrompt.ERROR_NO_BIOMETRICS, ""); - verify(handler).complete(AuthResult.ERROR_NOT_ENROLLED); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.NOT_ENROLLED) + .setErrorMessage("") + .build()); } @Test - public void onAuthenticationError_returnsNotAvailableForHardwareUnavailable() { + public void onAuthenticationError_returnsHardwareUnavailable() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -95,11 +150,16 @@ public void onAuthenticationError_returnsNotAvailableForHardwareUnavailable() { helper.onAuthenticationError(BiometricPrompt.ERROR_HW_UNAVAILABLE, ""); - verify(handler).complete(AuthResult.ERROR_NOT_AVAILABLE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.HARDWARE_UNAVAILABLE) + .setErrorMessage("") + .build()); } @Test - public void onAuthenticationError_returnsNotAvailableForHardwareNotPresent() { + public void onAuthenticationError_returnsHardwareNotPresent() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -112,7 +172,12 @@ public void onAuthenticationError_returnsNotAvailableForHardwareNotPresent() { helper.onAuthenticationError(BiometricPrompt.ERROR_HW_NOT_PRESENT, ""); - verify(handler).complete(AuthResult.ERROR_NOT_AVAILABLE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.NO_HARDWARE) + .setErrorMessage("") + .build()); } @Test @@ -129,7 +194,12 @@ public void onAuthenticationError_returnsTemporaryLockoutForLockout() { helper.onAuthenticationError(BiometricPrompt.ERROR_LOCKOUT, ""); - verify(handler).complete(AuthResult.ERROR_LOCKED_OUT_TEMPORARILY); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.LOCKED_OUT_TEMPORARILY) + .setErrorMessage("") + .build()); } @Test @@ -146,11 +216,16 @@ public void onAuthenticationError_returnsPermanentLockoutForLockoutPermanent() { helper.onAuthenticationError(BiometricPrompt.ERROR_LOCKOUT_PERMANENT, ""); - verify(handler).complete(AuthResult.ERROR_LOCKED_OUT_PERMANENTLY); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.LOCKED_OUT_PERMANENTLY) + .setErrorMessage("") + .build()); } @Test - public void onAuthenticationError_withoutSticky_returnsFailureForCanceled() { + public void onAuthenticationError_withoutSticky_returnsSystemCanceled() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -163,11 +238,76 @@ public void onAuthenticationError_withoutSticky_returnsFailureForCanceled() { helper.onAuthenticationError(BiometricPrompt.ERROR_CANCELED, ""); - verify(handler).complete(AuthResult.FAILURE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.SYSTEM_CANCELED) + .setErrorMessage("") + .build()); + } + + @Test + public void onAuthenticationError_returnsTimeout() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_TIMEOUT, ""); + + verify(handler) + .complete( + new AuthResult.Builder().setCode(AuthResultCode.TIMEOUT).setErrorMessage("").build()); + } + + @Test + public void onAuthenticationError_returnsNoSpace() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_NO_SPACE, ""); + + verify(handler) + .complete( + new AuthResult.Builder().setCode(AuthResultCode.NO_SPACE).setErrorMessage("").build()); + } + + @Test + public void onAuthenticationError_returnsSecurityUpdateRequired() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED, ""); + + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.SECURITY_UPDATE_REQUIRED) + .setErrorMessage("") + .build()); } @Test - public void onAuthenticationError_withoutSticky_returnsFailureForOtherCases() { + public void onAuthenticationError_returnsUnknownForOtherCases() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -178,9 +318,14 @@ public void onAuthenticationError_withoutSticky_returnsFailureForOtherCases() { handler, true); - helper.onAuthenticationError(BiometricPrompt.ERROR_VENDOR, ""); + helper.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, ""); - verify(handler).complete(AuthResult.FAILURE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.UNKNOWN_ERROR) + .setErrorMessage("") + .build()); } private FragmentActivity buildMockActivityWithContext(FragmentActivity mockActivity) { diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java index 1fbc20a7803..db78bc07482 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -32,6 +32,7 @@ import io.flutter.plugins.localauth.Messages.AuthClassification; import io.flutter.plugins.localauth.Messages.AuthOptions; import io.flutter.plugins.localauth.Messages.AuthResult; +import io.flutter.plugins.localauth.Messages.AuthResultCode; import io.flutter.plugins.localauth.Messages.AuthStrings; import io.flutter.plugins.localauth.Messages.Result; import java.util.List; @@ -74,7 +75,7 @@ public void authenticate_returnsErrorWhenAuthInProgress() { plugin.authenticate(defaultOptions, dummyStrings, mockResult); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); verify(mockResult).success(captor.capture()); - assertEquals(AuthResult.ERROR_ALREADY_IN_PROGRESS, captor.getValue()); + assertEquals(AuthResultCode.ALREADY_IN_PROGRESS, captor.getValue().getCode()); } @Test @@ -86,7 +87,7 @@ public void authenticate_returnsErrorWithNoForegroundActivity() { plugin.authenticate(defaultOptions, dummyStrings, mockResult); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); verify(mockResult).success(captor.capture()); - assertEquals(AuthResult.ERROR_NO_ACTIVITY, captor.getValue()); + assertEquals(AuthResultCode.NO_ACTIVITY, captor.getValue().getCode()); } @Test @@ -98,7 +99,7 @@ public void authenticate_returnsErrorWhenActivityNotFragmentActivity() { plugin.authenticate(defaultOptions, dummyStrings, mockResult); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); verify(mockResult).success(captor.capture()); - assertEquals(AuthResult.ERROR_NOT_FRAGMENT_ACTIVITY, captor.getValue()); + assertEquals(AuthResultCode.NOT_FRAGMENT_ACTIVITY, captor.getValue().getCode()); } @Test @@ -111,7 +112,7 @@ public void authenticate_returnsErrorWhenDeviceNotSupported() { plugin.authenticate(defaultOptions, dummyStrings, mockResult); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); verify(mockResult).success(captor.capture()); - assertEquals(AuthResult.ERROR_NOT_AVAILABLE, captor.getValue()); + assertEquals(AuthResultCode.NO_CREDENTIALS, captor.getValue().getCode()); } @Test diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart index 3c81c90297d..c00975d217b 100644 --- a/packages/local_auth/local_auth_android/lib/local_auth_android.dart +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; import 'src/auth_messages_android.dart'; @@ -43,58 +42,75 @@ class LocalAuthAndroid extends LocalAuthPlatform { ), _pigeonStringsFromAuthMessages(localizedReason, authMessages), ); - // TODO(stuartmorgan): Replace this with structured errors, coordinated - // across all platform implementations, per - // https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#platform-exception-handling - // The PlatformExceptions thrown here are for compatibiilty with the - // previous Java implementation. - switch (result) { - case AuthResult.success: + switch (result.code) { + case AuthResultCode.success: return true; - case AuthResult.failure: - return false; - case AuthResult.errorAlreadyInProgress: - throw PlatformException( - code: 'auth_in_progress', - message: 'Authentication in progress', + case AuthResultCode.launchedSettings: + case AuthResultCode.negativeButton: + case AuthResultCode.userCanceled: + // Variants of user cancelation format are not currently distinguished, + // but could be if there's a use case for it in the future. + throw const LocalAuthException( + code: LocalAuthExceptionCode.userCanceled, ); - case AuthResult.errorNoActivity: - throw PlatformException( - code: 'no_activity', - message: 'local_auth plugin requires a foreground activity', + case AuthResultCode.systemCanceled: + throw const LocalAuthException( + code: LocalAuthExceptionCode.systemCanceled, ); - case AuthResult.errorNotFragmentActivity: - throw PlatformException( - code: 'no_fragment_activity', - message: - 'local_auth plugin requires activity to be a FragmentActivity.', + case AuthResultCode.timeout: + throw const LocalAuthException(code: LocalAuthExceptionCode.timeout); + case AuthResultCode.alreadyInProgress: + throw const LocalAuthException( + code: LocalAuthExceptionCode.authInProgress, ); - case AuthResult.errorNotAvailable: - throw PlatformException( - code: 'NotAvailable', - message: 'Security credentials not available.', + case AuthResultCode.noActivity: + throw const LocalAuthException( + code: LocalAuthExceptionCode.uiUnavailable, + description: 'No Activity available.', ); - case AuthResult.errorNotEnrolled: - throw PlatformException( - code: 'NotEnrolled', - message: 'No Biometrics enrolled on this device.', + case AuthResultCode.notFragmentActivity: + throw const LocalAuthException( + code: LocalAuthExceptionCode.uiUnavailable, + description: 'The current Activity must be a FragmentActivity.', ); - case AuthResult.errorLockedOutTemporarily: - throw PlatformException( - code: 'LockedOut', - message: - 'The operation was canceled because the API is locked out ' - 'due to too many attempts. This occurs after 5 failed ' - 'attempts, and lasts for 30 seconds.', + case AuthResultCode.noCredentials: + throw const LocalAuthException( + code: LocalAuthExceptionCode.noCredentialsSet, ); - case AuthResult.errorLockedOutPermanently: - throw PlatformException( - code: 'PermanentlyLockedOut', - message: - 'The operation was canceled because ERROR_LOCKOUT ' - 'occurred too many times. Biometric authentication is disabled ' - 'until the user unlocks with strong authentication ' - '(PIN/Pattern/Password)', + case AuthResultCode.noHardware: + throw const LocalAuthException( + code: LocalAuthExceptionCode.noBiometricHardware, + ); + case AuthResultCode.hardwareUnavailable: + throw const LocalAuthException( + code: LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable, + ); + case AuthResultCode.notEnrolled: + throw const LocalAuthException( + code: LocalAuthExceptionCode.noBiometricsEnrolled, + ); + case AuthResultCode.lockedOutTemporarily: + throw const LocalAuthException( + code: LocalAuthExceptionCode.temporaryLockout, + ); + case AuthResultCode.lockedOutPermanently: + throw const LocalAuthException( + code: LocalAuthExceptionCode.biometricLockout, + ); + case AuthResultCode.noSpace: + throw LocalAuthException( + code: LocalAuthExceptionCode.deviceError, + description: 'Not enough space available: ${result.errorMessage}', + ); + case AuthResultCode.securityUpdateRequired: + throw LocalAuthException( + code: LocalAuthExceptionCode.deviceError, + description: 'Security update required: ${result.errorMessage}', + ); + case AuthResultCode.unknownError: + throw LocalAuthException( + code: LocalAuthExceptionCode.unknownError, + description: result.errorMessage, ); } } diff --git a/packages/local_auth/local_auth_android/lib/src/messages.g.dart b/packages/local_auth/local_auth_android/lib/src/messages.g.dart index ad0c2be01ef..98ec2c991f1 100644 --- a/packages/local_auth/local_auth_android/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_android/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 (v22.4.0), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), 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 @@ -19,34 +19,64 @@ PlatformException _createConnectionError(String channelName) { } /// Possible outcomes of an authentication attempt. -enum AuthResult { +enum AuthResultCode { /// The user authenticated successfully. success, - /// The user failed to successfully authenticate. - failure, + /// The user launched the settings dialog. + launchedSettings, + + /// The user pressed the negative button, which corresponds to + /// [AuthStrings.cancelButton]. + negativeButton, + + /// The user canceled authentication without pressing the negative button. + /// + /// This may be triggered by a swipe or a back button, for example. + userCanceled, + + /// Authentication was caneceled by the system. + systemCanceled, + + /// Authentication timed out. + timeout, /// An authentication was already in progress. - errorAlreadyInProgress, + alreadyInProgress, /// There is no foreground activity. - errorNoActivity, + noActivity, /// The foreground activity is not a FragmentActivity. - errorNotFragmentActivity, + notFragmentActivity, + + /// The device does not have any credentials available. + noCredentials, + + /// No biometric hardware is present. + noHardware, - /// The authentication system was not available. - errorNotAvailable, + /// The biometric is temporarily unavailable. + hardwareUnavailable, /// No biometrics are enrolled. - errorNotEnrolled, + notEnrolled, /// The user is locked out temporarily due to too many failed attempts. - errorLockedOutTemporarily, + lockedOutTemporarily, /// The user is locked out until they log in another way due to too many /// failed attempts. - errorLockedOutPermanently, + lockedOutPermanently, + + /// The device does not have enough storage to complete authentication. + noSpace, + + /// The hardware is unavailable until a security update is performed. + securityUpdateRequired, + + /// Some unrecognized error case was encountered + unknownError, } /// Pigeon equivalent of the subset of BiometricType used by Android. @@ -121,6 +151,29 @@ class AuthStrings { } } +/// The results of an authentication request. +class AuthResult { + AuthResult({required this.code, this.errorMessage}); + + /// The specific result returned from the SDK. + AuthResultCode code; + + /// The error message associated with the result, if any. + String? errorMessage; + + Object encode() { + return [code, errorMessage]; + } + + static AuthResult decode(Object result) { + result as List; + return AuthResult( + code: result[0]! as AuthResultCode, + errorMessage: result[1] as String?, + ); + } +} + class AuthOptions { AuthOptions({ required this.biometricOnly, @@ -164,7 +217,7 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is AuthResult) { + } else if (value is AuthResultCode) { buffer.putUint8(129); writeValue(buffer, value.index); } else if (value is AuthClassification) { @@ -173,9 +226,12 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is AuthStrings) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is AuthOptions) { + } else if (value is AuthResult) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is AuthOptions) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -186,13 +242,15 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: final int? value = readValue(buffer) as int?; - return value == null ? null : AuthResult.values[value]; + return value == null ? null : AuthResultCode.values[value]; case 130: final int? value = readValue(buffer) as int?; return value == null ? null : AuthClassification.values[value]; case 131: return AuthStrings.decode(readValue(buffer)!); case 132: + return AuthResult.decode(readValue(buffer)!); + case 133: return AuthOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); diff --git a/packages/local_auth/local_auth_android/pigeons/messages.dart b/packages/local_auth/local_auth_android/pigeons/messages.dart index f78ddb1a57c..87ef8d6e392 100644 --- a/packages/local_auth/local_auth_android/pigeons/messages.dart +++ b/packages/local_auth/local_auth_android/pigeons/messages.dart @@ -43,34 +43,75 @@ class AuthStrings { } /// Possible outcomes of an authentication attempt. -enum AuthResult { +enum AuthResultCode { /// The user authenticated successfully. success, - /// The user failed to successfully authenticate. - failure, + /// The user launched the settings dialog. + launchedSettings, + + /// The user pressed the negative button, which corresponds to + /// [AuthStrings.cancelButton]. + negativeButton, + + /// The user canceled authentication without pressing the negative button. + /// + /// This may be triggered by a swipe or a back button, for example. + userCanceled, + + /// Authentication was caneceled by the system. + systemCanceled, + + /// Authentication timed out. + timeout, /// An authentication was already in progress. - errorAlreadyInProgress, + alreadyInProgress, /// There is no foreground activity. - errorNoActivity, + noActivity, /// The foreground activity is not a FragmentActivity. - errorNotFragmentActivity, + notFragmentActivity, + + /// The device does not have any credentials available. + noCredentials, + + /// No biometric hardware is present. + noHardware, - /// The authentication system was not available. - errorNotAvailable, + /// The biometric is temporarily unavailable. + hardwareUnavailable, /// No biometrics are enrolled. - errorNotEnrolled, + notEnrolled, /// The user is locked out temporarily due to too many failed attempts. - errorLockedOutTemporarily, + lockedOutTemporarily, /// The user is locked out until they log in another way due to too many /// failed attempts. - errorLockedOutPermanently, + lockedOutPermanently, + + /// The device does not have enough storage to complete authentication. + noSpace, + + /// The hardware is unavailable until a security update is performed. + securityUpdateRequired, + + /// Some unrecognized error case was encountered + unknownError, +} + +/// The results of an authentication request. +class AuthResult { + const AuthResult({required this.code, this.errorMessage}); + + /// The specific result returned from the SDK. + final AuthResultCode code; + + /// The error message associated with the result, if any. + final String? errorMessage; } class AuthOptions { diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index badf49ed45a..2755bf1d7ec 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_android description: Android implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.51 +version: 2.0.0 environment: sdk: ^3.7.0 diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart index 1342b388bfa..61b9b4da768 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:local_auth_android/local_auth_android.dart'; import 'package:local_auth_android/src/messages.g.dart'; @@ -93,7 +92,7 @@ void main() { test('passes default values when nothing is provided', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); const String reason = 'test reason'; await plugin.authenticate( @@ -130,7 +129,7 @@ void main() { () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); const String reason = 'test reason'; await plugin.authenticate( @@ -169,7 +168,7 @@ void main() { test('passes all non-default values correctly', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); // These are arbitrary values; all that matters is that: // - they are different from the defaults, and @@ -221,7 +220,7 @@ void main() { test('passes provided messages with default fallbacks', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); // These are arbitrary values; all that matters is that: // - they are different from the defaults, and @@ -273,7 +272,7 @@ void main() { test('passes default values', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); await plugin.authenticate( localizedReason: 'reason', @@ -293,7 +292,7 @@ void main() { test('passes provided non-default values', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); await plugin.authenticate( localizedReason: 'reason', @@ -321,7 +320,7 @@ void main() { test('handles success', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); final bool result = await plugin.authenticate( localizedReason: 'reason', @@ -331,25 +330,58 @@ void main() { expect(result, true); }); - test('handles failure', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.failure); + test( + 'converts negativeButton to userCanceled LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.negativeButton), + ); - final bool result = await plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ); + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.userCanceled, + ), + ), + ); + }, + ); - expect(result, false); - }); + test( + 'converts userCanceled to userCanceled LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.userCanceled), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.userCanceled, + ), + ), + ); + }, + ); test( - 'converts errorAlreadyInProgress to legacy PlatformException', + 'converts systemCanceled to systemCanceled LocalAuthException', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorAlreadyInProgress); + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.systemCanceled), + ); expect( () async => plugin.authenticate( @@ -357,26 +389,20 @@ void main() { authMessages: [], ), throwsA( - isA() - .having( - (PlatformException e) => e.code, - 'code', - 'auth_in_progress', - ) - .having( - (PlatformException e) => e.message, - 'message', - 'Authentication in progress', - ), + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.systemCanceled, + ), ), ); }, ); - test('converts errorNoActivity to legacy PlatformException', () async { + test('converts timeout to timeout LocalAuthException', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorNoActivity); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.timeout)); expect( () async => plugin.authenticate( @@ -384,23 +410,21 @@ void main() { authMessages: [], ), throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'no_activity') - .having( - (PlatformException e) => e.message, - 'message', - 'local_auth plugin requires a foreground activity', - ), + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.timeout, + ), ), ); }); test( - 'converts errorNotFragmentActivity to legacy PlatformException', + 'converts alreadyInProgress to authInProgress LocalAuthException', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorNotFragmentActivity); + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.alreadyInProgress), + ); expect( () async => plugin.authenticate( @@ -408,26 +432,20 @@ void main() { authMessages: [], ), throwsA( - isA() - .having( - (PlatformException e) => e.code, - 'code', - 'no_fragment_activity', - ) - .having( - (PlatformException e) => e.message, - 'message', - 'local_auth plugin requires activity to be a FragmentActivity.', - ), + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.authInProgress, + ), ), ); }, ); - test('converts errorNotAvailable to legacy PlatformException', () async { + test('converts noActivity to uiUnavailable LocalAuthException', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorNotAvailable); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.noActivity)); expect( () async => plugin.authenticate( @@ -435,21 +453,180 @@ void main() { authMessages: [], ), throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'NotAvailable') - .having( - (PlatformException e) => e.message, - 'message', - 'Security credentials not available.', - ), + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ), ), ); }); - test('converts errorNotEnrolled to legacy PlatformException', () async { + test( + 'converts notFragmentActivity to uiUnavailable LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.notFragmentActivity), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ), + ), + ); + }, + ); + + test( + 'converts noCredentials to noCredentialsSet LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.noCredentials), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noCredentialsSet, + ), + ), + ); + }, + ); + + test( + 'converts noHardware to noBiometricHardware LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.noHardware), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricHardware, + ), + ), + ); + }, + ); + + test( + 'converts hardwareUnavailable to biometricHardwareTemporarilyUnavailable LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.hardwareUnavailable), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable, + ), + ), + ); + }, + ); + + test( + 'converts notEnrolled to noBiometricsEnrolled LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.notEnrolled), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricsEnrolled, + ), + ), + ); + }, + ); + + test( + 'converts lockedOutTemporarily to temporaryLockout LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.lockedOutTemporarily), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.temporaryLockout, + ), + ), + ); + }, + ); + + test( + 'converts lockedOutPermanently to biometricLockout LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.lockedOutPermanently), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.biometricLockout, + ), + ), + ); + }, + ); + + test('converts noSpace to deviceError LocalAuthException', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorNotEnrolled); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.noSpace)); expect( () async => plugin.authenticate( @@ -457,23 +634,28 @@ void main() { authMessages: [], ), throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'NotEnrolled') + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.deviceError, + ) .having( - (PlatformException e) => e.message, - 'message', - 'No Biometrics enrolled on this device.', + (LocalAuthException e) => e.description, + 'description', + startsWith('Not enough space available:'), ), ), ); }); test( - 'converts errorLockedOutTemporarily to legacy PlatformException', + 'converts securityUpdateRequired to deviceError LocalAuthException', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorLockedOutTemporarily); + when(api.authenticate(any, any)).thenAnswer( + (_) async => + AuthResult(code: AuthResultCode.securityUpdateRequired), + ); expect( () async => plugin.authenticate( @@ -481,14 +663,16 @@ void main() { authMessages: [], ), throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'LockedOut') + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.deviceError, + ) .having( - (PlatformException e) => e.message, - 'message', - 'The operation was canceled because the API is locked out ' - 'due to too many attempts. This occurs after 5 failed ' - 'attempts, and lasts for 30 seconds.', + (LocalAuthException e) => e.description, + 'description', + startsWith('Security update required:'), ), ), ); @@ -496,11 +680,15 @@ void main() { ); test( - 'converts errorLockedOutPermanently to legacy PlatformException', + 'converts unknownError to unknownError LocalAuthException, passing error message', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorLockedOutPermanently); + const String errorMessage = 'Some error message'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult( + code: AuthResultCode.unknownError, + errorMessage: errorMessage, + ), + ); expect( () async => plugin.authenticate( @@ -508,19 +696,16 @@ void main() { authMessages: [], ), throwsA( - isA() + isA() .having( - (PlatformException e) => e.code, + (LocalAuthException e) => e.code, 'code', - 'PermanentlyLockedOut', + LocalAuthExceptionCode.unknownError, ) .having( - (PlatformException e) => e.message, - 'message', - 'The operation was canceled because ERROR_LOCKOUT occurred ' - 'too many times. Biometric authentication is disabled ' - 'until the user unlocks with strong ' - 'authentication (PIN/Pattern/Password)', + (LocalAuthException e) => e.description, + 'description', + errorMessage, ), ), ); diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart b/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart index 549cb242bcd..2d0f81304c3 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in local_auth_android/test/local_auth_test.dart. // Do not manually edit this file. @@ -17,11 +17,17 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeAuthResult_0 extends _i1.SmartFake implements _i2.AuthResult { + _FakeAuthResult_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [LocalAuthApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -83,7 +89,10 @@ class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi { (super.noSuchMethod( Invocation.method(#authenticate, [options, strings]), returnValue: _i4.Future<_i2.AuthResult>.value( - _i2.AuthResult.success, + _FakeAuthResult_0( + this, + Invocation.method(#authenticate, [options, strings]), + ), ), ) as _i4.Future<_i2.AuthResult>); From 8ce45dc822e0fac5cc1d015848bea55b36247895 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 3 Sep 2025 16:41:04 -0400 Subject: [PATCH 06/19] Convert iOS --- .../local_auth/local_auth_darwin/CHANGELOG.md | 3 +- .../Tests/FLALocalAuthPluginTests.swift | 444 ++++++++--- .../local_auth_darwin/LocalAuthPlugin.swift | 135 ++-- .../local_auth_darwin/messages.g.swift | 36 +- .../lib/local_auth_darwin.dart | 85 ++- .../local_auth_darwin/lib/src/messages.g.dart | 44 +- .../local_auth_darwin/pigeons/messages.dart | 45 +- .../local_auth/local_auth_darwin/pubspec.yaml | 2 +- .../test/local_auth_darwin_test.dart | 706 +++++++++++++----- .../test/local_auth_darwin_test.mocks.dart | 3 +- 10 files changed, 1065 insertions(+), 438 deletions(-) diff --git a/packages/local_auth/local_auth_darwin/CHANGELOG.md b/packages/local_auth/local_auth_darwin/CHANGELOG.md index 382bd8e75f5..bcbb1ba39c8 100644 --- a/packages/local_auth/local_auth_darwin/CHANGELOG.md +++ b/packages/local_auth/local_auth_darwin/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.0 +* Switches to `LocalAuthException` for error reporting. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.6.0 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 096b54ec1ee..a7194ed3897 100644 --- a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift +++ b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift @@ -171,13 +171,10 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testSuccessfullAuthWithBiometrics() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider - ) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings() stubAuthContext.expectBiometrics = true @@ -187,7 +184,6 @@ class LocalAuthPluginTests: XCTestCase { options: AuthOptions(biometricOnly: true, sticky: false, useErrorDialogs: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): XCTAssertEqual(successDetails.result, .success) @@ -202,13 +198,10 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testSuccessfullAuthWithoutBiometrics() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() - let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings() stubAuthContext.evaluateResponse = true @@ -218,7 +211,6 @@ class LocalAuthPluginTests: XCTestCase { options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): XCTAssertEqual(successDetails.result, .success) @@ -233,12 +225,10 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testFailedAuthWithBiometrics() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings() stubAuthContext.expectBiometrics = true @@ -250,14 +240,9 @@ class LocalAuthPluginTests: XCTestCase { options: AuthOptions(biometricOnly: true, sticky: false, useErrorDialogs: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) - // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration - // behavior, so is preserved as part of the migration, but a failed - // authentication should return failure, not an error that results in a - // PlatformException. switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .errorNotAvailable) + XCTAssertEqual(successDetails.result, .authenticationFailed) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -267,28 +252,81 @@ class LocalAuthPluginTests: XCTestCase { } @MainActor - func testFailedAuthWithErrorUserCancelled() { + func testFailedAuthWithErrorAppCancel() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError( + domain: "LocalAuthentication", code: LAError.appCancel.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .appCancel) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorSystemCancel() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError( + domain: "LocalAuthentication", code: LAError.systemCancel.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .systemCancel) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorUserCancel() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( domain: "LocalAuthentication", code: LAError.userCancel.rawValue) - let expectation = expectation(description: "Result is called for user cancel") + let expectation = expectation(description: "Result is called") 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) + XCTAssertEqual(successDetails.result, .userCancel) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -300,26 +338,80 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testFailedAuthWithErrorUserFallback() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( domain: "LocalAuthentication", code: LAError.userFallback.rawValue) - let expectation = expectation(description: "Result is called for user fallback") + let expectation = expectation(description: "Result is called") 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) + XCTAssertEqual(successDetails.result, .userFallback) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @available(macOS 11.2, *) + @MainActor + func testFailedAuthWithErrorBiometricDisconnected() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryDisconnected.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .biometryDisconnected) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorBiometricLockout() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryLockout.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .biometryLockout) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -331,26 +423,193 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testFailedAuthWithErrorBiometricNotAvailable() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings() stubAuthContext.canEvaluateError = NSError( domain: "LocalAuthentication", code: LAError.biometryNotAvailable.rawValue) - let expectation = expectation(description: "Result is called for biometric not available") + let expectation = expectation(description: "Result is called") 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) + XCTAssertEqual(successDetails.result, .biometryNotAvailable) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorBiometricNotEnrolled() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryNotEnrolled.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .biometryNotEnrolled) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @available(macOS 11.2, *) + @MainActor + func testFailedAuthWithErrorBiometricNotPaired() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryNotPaired.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .biometryNotPaired) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorBiometricInvalidContext() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.invalidContext.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .invalidContext) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @available(macOS 12.0, *) + @MainActor + func testFailedAuthWithErrorBiometricInvalidDimensions() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.invalidDimensions.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .invalidDimensions) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorBiometricNotInteractive() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.notInteractive.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .notInteractive) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorBiometricPasscodeNotSet() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.passcodeNotSet.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .passcodeNotSet) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -362,12 +621,10 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testFailedWithUnknownErrorCode() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError(domain: "error", code: 99) @@ -377,10 +634,9 @@ class LocalAuthPluginTests: XCTestCase { options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .errorNotAvailable) + XCTAssertEqual(successDetails.result, .unknownError) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -392,12 +648,10 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testSystemCancelledWithoutStickyAuth() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError(domain: "error", code: LAError.systemCancel.rawValue) @@ -407,10 +661,9 @@ class LocalAuthPluginTests: XCTestCase { options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .failure) + XCTAssertEqual(successDetails.result, .systemCancel) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -422,12 +675,10 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testFailedAuthWithoutBiometrics() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( @@ -438,14 +689,9 @@ class LocalAuthPluginTests: XCTestCase { options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) - // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration - // behavior, so is preserved as part of the migration, but a failed - // authentication should return failure, not an error that results in a - // PlatformException. switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .errorNotAvailable) + XCTAssertEqual(successDetails.result, .authenticationFailed) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -488,12 +734,10 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testLocalizedFallbackTitle() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings(localizedFallbackTitle: "a title") stubAuthContext.evaluateResponse = true @@ -512,12 +756,10 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testSkippedLocalizedFallbackTitle() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let strings = createAuthStrings(localizedFallbackTitle: nil) stubAuthContext.evaluateResponse = true @@ -535,12 +777,10 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withEnrolledHardware() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) stubAuthContext.expectBiometrics = true @@ -550,12 +790,10 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withNonEnrolledHardware() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) stubAuthContext.expectBiometrics = true stubAuthContext.canEvaluateError = NSError( @@ -567,12 +805,10 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withBiometryNotAvailable() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) stubAuthContext.expectBiometrics = true stubAuthContext.canEvaluateError = NSError( @@ -584,12 +820,10 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withBiometryNotAvailableWhenPermissionsDenied() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) stubAuthContext.expectBiometrics = true stubAuthContext.biometryType = LABiometryType.touchID @@ -602,12 +836,10 @@ class LocalAuthPluginTests: XCTestCase { func testGetEnrolledBiometricsWithFaceID() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) stubAuthContext.expectBiometrics = true if #available(iOS 11, macOS 10.15, *) { @@ -621,12 +853,10 @@ class LocalAuthPluginTests: XCTestCase { func testGetEnrolledBiometricsWithTouchID() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) stubAuthContext.expectBiometrics = true stubAuthContext.biometryType = .touchID @@ -638,12 +868,10 @@ class LocalAuthPluginTests: XCTestCase { func testGetEnrolledBiometricsWithoutEnrolledHardware() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) stubAuthContext.expectBiometrics = true stubAuthContext.canEvaluateError = NSError( @@ -655,12 +883,10 @@ class LocalAuthPluginTests: XCTestCase { func testIsDeviceSupportedHandlesSupported() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let result = try plugin.isDeviceSupported() XCTAssertTrue(result) @@ -670,12 +896,10 @@ class LocalAuthPluginTests: XCTestCase { let stubAuthContext = StubAuthContext() // An arbitrary error to cause canEvaluatePolicy to return false. stubAuthContext.canEvaluateError = NSError(domain: "error", code: 1) - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + alertFactory: StubAlertFactory(), + viewProvider: StubViewProvider()) let result = try plugin.isDeviceSupported() XCTAssertFalse(result) 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 e42cc3ec7bf..0359f874859 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 @@ -183,8 +183,8 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch completion( .success( AuthResultDetails( - result: .failure, - errorMessage: "evaluatePolicy failed without an error" + result: .unknownError, + errorMessage: "canEvaluatePolicy failed without an error" ))) } } @@ -255,18 +255,23 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch alert.addButton(withTitle: dismissButtonTitle) if let window = viewProvider.view?.window { alert.beginSheetModal(for: window) { [weak self] code in - self?.handleResult(succeeded: false, completion: completion) + self?.handleResult(result: .showedAlert, completion: completion) } } else { alert.runModal() - self.handleResult(succeeded: false, completion: completion) + self.handleResult(result: .showedAlert, completion: completion) } #elseif os(iOS) // TODO(stuartmorgan): Get the view controller from the view provider once it's possible. // See https://github.com/flutter/flutter/issues/104117. guard let controller = UIApplication.shared.delegate?.window??.rootViewController else { - // TODO(stuartmorgan): Create a new error code for failure to show UI, and return it here. - self.handleResult(succeeded: false, completion: completion) + completion( + .success( + AuthResultDetails( + result: .uiUnavailable, + errorMessage: "Unable to obtain root view controller", + errorDetails: nil) + )) return } let alert = alertFactory.createAlertController( @@ -278,7 +283,7 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch title: dismissButtonTitle, style: .default ) { [weak self] action in - self?.handleResult(succeeded: false, completion: completion) + self?.handleResult(result: .showedAlert, completion: completion) } alert.addAction(defaultAction) @@ -290,7 +295,7 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch style: .default ) { [weak self] action in UIApplication.shared.open(url, options: [:], completionHandler: nil) - self?.handleResult(succeeded: false, completion: completion) + self?.handleResult(result: .showedAlert, completion: completion) } alert.addAction(additionalAction) } @@ -306,54 +311,38 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch completion: @escaping (Result) -> Void ) { if success { - handleResult(succeeded: true, completion: completion) + handleResult(result: .success, completion: completion) return } if let error = error as? NSError { - switch LAError.Code(rawValue: error.code) { - case .biometryNotAvailable, - .biometryNotEnrolled, - .biometryLockout, - .userFallback, - .passcodeNotSet, - .authenticationFailed: - handleError(error, options: options, strings: strings, completion: completion) - case .systemCancel: - if options.sticky { - lastCallState = StickyAuthState( - options: options, - strings: strings, - resultHandler: completion) - } else { - handleResult(succeeded: false, completion: completion) - } - default: + if error.code == LAError.Code.systemCancel.rawValue && options.sticky { + lastCallState = StickyAuthState( + options: options, + strings: strings, + resultHandler: completion) + } else { handleError(error, options: options, strings: strings, completion: completion) } } else { - // The Obj-C declaration of evaluatePolicy defines the callback type as NSError*, but the - // Swift version is (any Error)?, so provide a fallback in case somehow the type is not - // NSError. - // TODO(stuartmorgan): Add an "unknown error" enum option and return that here instead of - // failure. + // This should not happen according to docs, but if it ever does the plugin should still + // fire the completion. completion( .success( AuthResultDetails( - result: .failure, - errorMessage: "Unknown error from evaluatePolicy", - errorDetails: error?.localizedDescription) - )) + result: .unknownError, + errorMessage: "evaluatePolicy failed without an error" + ))) } } private func handleResult( - succeeded: Bool, completion: @escaping (Result) -> Void + result: AuthResult, completion: @escaping (Result) -> Void ) { completion( .success( AuthResultDetails( - result: succeeded ? .success : .failure, + result: result, errorMessage: nil, errorDetails: nil) )) @@ -368,8 +357,29 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch let result: AuthResult let errorCode = LAError.Code(rawValue: authError.code) switch errorCode { - case .passcodeNotSet, - .biometryNotEnrolled: + case .appCancel: + result = .appCancel + case .systemCancel: + result = .systemCancel + case .userCancel: + result = .userCancel + case .biometryDisconnected: + result = .biometryDisconnected + case .biometryLockout: + if options.useErrorDialogs { + DispatchQueue.main.async { [weak self] in + self?.showAlert( + message: strings.lockOut, + dismissButtonTitle: strings.cancelButton, + openSettingsButtonTitle: nil, + completion: completion) + } + return + } + result = .biometryLockout + case .biometryNotAvailable: + result = .biometryNotAvailable + case .biometryNotEnrolled: if options.useErrorDialogs { DispatchQueue.main.async { [weak self] in self?.showAlert( @@ -380,33 +390,40 @@ 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( - message: strings.lockOut, - dismissButtonTitle: strings.cancelButton, - openSettingsButtonTitle: nil, - completion: completion) + result = .biometryNotEnrolled + case .biometryNotPaired: + result = .biometryNotPaired + case .authenticationFailed: + result = .authenticationFailed + case .invalidContext: + result = .invalidContext + case .invalidDimensions: + result = .invalidDimensions + case .notInteractive: + result = .notInteractive + case .passcodeNotSet: + if options.useErrorDialogs { + DispatchQueue.main.async { [weak self] in + self?.showAlert( + message: strings.goToSettingsDescription, + dismissButtonTitle: strings.cancelButton, + openSettingsButtonTitle: strings.goToSettingsButton, + completion: completion) + } + return } - return + result = .passcodeNotSet + case .userFallback: + result = .userFallback default: - // TODO(stuartmorgan): Improve the error mapping as part of a cross-platform overhaul of - // error handling. See https://github.com/flutter/flutter/issues/113687 - result = .errorNotAvailable + result = .unknownError } completion( .success( AuthResultDetails( result: result, errorMessage: authError.localizedDescription, - errorDetails: authError.domain) + errorDetails: "\(authError.domain): \(authError.code)") )) } 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 a0706af4fe1..777c7e254b4 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.5.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -133,20 +133,26 @@ func deepHashmessages(value: Any?, hasher: inout Hasher) { enum AuthResult: Int { /// The user authenticated successfully. case success = 0 - /// The user failed to successfully authenticate. - case failure = 1 - /// The authentication system was not available. - case errorNotAvailable = 2 - /// No biometrics are enrolled. - 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 + /// Native UI needed to be displayed, but couldn't be. + case uiUnavailable = 1 + /// The plugin showed an alert as the final step. + case showedAlert = 2 + case appCancel = 3 + case systemCancel = 4 + case userCancel = 5 + case biometryDisconnected = 6 + case biometryLockout = 7 + case biometryNotAvailable = 8 + case biometryNotEnrolled = 9 + case biometryNotPaired = 10 + case authenticationFailed = 11 + case invalidContext = 12 + case invalidDimensions = 13 + case notInteractive = 14 + case passcodeNotSet = 15 + case userFallback = 16 + /// An error other than the expected types occurred. + case unknownError = 17 } /// 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 a9c4e223b17..2ce9f131e00 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 @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; import 'src/messages.g.dart'; @@ -51,53 +50,53 @@ class LocalAuthDarwin extends LocalAuthPlatform { ? _pigeonStringsFromMacOSAuthMessages(localizedReason, authMessages) : _pigeonStringsFromiOSAuthMessages(localizedReason, authMessages), ); - // TODO(stuartmorgan): Replace this with structured errors, coordinated - // across all platform implementations, per - // https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#platform-exception-handling - // The PlatformExceptions thrown here are for compatibiilty with the - // previous Objective-C implementation. + LocalAuthExceptionCode code; + String? description = resultDetails.errorMessage; switch (resultDetails.result) { case AuthResult.success: return true; - case AuthResult.failure: + case AuthResult.authenticationFailed: return false; - case AuthResult.errorNotAvailable: - throw PlatformException( - code: 'NotAvailable', - message: resultDetails.errorMessage, - details: resultDetails.errorDetails, - ); - case AuthResult.errorNotEnrolled: - throw PlatformException( - code: 'NotEnrolled', - message: resultDetails.errorMessage, - details: resultDetails.errorDetails, - ); - case AuthResult.errorPasscodeNotSet: - throw PlatformException( - 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, - ); + case AuthResult.showedAlert: + // Temporary compat with previous return until alerts are removed. + return false; + case AuthResult.appCancel: + // If the plugin client intentionally canceled authentication, no need + // to return a specific error. + return false; + case AuthResult.uiUnavailable: + code = LocalAuthExceptionCode.uiUnavailable; + case AuthResult.systemCancel: + code = LocalAuthExceptionCode.systemCanceled; + case AuthResult.userCancel: + code = LocalAuthExceptionCode.userCanceled; + case AuthResult.biometryDisconnected: + code = LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable; + case AuthResult.biometryLockout: + code = LocalAuthExceptionCode.biometricLockout; + case AuthResult.biometryNotAvailable: + // Treated as no hardware since docs suggest that this means that there is + // no known device; paired but not connected is biometryDisconnected. + case AuthResult.biometryNotPaired: + code = LocalAuthExceptionCode.noBiometricHardware; + case AuthResult.biometryNotEnrolled: + code = LocalAuthExceptionCode.noBiometricsEnrolled; + case AuthResult.invalidContext: + case AuthResult.invalidDimensions: + case AuthResult.notInteractive: + code = LocalAuthExceptionCode.uiUnavailable; + case AuthResult.passcodeNotSet: + code = LocalAuthExceptionCode.noCredentialsSet; + case AuthResult.userFallback: + code = LocalAuthExceptionCode.userRequestedFallback; + case AuthResult.unknownError: + code = LocalAuthExceptionCode.unknownError; } + throw LocalAuthException( + code: code, + description: description, + details: resultDetails.errorDetails, + ); } @override 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 8b829b5e7bd..fc630518630 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.5.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), 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 @@ -41,26 +41,28 @@ enum AuthResult { /// The user authenticated successfully. success, - /// The user failed to successfully authenticate. - failure, - - /// The authentication system was not available. - errorNotAvailable, - - /// No biometrics are enrolled. - errorNotEnrolled, - - /// 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, + /// Native UI needed to be displayed, but couldn't be. + uiUnavailable, + + /// The plugin showed an alert as the final step. + showedAlert, + appCancel, + systemCancel, + userCancel, + biometryDisconnected, + biometryLockout, + biometryNotAvailable, + biometryNotEnrolled, + biometryNotPaired, + authenticationFailed, + invalidContext, + invalidDimensions, + notInteractive, + passcodeNotSet, + userFallback, + + /// An error other than the expected types occurred. + unknownError, } /// 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 5216c1dbe4d..f2a4401cd3f 100644 --- a/packages/local_auth/local_auth_darwin/pigeons/messages.dart +++ b/packages/local_auth/local_auth_darwin/pigeons/messages.dart @@ -39,26 +39,31 @@ enum AuthResult { /// The user authenticated successfully. success, - /// The user failed to successfully authenticate. - failure, - - /// The authentication system was not available. - errorNotAvailable, - - /// No biometrics are enrolled. - errorNotEnrolled, - - /// 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, + /// Native UI needed to be displayed, but couldn't be. + uiUnavailable, + + /// The plugin showed an alert as the final step. + showedAlert, + + // LAError codes; see + // https://developer.apple.com/documentation/localauthentication/laerror-swift.struct/code + appCancel, + systemCancel, + userCancel, + biometryDisconnected, + biometryLockout, + biometryNotAvailable, + biometryNotEnrolled, + biometryNotPaired, + authenticationFailed, + invalidContext, + invalidDimensions, + notInteractive, + passcodeNotSet, + userFallback, + + /// An error other than the expected types occurred. + unknownError, } class AuthOptions { diff --git a/packages/local_auth/local_auth_darwin/pubspec.yaml b/packages/local_auth/local_auth_darwin/pubspec.yaml index 5af84f8704b..a10d684a9e8 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.6.0 +version: 2.0.0 environment: sdk: ^3.7.0 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 344de548922..74cdad1830b 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 @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:local_auth_darwin/local_auth_darwin.dart'; import 'package:local_auth_darwin/src/messages.g.dart'; @@ -374,7 +373,8 @@ void main() { test('handles failure', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(result: AuthResult.failure), + (_) async => + AuthResultDetails(result: AuthResult.authenticationFailed), ); final bool result = await plugin.authenticate( @@ -385,189 +385,561 @@ void main() { expect(result, false); }); - test('converts errorNotAvailable to legacy PlatformException', () async { - const String errorMessage = 'a message'; - const String errorDetails = 'some details'; + test('handles appCancel as failure', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails( - result: AuthResult.errorNotAvailable, - errorMessage: errorMessage, - errorDetails: errorDetails, - ), + (_) async => AuthResultDetails(result: AuthResult.appCancel), ); - expect( - () async => plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ), - throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'NotAvailable') - .having( - (PlatformException e) => e.message, - 'message', - errorMessage, - ) - .having( - (PlatformException e) => e.details, - 'details', - errorDetails, - ), - ), + final bool result = await plugin.authenticate( + localizedReason: 'reason', + authMessages: [], ); + + expect(result, false); }); - test('converts errorNotEnrolled to legacy PlatformException', () async { - const String errorMessage = 'a message'; - const String errorDetails = 'some details'; + test('handles showedAlert as failure', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails( - result: AuthResult.errorNotEnrolled, - errorMessage: errorMessage, - errorDetails: errorDetails, - ), + (_) async => AuthResultDetails(result: AuthResult.showedAlert), ); - expect( - () async => plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ), - throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'NotEnrolled') - .having( - (PlatformException e) => e.message, - 'message', - errorMessage, - ) - .having( - (PlatformException e) => e.details, - 'details', - errorDetails, - ), - ), + final bool result = await plugin.authenticate( + localizedReason: 'reason', + authMessages: [], ); + + expect(result, false); }); - 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, - ), - ); + test( + 'converts uiUnavailable to LocalAuthExceptionCode.uiUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.uiUnavailable, + 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, - ), - ), - ); - }); + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException 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, - ), - ); + test( + 'converts systemCancel to LocalAuthExceptionCode.systemCanceled', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.systemCancel, + 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, - ), - ), - ); - }); + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.systemCanceled, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException 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, - ), - ); + test( + 'converts userCancel to LocalAuthExceptionCode.userCanceled', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.userCancel, + 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, - ), - ), - ); - }); + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.userCanceled, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts biometryDisconnected to LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryDisconnected, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode + .biometricHardwareTemporarilyUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts biometryLockout to LocalAuthExceptionCode.biometricLockout', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryLockout, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.biometricLockout, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts biometryNotAvailable to LocalAuthExceptionCode.noBiometricHardware', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryNotAvailable, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricHardware, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts biometryNotPaired to LocalAuthExceptionCode.noBiometricHardware', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryNotPaired, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricHardware, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts biometryNotEnrolled to LocalAuthExceptionCode.noBiometricsEnrolled', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryNotEnrolled, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricsEnrolled, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts invalidContext to LocalAuthExceptionCode.uiUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.invalidContext, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts invalidDimensions to LocalAuthExceptionCode.uiUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.invalidDimensions, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts notInteractive to LocalAuthExceptionCode.uiUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.notInteractive, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts passcodeNotSet to LocalAuthExceptionCode.noCredentialsSet', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.passcodeNotSet, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noCredentialsSet, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts userFallback to LocalAuthExceptionCode.userRequestedFallback', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.userFallback, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.userRequestedFallback, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); test( - 'converts errorPasscodeNotSet to legacy PlatformException', + 'converts unknownError to LocalAuthExceptionCode.unknownError', () async { const String errorMessage = 'a message'; const String errorDetails = 'some details'; when(api.authenticate(any, any)).thenAnswer( (_) async => AuthResultDetails( - result: AuthResult.errorPasscodeNotSet, + result: AuthResult.unknownError, errorMessage: errorMessage, errorDetails: errorDetails, ), @@ -579,19 +951,19 @@ void main() { authMessages: [], ), throwsA( - isA() + isA() .having( - (PlatformException e) => e.code, + (LocalAuthException e) => e.code, 'code', - 'PasscodeNotSet', + LocalAuthExceptionCode.unknownError, ) .having( - (PlatformException e) => e.message, - 'message', + (LocalAuthException e) => e.description, + 'description', errorMessage, ) .having( - (PlatformException e) => e.details, + (LocalAuthException e) => e.details, 'details', errorDetails, ), diff --git a/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.mocks.dart b/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.mocks.dart index ffa5c074a44..1c93c29927e 100644 --- a/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.mocks.dart +++ b/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in local_auth_darwin/test/local_auth_darwin_test.dart. // Do not manually edit this file. @@ -17,6 +17,7 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types From 6b44729a4dceb8bf0d03d1978fe4c20d6af1988c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 4 Sep 2025 16:42:29 -0400 Subject: [PATCH 07/19] Documentation updates --- packages/local_auth/local_auth/README.md | 7 +++---- .../local_auth/local_auth/lib/local_auth.dart | 10 ++++++---- .../local_auth/lib/src/local_auth.dart | 19 +++++++++---------- .../lib/types/auth_messages_ios.dart | 2 ++ .../lib/types/auth_messages_macos.dart | 2 ++ .../lib/local_auth_platform_interface.dart | 2 +- 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/local_auth/local_auth/README.md b/packages/local_auth/local_auth/README.md index 2a7e4c043a8..fbd86545b9a 100644 --- a/packages/local_auth/local_auth/README.md +++ b/packages/local_auth/local_auth/README.md @@ -158,14 +158,13 @@ each platform. ### Exceptions -`authenticate` throws `PlatformException`s in many error cases. See -`error_codes.dart` for known error codes that you may want to have specific -handling for. For example: +`authenticate` throws `LocalAuthException`s in most failure cases. See +`LocalAuthExceptionCodes` for known error codes that you may want to have +specific handling for. For example: ```dart import 'package:flutter/services.dart'; -import 'package:local_auth/error_codes.dart' as auth_error; import 'package:local_auth/local_auth.dart'; // ··· final LocalAuthentication auth = LocalAuthentication(); diff --git a/packages/local_auth/local_auth/lib/local_auth.dart b/packages/local_auth/local_auth/lib/local_auth.dart index 7c42fedc775..395b2a28254 100644 --- a/packages/local_auth/local_auth/lib/local_auth.dart +++ b/packages/local_auth/local_auth/lib/local_auth.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. export 'package:local_auth/src/local_auth.dart' show LocalAuthentication; -export 'package:local_auth_platform_interface/types/auth_options.dart' - show AuthenticationOptions; -export 'package:local_auth_platform_interface/types/biometric_type.dart' - show BiometricType; +export 'package:local_auth_platform_interface/local_auth_platform_interface.dart' + show + AuthenticationOptions, + BiometricType, + LocalAuthException, + LocalAuthExceptionCode; diff --git a/packages/local_auth/local_auth/lib/src/local_auth.dart b/packages/local_auth/local_auth/lib/src/local_auth.dart index 6d56548d018..69bb0e85a9c 100644 --- a/packages/local_auth/local_auth/lib/src/local_auth.dart +++ b/packages/local_auth/local_auth/lib/src/local_auth.dart @@ -10,7 +10,6 @@ import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:local_auth_android/local_auth_android.dart'; import 'package:local_auth_darwin/local_auth_darwin.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; @@ -21,7 +20,11 @@ class LocalAuthentication { /// Authenticates the user with biometrics available on the device while also /// allowing the user to use device authentication - pin, pattern, passcode. /// - /// Returns true if the user successfully authenticated, false otherwise. + /// Returns true if the user successfully authenticated. + /// + /// If the user fails the authentication challenge without any side effects, + /// returns false. For other all other failures cases, throws a + /// [LocalAuthException] with details about the reason it did not succeed. /// /// [localizedReason] is the message to show to user while prompting them /// for authentication. This is typically along the lines of: 'Authenticate @@ -31,11 +34,6 @@ class LocalAuthentication { /// customize messages in the dialogs. /// /// Provide [options] for configuring further authentication related options. - /// - /// Throws a [PlatformException] if there were technical problems with local - /// authentication (e.g. lack of relevant hardware). This might throw - /// [PlatformException] with error code [otherOperatingSystem] on the iOS - /// simulator. Future authenticate({ required String localizedReason, Iterable authMessages = const [ @@ -53,10 +51,11 @@ class LocalAuthentication { } /// Cancels any in-progress authentication, returning true if auth was - /// cancelled successfully. + /// canceled successfully. + /// + /// This API may not be supported by all platforms. /// - /// This API is not supported by all platforms. - /// Returns false if there was some error, no authentication in progress, + /// Returns false if there was some error, no authentication is in progress, /// or the current platform lacks support. Future stopAuthentication() async { return LocalAuthPlatform.instance.stopAuthentication(); diff --git a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart index 2650d78713a..d5a4c317143 100644 --- a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart +++ b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart @@ -38,6 +38,8 @@ class IOSAuthMessages extends AuthMessages { /// The localized title for the fallback button in the dialog presented to /// the user during authentication. + /// + /// Set this to an empty string to hide the fallback button. final String? localizedFallbackTitle; @override diff --git a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart index c4d6860cf5c..39fb82edc0f 100644 --- a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart +++ b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart @@ -32,6 +32,8 @@ class MacOSAuthMessages extends AuthMessages { /// The localized title for the fallback button in the dialog presented to /// the user during authentication. + /// + /// Set this to an empty string to hide the fallback button. final String? localizedFallbackTitle; @override diff --git a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart index 5aa5ba9d374..b3d8e96d68f 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart @@ -7,7 +7,7 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'default_method_channel_platform.dart'; import 'types/types.dart'; -export 'package:local_auth_platform_interface/types/types.dart'; +export 'types/types.dart'; /// The interface that implementations of local_auth must implement. /// From 3894214a3c9c67902cd87f2fd1c63e0531e72a5a Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 8 Sep 2025 14:04:36 -0400 Subject: [PATCH 08/19] Remove useErrorDialogs and all related handling, update README --- packages/local_auth/local_auth/README.md | 85 +--- .../local_auth/example/lib/main.dart | 32 +- .../example/lib/readme_excerpts.dart | 52 +-- .../local_auth/local_auth/lib/local_auth.dart | 6 +- .../local_auth/lib/src/local_auth.dart | 28 +- .../localauth/AuthenticationHelper.java | 52 --- .../plugins/localauth/LocalAuthPlugin.java | 5 + .../flutter/plugins/localauth/Messages.java | 242 +--------- .../src/main/res/layout/go_to_setting.xml | 30 -- .../localauth/AuthenticationHelperTest.java | 7 - .../plugins/localauth/LocalAuthTest.java | 8 - .../local_auth_android/example/lib/main.dart | 24 +- .../lib/local_auth_android.dart | 15 - .../lib/src/auth_messages_android.dart | 140 +----- .../lib/src/messages.g.dart | 55 +-- .../local_auth_android/pigeons/messages.dart | 17 - .../test/local_auth_test.dart | 70 +-- .../Tests/FLALocalAuthPluginTests.swift | 433 +++++------------- .../local_auth_darwin/LocalAuthPlugin.swift | 179 +------- .../local_auth_darwin/SystemWrappers.swift | 90 ---- .../local_auth_darwin/messages.g.swift | 54 +-- .../local_auth_darwin/example/lib/main.dart | 24 +- .../lib/local_auth_darwin.dart | 18 +- .../local_auth_darwin/lib/src/messages.g.dart | 39 +- .../lib/types/auth_messages_ios.dart | 68 +-- .../lib/types/auth_messages_macos.dart | 41 +- .../local_auth_darwin/pigeons/messages.dart | 20 +- .../test/local_auth_darwin_test.dart | 61 +-- .../lib/types/auth_options.dart | 11 +- .../default_method_channel_platform_test.dart | 10 +- 30 files changed, 329 insertions(+), 1587 deletions(-) delete mode 100644 packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml diff --git a/packages/local_auth/local_auth/README.md b/packages/local_auth/local_auth/README.md index fbd86545b9a..b59cc6882b1 100644 --- a/packages/local_auth/local_auth/README.md +++ b/packages/local_auth/local_auth/README.md @@ -66,72 +66,34 @@ if (availableBiometrics.contains(BiometricType.strong) || ### Options -The `authenticate()` method uses biometric authentication when possible, but -also allows fallback to pin, pattern, or passcode. +#### Requiring Biometrics - -```dart -try { - final bool didAuthenticate = await auth.authenticate( - localizedReason: 'Please authenticate to show account balance', - ); - // ··· -} on PlatformException { - // ... -} -``` - -To require biometric authentication, pass `AuthenticationOptions` with -`biometricOnly` set to `true`. +The `authenticate()` method uses biometric authentication when possible, but +by default also allows fallback to pin, pattern, or passcode. To require +biometric authentication, set `biometricOnly` to `true`. ```dart final bool didAuthenticate = await auth.authenticate( localizedReason: 'Please authenticate to show account balance', - options: const AuthenticationOptions(biometricOnly: true), + biometricOnly: true, ); ``` *Note*: `biometricOnly` is not supported on Windows since the Windows implementation's underlying API (Windows Hello) doesn't support selecting the authentication method. -#### Dialogs +#### Background Handling -The plugin provides default dialogs for the following cases: +On mobile platforms, authentication may be canceled by the system if the app +is backgrounded. This might happen if the user receives a phone call before +they get a chance to authenticate, for example. Setting +`persistAcrossBackgrounding` to true will cause the plugin to instead wait until +the app is foregrounded again, retry the authentication, and only return once +that new attempt completes. -1. Passcode/PIN/Pattern Not Set: The user has not yet configured a passcode on - iOS or PIN/pattern on Android. -2. Biometrics Not Enrolled: The user has not enrolled any biometrics on the - device. - -If a user does not have the necessary authentication enrolled when -`authenticate` is called, they will be given the option to enroll at that point, -or cancel authentication. - -If you don't want to use the default dialogs, set the `useErrorDialogs` option -to `false` to have `authenticate` immediately return an error in those cases. - - -```dart -import 'package:local_auth/error_codes.dart' as auth_error; -// ··· - try { - final bool didAuthenticate = await auth.authenticate( - localizedReason: 'Please authenticate to show account balance', - options: const AuthenticationOptions(useErrorDialogs: false), - ); - // ··· - } on PlatformException catch (e) { - if (e.code == auth_error.notAvailable) { - // Add handling of no hardware here. - } else if (e.code == auth_error.notEnrolled) { - // ... - } else { - // ... - } - } -``` +#### Dialog customization -If you want to customize the messages in the dialogs, you can pass +If you want to customize the messages in the system dialogs, you can pass `AuthMessages` for each platform you support. These are platform-specific, so you will need to import the platform-specific implementation packages. For instance, to customize Android and iOS: @@ -164,7 +126,6 @@ specific handling for. For example: ```dart -import 'package:flutter/services.dart'; import 'package:local_auth/local_auth.dart'; // ··· final LocalAuthentication auth = LocalAuthentication(); @@ -172,14 +133,13 @@ import 'package:local_auth/local_auth.dart'; try { final bool didAuthenticate = await auth.authenticate( localizedReason: 'Please authenticate to show account balance', - options: const AuthenticationOptions(useErrorDialogs: false), ); // ··· - } on PlatformException catch (e) { - if (e.code == auth_error.notEnrolled) { + } on LocalAuthException catch (e) { + if (e.code == LocalAuthExceptionCode.noBiometricHardware) { // Add handling of no hardware here. - } else if (e.code == auth_error.lockedOut || - e.code == auth_error.permanentlyLockedOut) { + } else if (e.code == LocalAuthExceptionCode.temporaryLockout || + e.code == LocalAuthExceptionCode.biometricLockout) { // ... } else { // ... @@ -289,12 +249,3 @@ the Android theme directly in `android/app/src/main/AndroidManifest.xml`: ... ``` - -## Sticky Auth - -You can set the `stickyAuth` option on the plugin to true so that plugin does not -return failure if the app is put to background by the system. This might happen -if the user receives a phone call before they get a chance to authenticate. With -`stickyAuth` set to false, this would result in plugin returning failure result -to the Dart app. If set to true, the plugin will retry authenticating when the -app resumes. diff --git a/packages/local_auth/local_auth/example/lib/main.dart b/packages/local_auth/local_auth/example/lib/main.dart index 9f7b5cbaac5..d2867441b07 100644 --- a/packages/local_auth/local_auth/example/lib/main.dart +++ b/packages/local_auth/local_auth/example/lib/main.dart @@ -86,16 +86,26 @@ class _MyAppState extends State { }); authenticated = await auth.authenticate( localizedReason: 'Let OS determine authentication method', - options: const AuthenticationOptions(stickyAuth: true), + persistAcrossBackgrounding: true, ); setState(() { _isAuthenticating = false; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected error - ${e.message}'; }); return; } @@ -118,20 +128,28 @@ class _MyAppState extends State { authenticated = await auth.authenticate( localizedReason: 'Scan your fingerprint (or face or whatever) to authenticate', - options: const AuthenticationOptions( - stickyAuth: true, - biometricOnly: true, - ), + persistAcrossBackgrounding: true, + biometricOnly: true, ); setState(() { _isAuthenticating = false; _authorized = 'Authenticating'; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected Error - ${e.message}'; }); return; } diff --git a/packages/local_auth/local_auth/example/lib/readme_excerpts.dart b/packages/local_auth/local_auth/example/lib/readme_excerpts.dart index f48d21b95bd..6eeb577089e 100644 --- a/packages/local_auth/local_auth/example/lib/readme_excerpts.dart +++ b/packages/local_auth/local_auth/example/lib/readme_excerpts.dart @@ -9,10 +9,6 @@ import 'package:flutter/material.dart'; // #docregion ErrorHandling -import 'package:flutter/services.dart'; -// #docregion NoErrorDialogs -import 'package:local_auth/error_codes.dart' as auth_error; -// #enddocregion NoErrorDialogs // #docregion CanCheck import 'package:local_auth/local_auth.dart'; // #enddocregion CanCheck @@ -79,68 +75,30 @@ class _MyAppState extends State { // #enddocregion Enrolled } - Future authenticate() async { - // #docregion AuthAny - try { - final bool didAuthenticate = await auth.authenticate( - localizedReason: 'Please authenticate to show account balance', - ); - // #enddocregion AuthAny - print(didAuthenticate); - // #docregion AuthAny - } on PlatformException { - // ... - } - // #enddocregion AuthAny - } - Future authenticateWithBiometrics() async { // #docregion AuthBioOnly final bool didAuthenticate = await auth.authenticate( localizedReason: 'Please authenticate to show account balance', - options: const AuthenticationOptions(biometricOnly: true), + biometricOnly: true, ); // #enddocregion AuthBioOnly print(didAuthenticate); } - Future authenticateWithoutDialogs() async { - // #docregion NoErrorDialogs - try { - final bool didAuthenticate = await auth.authenticate( - localizedReason: 'Please authenticate to show account balance', - options: const AuthenticationOptions(useErrorDialogs: false), - ); - // #enddocregion NoErrorDialogs - print(didAuthenticate ? 'Success!' : 'Failure'); - // #docregion NoErrorDialogs - } on PlatformException catch (e) { - if (e.code == auth_error.notAvailable) { - // Add handling of no hardware here. - } else if (e.code == auth_error.notEnrolled) { - // ... - } else { - // ... - } - } - // #enddocregion NoErrorDialogs - } - Future authenticateWithErrorHandling() async { // #docregion ErrorHandling try { final bool didAuthenticate = await auth.authenticate( localizedReason: 'Please authenticate to show account balance', - options: const AuthenticationOptions(useErrorDialogs: false), ); // #enddocregion ErrorHandling print(didAuthenticate ? 'Success!' : 'Failure'); // #docregion ErrorHandling - } on PlatformException catch (e) { - if (e.code == auth_error.notEnrolled) { + } on LocalAuthException catch (e) { + if (e.code == LocalAuthExceptionCode.noBiometricHardware) { // Add handling of no hardware here. - } else if (e.code == auth_error.lockedOut || - e.code == auth_error.permanentlyLockedOut) { + } else if (e.code == LocalAuthExceptionCode.temporaryLockout || + e.code == LocalAuthExceptionCode.biometricLockout) { // ... } else { // ... diff --git a/packages/local_auth/local_auth/lib/local_auth.dart b/packages/local_auth/local_auth/lib/local_auth.dart index 395b2a28254..94fd4f1321a 100644 --- a/packages/local_auth/local_auth/lib/local_auth.dart +++ b/packages/local_auth/local_auth/lib/local_auth.dart @@ -4,8 +4,4 @@ export 'package:local_auth/src/local_auth.dart' show LocalAuthentication; export 'package:local_auth_platform_interface/local_auth_platform_interface.dart' - show - AuthenticationOptions, - BiometricType, - LocalAuthException, - LocalAuthExceptionCode; + show BiometricType, LocalAuthException, LocalAuthExceptionCode; diff --git a/packages/local_auth/local_auth/lib/src/local_auth.dart b/packages/local_auth/local_auth/lib/src/local_auth.dart index 69bb0e85a9c..7f1bfc3ba44 100644 --- a/packages/local_auth/local_auth/lib/src/local_auth.dart +++ b/packages/local_auth/local_auth/lib/src/local_auth.dart @@ -23,17 +23,27 @@ class LocalAuthentication { /// Returns true if the user successfully authenticated. /// /// If the user fails the authentication challenge without any side effects, - /// returns false. For other all other failures cases, throws a + /// returns false. For other other failures cases, throws a /// [LocalAuthException] with details about the reason it did not succeed. /// /// [localizedReason] is the message to show to user while prompting them /// for authentication. This is typically along the lines of: 'Authenticate /// to access MyApp.'. This must not be empty. /// - /// Provide [authMessages] if you want to - /// customize messages in the dialogs. + /// Provide [authMessages] if you want to customize messages in the dialogs. /// - /// Provide [options] for configuring further authentication related options. + /// Set [biometricOnly] to true to prevent authentications from using + /// non-biometric local authentication such as pin, passcode, or pattern. + /// + /// [sensitiveTransaction], which defaults to true, controls whether + /// platform-specific precautions are enabled, such as showing a confirmation + /// dialog after face unlock is recognized to make sure the user meant to + /// unlock their device. + /// + /// On mobile platforms, authentication may be stopped by the system when the + /// is backgrounded during an authentication. Set [persistAcrossBackgrounding] + /// to true to have the plugin automatically retry the authentication on + /// foregrounding instead of failing with an error on backgrounding. Future authenticate({ required String localizedReason, Iterable authMessages = const [ @@ -41,12 +51,18 @@ class LocalAuthentication { AndroidAuthMessages(), WindowsAuthMessages(), ], - AuthenticationOptions options = const AuthenticationOptions(), + bool biometricOnly = false, + bool sensitiveTransaction = true, + bool persistAcrossBackgrounding = false, }) { return LocalAuthPlatform.instance.authenticate( localizedReason: localizedReason, authMessages: authMessages, - options: options, + options: AuthenticationOptions( + stickyAuth: persistAcrossBackgrounding, + biometricOnly: biometricOnly, + sensitiveTransaction: sensitiveTransaction, + ), ); } diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java index a1c926492ba..810fd557761 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java @@ -5,19 +5,10 @@ import android.annotation.SuppressLint; import android.app.Activity; -import android.app.AlertDialog; import android.app.Application; -import android.content.Context; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.provider.Settings; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; @@ -46,7 +37,6 @@ interface AuthCompletionHandler { private final Lifecycle lifecycle; private final FragmentActivity activity; private final AuthCompletionHandler completionHandler; - private final boolean useErrorDialogs; private final Messages.AuthStrings strings; private final BiometricPrompt.PromptInfo promptInfo; private final boolean isAuthSticky; @@ -66,7 +56,6 @@ interface AuthCompletionHandler { this.completionHandler = completionHandler; this.strings = strings; this.isAuthSticky = options.getSticky(); - this.useErrorDialogs = options.getUseErrorDialgs(); this.uiThreadExecutor = new UiThreadExecutor(); BiometricPrompt.PromptInfo.Builder promptBuilder = @@ -130,20 +119,9 @@ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString code = AuthResultCode.NEGATIVE_BUTTON; break; case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL: - if (useErrorDialogs) { - showGoToSettingsDialog( - strings.getDeviceCredentialsRequiredTitle(), - strings.getDeviceCredentialsSetupDescription()); - return; - } code = AuthResultCode.NO_CREDENTIALS; break; case BiometricPrompt.ERROR_NO_BIOMETRICS: - if (useErrorDialogs) { - showGoToSettingsDialog( - strings.getBiometricRequiredTitle(), strings.getGoToSettingsDescription()); - return; - } code = AuthResultCode.NOT_ENROLLED; break; case BiometricPrompt.ERROR_HW_UNAVAILABLE: @@ -228,36 +206,6 @@ public void onResume(@NonNull LifecycleOwner owner) { onActivityResumed(null); } - // Suppress inflateParams lint because dialogs do not need to attach to a parent view. - @SuppressLint("InflateParams") - private void showGoToSettingsDialog(String title, String descriptionText) { - View view = LayoutInflater.from(activity).inflate(R.layout.go_to_setting, null, false); - TextView message = view.findViewById(R.id.fingerprint_required); - TextView description = view.findViewById(R.id.go_to_setting_description); - message.setText(title); - description.setText(descriptionText); - Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom); - OnClickListener goToSettingHandler = - (dialog, which) -> { - completionHandler.complete( - new AuthResult.Builder().setCode(AuthResultCode.LAUNCHED_SETTINGS).build()); - stop(); - activity.startActivity(new Intent(Settings.ACTION_SECURITY_SETTINGS)); - }; - OnClickListener cancelHandler = - (dialog, which) -> { - completionHandler.complete( - new AuthResult.Builder().setCode(AuthResultCode.NEGATIVE_BUTTON).build()); - stop(); - }; - new AlertDialog.Builder(context) - .setView(view) - .setPositiveButton(strings.getGoToSettingsButton(), goToSettingHandler) - .setNegativeButton(strings.getCancelButton(), cancelHandler) - .setCancelable(false) - .show(); - } - // Unused methods for activity lifecycle. @Override diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index 094b0b9db86..be100b87db8 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -53,14 +53,17 @@ public class LocalAuthPlugin implements FlutterPlugin, ActivityAware, LocalAuthA */ public LocalAuthPlugin() {} + @Override public @NonNull Boolean isDeviceSupported() { return isDeviceSecure() || canAuthenticateWithBiometrics(); } + @Override public @NonNull Boolean deviceCanSupportBiometrics() { return hasBiometricHardware(); } + @Override public @NonNull List getEnrolledBiometrics() { ArrayList biometrics = new ArrayList<>(); if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) @@ -74,6 +77,7 @@ public LocalAuthPlugin() {} return biometrics; } + @Override public @NonNull Boolean stopAuthentication() { try { if (authHelper != null && authInProgress.get()) { @@ -87,6 +91,7 @@ public LocalAuthPlugin() {} } } + @Override public void authenticate( @NonNull AuthOptions options, @NonNull AuthStrings strings, diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java index 9fdf10f5e6e..4f52140ed14 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java @@ -69,44 +69,42 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { public enum AuthResultCode { /** The user authenticated successfully. */ SUCCESS(0), - /** The user launched the settings dialog. */ - LAUNCHED_SETTINGS(1), /** The user pressed the negative button, which corresponds to [AuthStrings.cancelButton]. */ - NEGATIVE_BUTTON(2), + NEGATIVE_BUTTON(1), /** * The user canceled authentication without pressing the negative button. * *

This may be triggered by a swipe or a back button, for example. */ - USER_CANCELED(3), + USER_CANCELED(2), /** Authentication was caneceled by the system. */ - SYSTEM_CANCELED(4), + SYSTEM_CANCELED(3), /** Authentication timed out. */ - TIMEOUT(5), + TIMEOUT(4), /** An authentication was already in progress. */ - ALREADY_IN_PROGRESS(6), + ALREADY_IN_PROGRESS(5), /** There is no foreground activity. */ - NO_ACTIVITY(7), + NO_ACTIVITY(6), /** The foreground activity is not a FragmentActivity. */ - NOT_FRAGMENT_ACTIVITY(8), + NOT_FRAGMENT_ACTIVITY(7), /** The device does not have any credentials available. */ - NO_CREDENTIALS(9), + NO_CREDENTIALS(8), /** No biometric hardware is present. */ - NO_HARDWARE(10), + NO_HARDWARE(9), /** The biometric is temporarily unavailable. */ - HARDWARE_UNAVAILABLE(11), + HARDWARE_UNAVAILABLE(10), /** No biometrics are enrolled. */ - NOT_ENROLLED(12), + NOT_ENROLLED(11), /** The user is locked out temporarily due to too many failed attempts. */ - LOCKED_OUT_TEMPORARILY(13), + LOCKED_OUT_TEMPORARILY(12), /** The user is locked out until they log in another way due to too many failed attempts. */ - LOCKED_OUT_PERMANENTLY(14), + LOCKED_OUT_PERMANENTLY(13), /** The device does not have enough storage to complete authentication. */ - NO_SPACE(15), + NO_SPACE(14), /** The hardware is unavailable until a security update is performed. */ - SECURITY_UPDATE_REQUIRED(16), + SECURITY_UPDATE_REQUIRED(15), /** Some unrecognized error case was encountered */ - UNKNOWN_ERROR(17); + UNKNOWN_ERROR(16); final int index; @@ -161,32 +159,6 @@ public void setBiometricHint(@NonNull String setterArg) { this.biometricHint = setterArg; } - private @NonNull String biometricNotRecognized; - - public @NonNull String getBiometricNotRecognized() { - return biometricNotRecognized; - } - - public void setBiometricNotRecognized(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"biometricNotRecognized\" is null."); - } - this.biometricNotRecognized = setterArg; - } - - private @NonNull String biometricRequiredTitle; - - public @NonNull String getBiometricRequiredTitle() { - return biometricRequiredTitle; - } - - public void setBiometricRequiredTitle(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"biometricRequiredTitle\" is null."); - } - this.biometricRequiredTitle = setterArg; - } - private @NonNull String cancelButton; public @NonNull String getCancelButton() { @@ -200,60 +172,6 @@ public void setCancelButton(@NonNull String setterArg) { this.cancelButton = setterArg; } - private @NonNull String deviceCredentialsRequiredTitle; - - public @NonNull String getDeviceCredentialsRequiredTitle() { - return deviceCredentialsRequiredTitle; - } - - public void setDeviceCredentialsRequiredTitle(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException( - "Nonnull field \"deviceCredentialsRequiredTitle\" is null."); - } - this.deviceCredentialsRequiredTitle = setterArg; - } - - private @NonNull String deviceCredentialsSetupDescription; - - public @NonNull String getDeviceCredentialsSetupDescription() { - return deviceCredentialsSetupDescription; - } - - public void setDeviceCredentialsSetupDescription(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException( - "Nonnull field \"deviceCredentialsSetupDescription\" is null."); - } - this.deviceCredentialsSetupDescription = setterArg; - } - - private @NonNull String goToSettingsButton; - - public @NonNull String getGoToSettingsButton() { - return goToSettingsButton; - } - - public void setGoToSettingsButton(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"goToSettingsButton\" is null."); - } - this.goToSettingsButton = setterArg; - } - - private @NonNull String goToSettingsDescription; - - public @NonNull String getGoToSettingsDescription() { - return goToSettingsDescription; - } - - public void setGoToSettingsDescription(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"goToSettingsDescription\" is null."); - } - this.goToSettingsDescription = setterArg; - } - private @NonNull String signInTitle; public @NonNull String getSignInTitle() { @@ -281,29 +199,13 @@ public boolean equals(Object o) { AuthStrings that = (AuthStrings) o; return reason.equals(that.reason) && biometricHint.equals(that.biometricHint) - && biometricNotRecognized.equals(that.biometricNotRecognized) - && biometricRequiredTitle.equals(that.biometricRequiredTitle) && cancelButton.equals(that.cancelButton) - && deviceCredentialsRequiredTitle.equals(that.deviceCredentialsRequiredTitle) - && deviceCredentialsSetupDescription.equals(that.deviceCredentialsSetupDescription) - && goToSettingsButton.equals(that.goToSettingsButton) - && goToSettingsDescription.equals(that.goToSettingsDescription) && signInTitle.equals(that.signInTitle); } @Override public int hashCode() { - return Objects.hash( - reason, - biometricHint, - biometricNotRecognized, - biometricRequiredTitle, - cancelButton, - deviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription, - goToSettingsButton, - goToSettingsDescription, - signInTitle); + return Objects.hash(reason, biometricHint, cancelButton, signInTitle); } public static final class Builder { @@ -324,22 +226,6 @@ public static final class Builder { return this; } - private @Nullable String biometricNotRecognized; - - @CanIgnoreReturnValue - public @NonNull Builder setBiometricNotRecognized(@NonNull String setterArg) { - this.biometricNotRecognized = setterArg; - return this; - } - - private @Nullable String biometricRequiredTitle; - - @CanIgnoreReturnValue - public @NonNull Builder setBiometricRequiredTitle(@NonNull String setterArg) { - this.biometricRequiredTitle = setterArg; - return this; - } - private @Nullable String cancelButton; @CanIgnoreReturnValue @@ -348,38 +234,6 @@ public static final class Builder { return this; } - private @Nullable String deviceCredentialsRequiredTitle; - - @CanIgnoreReturnValue - public @NonNull Builder setDeviceCredentialsRequiredTitle(@NonNull String setterArg) { - this.deviceCredentialsRequiredTitle = setterArg; - return this; - } - - private @Nullable String deviceCredentialsSetupDescription; - - @CanIgnoreReturnValue - public @NonNull Builder setDeviceCredentialsSetupDescription(@NonNull String setterArg) { - this.deviceCredentialsSetupDescription = setterArg; - return this; - } - - private @Nullable String goToSettingsButton; - - @CanIgnoreReturnValue - public @NonNull Builder setGoToSettingsButton(@NonNull String setterArg) { - this.goToSettingsButton = setterArg; - return this; - } - - private @Nullable String goToSettingsDescription; - - @CanIgnoreReturnValue - public @NonNull Builder setGoToSettingsDescription(@NonNull String setterArg) { - this.goToSettingsDescription = setterArg; - return this; - } - private @Nullable String signInTitle; @CanIgnoreReturnValue @@ -392,13 +246,7 @@ public static final class Builder { AuthStrings pigeonReturn = new AuthStrings(); pigeonReturn.setReason(reason); pigeonReturn.setBiometricHint(biometricHint); - pigeonReturn.setBiometricNotRecognized(biometricNotRecognized); - pigeonReturn.setBiometricRequiredTitle(biometricRequiredTitle); pigeonReturn.setCancelButton(cancelButton); - pigeonReturn.setDeviceCredentialsRequiredTitle(deviceCredentialsRequiredTitle); - pigeonReturn.setDeviceCredentialsSetupDescription(deviceCredentialsSetupDescription); - pigeonReturn.setGoToSettingsButton(goToSettingsButton); - pigeonReturn.setGoToSettingsDescription(goToSettingsDescription); pigeonReturn.setSignInTitle(signInTitle); return pigeonReturn; } @@ -406,16 +254,10 @@ public static final class Builder { @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(10); + ArrayList toListResult = new ArrayList<>(4); toListResult.add(reason); toListResult.add(biometricHint); - toListResult.add(biometricNotRecognized); - toListResult.add(biometricRequiredTitle); toListResult.add(cancelButton); - toListResult.add(deviceCredentialsRequiredTitle); - toListResult.add(deviceCredentialsSetupDescription); - toListResult.add(goToSettingsButton); - toListResult.add(goToSettingsDescription); toListResult.add(signInTitle); return toListResult; } @@ -426,21 +268,9 @@ ArrayList toList() { pigeonResult.setReason((String) reason); Object biometricHint = pigeonVar_list.get(1); pigeonResult.setBiometricHint((String) biometricHint); - Object biometricNotRecognized = pigeonVar_list.get(2); - pigeonResult.setBiometricNotRecognized((String) biometricNotRecognized); - Object biometricRequiredTitle = pigeonVar_list.get(3); - pigeonResult.setBiometricRequiredTitle((String) biometricRequiredTitle); - Object cancelButton = pigeonVar_list.get(4); + Object cancelButton = pigeonVar_list.get(2); pigeonResult.setCancelButton((String) cancelButton); - Object deviceCredentialsRequiredTitle = pigeonVar_list.get(5); - pigeonResult.setDeviceCredentialsRequiredTitle((String) deviceCredentialsRequiredTitle); - Object deviceCredentialsSetupDescription = pigeonVar_list.get(6); - pigeonResult.setDeviceCredentialsSetupDescription((String) deviceCredentialsSetupDescription); - Object goToSettingsButton = pigeonVar_list.get(7); - pigeonResult.setGoToSettingsButton((String) goToSettingsButton); - Object goToSettingsDescription = pigeonVar_list.get(8); - pigeonResult.setGoToSettingsDescription((String) goToSettingsDescription); - Object signInTitle = pigeonVar_list.get(9); + Object signInTitle = pigeonVar_list.get(3); pigeonResult.setSignInTitle((String) signInTitle); return pigeonResult; } @@ -582,19 +412,6 @@ public void setSticky(@NonNull Boolean setterArg) { this.sticky = setterArg; } - private @NonNull Boolean useErrorDialgs; - - public @NonNull Boolean getUseErrorDialgs() { - return useErrorDialgs; - } - - public void setUseErrorDialgs(@NonNull Boolean setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"useErrorDialgs\" is null."); - } - this.useErrorDialgs = setterArg; - } - /** Constructor is non-public to enforce null safety; use Builder. */ AuthOptions() {} @@ -609,13 +426,12 @@ public boolean equals(Object o) { AuthOptions that = (AuthOptions) o; return biometricOnly.equals(that.biometricOnly) && sensitiveTransaction.equals(that.sensitiveTransaction) - && sticky.equals(that.sticky) - && useErrorDialgs.equals(that.useErrorDialgs); + && sticky.equals(that.sticky); } @Override public int hashCode() { - return Objects.hash(biometricOnly, sensitiveTransaction, sticky, useErrorDialgs); + return Objects.hash(biometricOnly, sensitiveTransaction, sticky); } public static final class Builder { @@ -644,31 +460,21 @@ public static final class Builder { return this; } - private @Nullable Boolean useErrorDialgs; - - @CanIgnoreReturnValue - public @NonNull Builder setUseErrorDialgs(@NonNull Boolean setterArg) { - this.useErrorDialgs = setterArg; - return this; - } - public @NonNull AuthOptions build() { AuthOptions pigeonReturn = new AuthOptions(); pigeonReturn.setBiometricOnly(biometricOnly); pigeonReturn.setSensitiveTransaction(sensitiveTransaction); pigeonReturn.setSticky(sticky); - pigeonReturn.setUseErrorDialgs(useErrorDialgs); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(4); + ArrayList toListResult = new ArrayList<>(3); toListResult.add(biometricOnly); toListResult.add(sensitiveTransaction); toListResult.add(sticky); - toListResult.add(useErrorDialgs); return toListResult; } @@ -680,8 +486,6 @@ ArrayList toList() { pigeonResult.setSensitiveTransaction((Boolean) sensitiveTransaction); Object sticky = pigeonVar_list.get(2); pigeonResult.setSticky((Boolean) sticky); - Object useErrorDialgs = pigeonVar_list.get(3); - pigeonResult.setUseErrorDialgs((Boolean) useErrorDialgs); return pigeonResult; } } diff --git a/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml b/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml deleted file mode 100644 index 902635ef543..00000000000 --- a/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java index 9c5d4fcf3ef..dcb9ce4952a 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java @@ -30,13 +30,7 @@ public class AuthenticationHelperTest { new AuthStrings.Builder() .setReason("a reason") .setBiometricHint("a hint") - .setBiometricNotRecognized("biometric not recognized") - .setBiometricRequiredTitle("biometric required") .setCancelButton("cancel") - .setDeviceCredentialsRequiredTitle("credentials required") - .setDeviceCredentialsSetupDescription("credentials setup description") - .setGoToSettingsButton("go") - .setGoToSettingsDescription("go to settings description") .setSignInTitle("sign in") .build(); @@ -45,7 +39,6 @@ public class AuthenticationHelperTest { .setBiometricOnly(false) .setSensitiveTransaction(false) .setSticky(false) - .setUseErrorDialgs(false) .build(); @Test diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java index db78bc07482..de199713a1c 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -48,13 +48,7 @@ public class LocalAuthTest { new AuthStrings.Builder() .setReason("a reason") .setBiometricHint("a hint") - .setBiometricNotRecognized("biometric not recognized") - .setBiometricRequiredTitle("biometric required") .setCancelButton("cancel") - .setDeviceCredentialsRequiredTitle("credentials required") - .setDeviceCredentialsSetupDescription("credentials setup description") - .setGoToSettingsButton("go") - .setGoToSettingsDescription("go to settings description") .setSignInTitle("sign in") .build(); @@ -63,7 +57,6 @@ public class LocalAuthTest { .setBiometricOnly(false) .setSensitiveTransaction(false) .setSticky(false) - .setUseErrorDialgs(false) .build(); @Test @@ -144,7 +137,6 @@ public void authenticate_properlyConfiguresBiometricOnlyAuthenticationRequest() .setBiometricOnly(true) .setSensitiveTransaction(false) .setSticky(false) - .setUseErrorDialgs(false) .build(); plugin.authenticate(options, dummyStrings, mockResult); assertFalse(allowCredentialsCaptor.getValue()); diff --git a/packages/local_auth/local_auth_android/example/lib/main.dart b/packages/local_auth/local_auth_android/example/lib/main.dart index 16790279b75..a355a6c92e5 100644 --- a/packages/local_auth/local_auth_android/example/lib/main.dart +++ b/packages/local_auth/local_auth_android/example/lib/main.dart @@ -94,11 +94,21 @@ class _MyAppState extends State { setState(() { _isAuthenticating = false; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected Error - ${e.message}'; }); return; } @@ -131,11 +141,21 @@ class _MyAppState extends State { _isAuthenticating = false; _authorized = 'Authenticating'; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected Error - ${e.message}'; }); return; } diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart index c00975d217b..90b22dec536 100644 --- a/packages/local_auth/local_auth_android/lib/local_auth_android.dart +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart @@ -38,14 +38,12 @@ class LocalAuthAndroid extends LocalAuthPlatform { biometricOnly: options.biometricOnly, sensitiveTransaction: options.sensitiveTransaction, sticky: options.stickyAuth, - useErrorDialgs: options.useErrorDialogs, ), _pigeonStringsFromAuthMessages(localizedReason, authMessages), ); switch (result.code) { case AuthResultCode.success: return true; - case AuthResultCode.launchedSettings: case AuthResultCode.negativeButton: case AuthResultCode.userCanceled: // Variants of user cancelation format are not currently distinguished, @@ -152,20 +150,7 @@ class LocalAuthAndroid extends LocalAuthPlatform { return AuthStrings( reason: localizedReason, biometricHint: messages?.biometricHint ?? androidBiometricHint, - biometricNotRecognized: - messages?.biometricNotRecognized ?? androidBiometricNotRecognized, - biometricRequiredTitle: - messages?.biometricRequiredTitle ?? androidBiometricRequiredTitle, cancelButton: messages?.cancelButton ?? androidCancelButton, - deviceCredentialsRequiredTitle: - messages?.deviceCredentialsRequiredTitle ?? - androidDeviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription: - messages?.deviceCredentialsSetupDescription ?? - androidDeviceCredentialsSetupDescription, - goToSettingsButton: messages?.goToSettingsButton ?? goToSettings, - goToSettingsDescription: - messages?.goToSettingsDescription ?? androidGoToSettingsDescription, signInTitle: messages?.signInTitle ?? androidSignInTitle, ); } diff --git a/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart b/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart index e9918e6c573..fa623d82661 100644 --- a/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart +++ b/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart @@ -14,14 +14,7 @@ class AndroidAuthMessages extends AuthMessages { /// Constructs a new instance. const AndroidAuthMessages({ this.biometricHint, - this.biometricNotRecognized, - this.biometricRequiredTitle, - this.biometricSuccess, this.cancelButton, - this.deviceCredentialsRequiredTitle, - this.deviceCredentialsSetupDescription, - this.goToSettingsButton, - this.goToSettingsDescription, this.signInTitle, }); @@ -29,42 +22,11 @@ class AndroidAuthMessages extends AuthMessages { /// Maximum 60 characters. final String? biometricHint; - /// Message to let the user know that authentication was failed. - /// Maximum 60 characters. - final String? biometricNotRecognized; - - /// Message shown as a title in a dialog which indicates the user - /// has not set up biometric authentication on their device. - /// Maximum 60 characters. - final String? biometricRequiredTitle; - - /// Message to let the user know that authentication was successful. - /// Maximum 60 characters - final String? biometricSuccess; - /// Message shown on a button that the user can click to leave the /// current dialog. /// Maximum 30 characters. final String? cancelButton; - /// Message shown as a title in a dialog which indicates the user - /// has not set up credentials authentication on their device. - /// Maximum 60 characters. - final String? deviceCredentialsRequiredTitle; - - /// Message advising the user to go to the settings and configure - /// device credentials on their device. - final String? deviceCredentialsSetupDescription; - - /// Message shown on a button that the user can click to go to settings pages - /// from the current dialog. - /// Maximum 30 characters. - final String? goToSettingsButton; - - /// Message advising the user to go to the settings and configure - /// biometric on their device. - final String? goToSettingsDescription; - /// Message shown as a title in a dialog which indicates the user /// that they need to scan biometric to continue. /// Maximum 60 characters. @@ -74,21 +36,7 @@ class AndroidAuthMessages extends AuthMessages { Map get args { return { 'biometricHint': biometricHint ?? androidBiometricHint, - 'biometricNotRecognized': - biometricNotRecognized ?? androidBiometricNotRecognized, - 'biometricSuccess': biometricSuccess ?? androidBiometricSuccess, - 'biometricRequired': - biometricRequiredTitle ?? androidBiometricRequiredTitle, 'cancelButton': cancelButton ?? androidCancelButton, - 'deviceCredentialsRequired': - deviceCredentialsRequiredTitle ?? - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - deviceCredentialsSetupDescription ?? - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettingsButton ?? goToSettings, - 'goToSettingDescription': - goToSettingsDescription ?? androidGoToSettingsDescription, 'signInTitle': signInTitle ?? androidSignInTitle, }; } @@ -99,46 +47,17 @@ class AndroidAuthMessages extends AuthMessages { other is AndroidAuthMessages && runtimeType == other.runtimeType && biometricHint == other.biometricHint && - biometricNotRecognized == other.biometricNotRecognized && - biometricRequiredTitle == other.biometricRequiredTitle && - biometricSuccess == other.biometricSuccess && cancelButton == other.cancelButton && - deviceCredentialsRequiredTitle == - other.deviceCredentialsRequiredTitle && - deviceCredentialsSetupDescription == - other.deviceCredentialsSetupDescription && - goToSettingsButton == other.goToSettingsButton && - goToSettingsDescription == other.goToSettingsDescription && signInTitle == other.signInTitle; @override - int get hashCode => Object.hash( - super.hashCode, - biometricHint, - biometricNotRecognized, - biometricRequiredTitle, - biometricSuccess, - cancelButton, - deviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription, - goToSettingsButton, - goToSettingsDescription, - signInTitle, - ); + int get hashCode => + Object.hash(super.hashCode, biometricHint, cancelButton, signInTitle); } // Default strings for AndroidAuthMessages. Currently supports English. // Intl.message must be string literals. -/// Message shown on a button that the user can click to go to settings pages -/// from the current dialog. -String get goToSettings => Intl.message( - 'Go to settings', - desc: - 'Message shown on a button that the user can click to go to ' - 'settings pages from the current dialog. Maximum 30 characters.', -); - /// Hint message advising the user how to authenticate with biometrics. String get androidBiometricHint => Intl.message( 'Verify identity', @@ -147,22 +66,6 @@ String get androidBiometricHint => Intl.message( 'Maximum 60 characters.', ); -/// Message to let the user know that authentication was failed. -String get androidBiometricNotRecognized => Intl.message( - 'Not recognized. Try again.', - desc: - 'Message to let the user know that authentication was failed. ' - 'Maximum 60 characters.', -); - -/// Message to let the user know that authentication was successful. It -String get androidBiometricSuccess => Intl.message( - 'Success', - desc: - 'Message to let the user know that authentication was successful. ' - 'Maximum 60 characters.', -); - /// Message shown on a button that the user can click to leave the /// current dialog. String get androidCancelButton => Intl.message( @@ -180,42 +83,3 @@ String get androidSignInTitle => Intl.message( 'Message shown as a title in a dialog which indicates the user ' 'that they need to scan biometric to continue. Maximum 60 characters.', ); - -/// Message shown as a title in a dialog which indicates the user -/// has not set up biometric authentication on their device. -String get androidBiometricRequiredTitle => Intl.message( - 'Biometric required', - desc: - 'Message shown as a title in a dialog which indicates the user ' - 'has not set up biometric authentication on their device. ' - 'Maximum 60 characters.', -); - -/// Message shown as a title in a dialog which indicates the user -/// has not set up credentials authentication on their device. -String get androidDeviceCredentialsRequiredTitle => Intl.message( - 'Device credentials required', - desc: - 'Message shown as a title in a dialog which indicates the user ' - 'has not set up credentials authentication on their device. ' - 'Maximum 60 characters.', -); - -/// Message advising the user to go to the settings and configure -/// device credentials on their device. -String get androidDeviceCredentialsSetupDescription => Intl.message( - 'Device credentials required', - desc: - 'Message advising the user to go to the settings and configure ' - 'device credentials on their device.', -); - -/// Message advising the user to go to the settings and configure -/// biometric on their device. -String get androidGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Go to ' - "'Settings > Security' to add biometric authentication.", - desc: - 'Message advising the user to go to the settings and configure ' - 'biometric on their device.', -); diff --git a/packages/local_auth/local_auth_android/lib/src/messages.g.dart b/packages/local_auth/local_auth_android/lib/src/messages.g.dart index 98ec2c991f1..bbf8c67eb61 100644 --- a/packages/local_auth/local_auth_android/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_android/lib/src/messages.g.dart @@ -23,9 +23,6 @@ enum AuthResultCode { /// The user authenticated successfully. success, - /// The user launched the settings dialog. - launchedSettings, - /// The user pressed the negative button, which corresponds to /// [AuthStrings.cancelButton]. negativeButton, @@ -89,13 +86,7 @@ class AuthStrings { AuthStrings({ required this.reason, required this.biometricHint, - required this.biometricNotRecognized, - required this.biometricRequiredTitle, required this.cancelButton, - required this.deviceCredentialsRequiredTitle, - required this.deviceCredentialsSetupDescription, - required this.goToSettingsButton, - required this.goToSettingsDescription, required this.signInTitle, }); @@ -103,35 +94,12 @@ class AuthStrings { String biometricHint; - String biometricNotRecognized; - - String biometricRequiredTitle; - String cancelButton; - String deviceCredentialsRequiredTitle; - - String deviceCredentialsSetupDescription; - - String goToSettingsButton; - - String goToSettingsDescription; - String signInTitle; Object encode() { - return [ - reason, - biometricHint, - biometricNotRecognized, - biometricRequiredTitle, - cancelButton, - deviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription, - goToSettingsButton, - goToSettingsDescription, - signInTitle, - ]; + return [reason, biometricHint, cancelButton, signInTitle]; } static AuthStrings decode(Object result) { @@ -139,14 +107,8 @@ class AuthStrings { return AuthStrings( reason: result[0]! as String, biometricHint: result[1]! as String, - biometricNotRecognized: result[2]! as String, - biometricRequiredTitle: result[3]! as String, - cancelButton: result[4]! as String, - deviceCredentialsRequiredTitle: result[5]! as String, - deviceCredentialsSetupDescription: result[6]! as String, - goToSettingsButton: result[7]! as String, - goToSettingsDescription: result[8]! as String, - signInTitle: result[9]! as String, + cancelButton: result[2]! as String, + signInTitle: result[3]! as String, ); } } @@ -179,7 +141,6 @@ class AuthOptions { required this.biometricOnly, required this.sensitiveTransaction, required this.sticky, - required this.useErrorDialgs, }); bool biometricOnly; @@ -188,15 +149,8 @@ class AuthOptions { bool sticky; - bool useErrorDialgs; - Object encode() { - return [ - biometricOnly, - sensitiveTransaction, - sticky, - useErrorDialgs, - ]; + return [biometricOnly, sensitiveTransaction, sticky]; } static AuthOptions decode(Object result) { @@ -205,7 +159,6 @@ class AuthOptions { biometricOnly: result[0]! as bool, sensitiveTransaction: result[1]! as bool, sticky: result[2]! as bool, - useErrorDialgs: result[3]! as bool, ); } } diff --git a/packages/local_auth/local_auth_android/pigeons/messages.dart b/packages/local_auth/local_auth_android/pigeons/messages.dart index 87ef8d6e392..2916594b130 100644 --- a/packages/local_auth/local_auth_android/pigeons/messages.dart +++ b/packages/local_auth/local_auth_android/pigeons/messages.dart @@ -20,25 +20,13 @@ class AuthStrings { const AuthStrings({ required this.reason, required this.biometricHint, - required this.biometricNotRecognized, - required this.biometricRequiredTitle, required this.cancelButton, - required this.deviceCredentialsRequiredTitle, - required this.deviceCredentialsSetupDescription, - required this.goToSettingsButton, - required this.goToSettingsDescription, required this.signInTitle, }); final String reason; final String biometricHint; - final String biometricNotRecognized; - final String biometricRequiredTitle; final String cancelButton; - final String deviceCredentialsRequiredTitle; - final String deviceCredentialsSetupDescription; - final String goToSettingsButton; - final String goToSettingsDescription; final String signInTitle; } @@ -47,9 +35,6 @@ enum AuthResultCode { /// The user authenticated successfully. success, - /// The user launched the settings dialog. - launchedSettings, - /// The user pressed the negative button, which corresponds to /// [AuthStrings.cancelButton]. negativeButton, @@ -119,12 +104,10 @@ class AuthOptions { required this.biometricOnly, required this.sensitiveTransaction, required this.sticky, - required this.useErrorDialgs, }); final bool biometricOnly; final bool sensitiveTransaction; final bool sticky; - final bool useErrorDialgs; } /// Pigeon equivalent of the subset of BiometricType used by Android. diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart index 61b9b4da768..2b83bfedde4 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -108,19 +108,7 @@ void main() { // These should all be the default values from // auth_messages_android.dart expect(strings.biometricHint, androidBiometricHint); - expect(strings.biometricNotRecognized, androidBiometricNotRecognized); - expect(strings.biometricRequiredTitle, androidBiometricRequiredTitle); expect(strings.cancelButton, androidCancelButton); - expect( - strings.deviceCredentialsRequiredTitle, - androidDeviceCredentialsRequiredTitle, - ); - expect( - strings.deviceCredentialsSetupDescription, - androidDeviceCredentialsSetupDescription, - ); - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, androidGoToSettingsDescription); expect(strings.signInTitle, androidSignInTitle); }); @@ -145,22 +133,7 @@ void main() { // These should all be the default values from // auth_messages_android.dart expect(strings.biometricHint, androidBiometricHint); - expect(strings.biometricNotRecognized, androidBiometricNotRecognized); - expect(strings.biometricRequiredTitle, androidBiometricRequiredTitle); expect(strings.cancelButton, androidCancelButton); - expect( - strings.deviceCredentialsRequiredTitle, - androidDeviceCredentialsRequiredTitle, - ); - expect( - strings.deviceCredentialsSetupDescription, - androidDeviceCredentialsSetupDescription, - ); - expect(strings.goToSettingsButton, goToSettings); - expect( - strings.goToSettingsDescription, - androidGoToSettingsDescription, - ); expect(strings.signInTitle, androidSignInTitle); }, ); @@ -175,26 +148,14 @@ void main() { // - they are different from each other. const String reason = 'A'; const String hint = 'B'; - const String bioNotRecognized = 'C'; - const String bioRequired = 'D'; - const String cancel = 'E'; - const String credentialsRequired = 'F'; - const String credentialsSetup = 'G'; - const String goButton = 'H'; - const String goDescription = 'I'; - const String signInTitle = 'J'; + const String cancel = 'C'; + const String signInTitle = 'D'; await plugin.authenticate( localizedReason: reason, authMessages: [ const AndroidAuthMessages( biometricHint: hint, - biometricNotRecognized: bioNotRecognized, - biometricRequiredTitle: bioRequired, cancelButton: cancel, - deviceCredentialsRequiredTitle: credentialsRequired, - deviceCredentialsSetupDescription: credentialsSetup, - goToSettingsButton: goButton, - goToSettingsDescription: goDescription, signInTitle: signInTitle, ), AnotherPlatformAuthMessages(), @@ -207,13 +168,7 @@ void main() { final AuthStrings strings = result.captured[0] as AuthStrings; expect(strings.reason, reason); expect(strings.biometricHint, hint); - expect(strings.biometricNotRecognized, bioNotRecognized); - expect(strings.biometricRequiredTitle, bioRequired); expect(strings.cancelButton, cancel); - expect(strings.deviceCredentialsRequiredTitle, credentialsRequired); - expect(strings.deviceCredentialsSetupDescription, credentialsSetup); - expect(strings.goToSettingsButton, goButton); - expect(strings.goToSettingsDescription, goDescription); expect(strings.signInTitle, signInTitle); }); @@ -227,16 +182,12 @@ void main() { // - they are different from each other. const String reason = 'A'; const String hint = 'B'; - const String bioNotRecognized = 'C'; - const String bioRequired = 'D'; - const String cancel = 'E'; + const String cancel = 'C'; await plugin.authenticate( localizedReason: reason, authMessages: [ const AndroidAuthMessages( biometricHint: hint, - biometricNotRecognized: bioNotRecognized, - biometricRequiredTitle: bioRequired, cancelButton: cancel, ), ], @@ -249,21 +200,9 @@ void main() { expect(strings.reason, reason); // These should all be the provided values. expect(strings.biometricHint, hint); - expect(strings.biometricNotRecognized, bioNotRecognized); - expect(strings.biometricRequiredTitle, bioRequired); expect(strings.cancelButton, cancel); // These were non set, so should all be the default values from // auth_messages_android.dart - expect( - strings.deviceCredentialsRequiredTitle, - androidDeviceCredentialsRequiredTitle, - ); - expect( - strings.deviceCredentialsSetupDescription, - androidDeviceCredentialsSetupDescription, - ); - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, androidGoToSettingsDescription); expect(strings.signInTitle, androidSignInTitle); }); }); @@ -286,7 +225,6 @@ void main() { expect(options.biometricOnly, false); expect(options.sensitiveTransaction, true); expect(options.sticky, false); - expect(options.useErrorDialgs, true); }); test('passes provided non-default values', () async { @@ -301,7 +239,6 @@ void main() { biometricOnly: true, sensitiveTransaction: false, stickyAuth: true, - useErrorDialogs: false, ), ); @@ -312,7 +249,6 @@ void main() { expect(options.biometricOnly, true); expect(options.sensitiveTransaction, false); expect(options.sticky, true); - expect(options.useErrorDialgs, false); }); }); 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 a7194ed3897..fb8a3e29d67 100644 --- a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift +++ b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift @@ -30,97 +30,6 @@ final class StubAuthContextFactory: AuthContextFactory { } } -final class StubViewProvider: ViewProvider { - #if os(macOS) - var view: NSView? - var window: NSWindow - init() { - self.window = NSWindow() - self.view = NSView() - self.window.contentView = self.view - } - #endif -} - -#if os(macOS) - final class TestAlert: AuthAlert { - var messageText: String = "" - var buttons: [String] = [] - var presentingWindow: NSWindow? - - func addButton(withTitle title: String) -> NSButton { - buttons.append(title) - return NSButton() // The return value is not used by the plugin. - } - - func beginSheetModal( - for sheetWindow: NSWindow, - completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil - ) { - presentingWindow = sheetWindow - handler?(NSApplication.ModalResponse.OK) - } - - func runModal() -> NSApplication.ModalResponse { - return NSApplication.ModalResponse.OK - } - } -#else - final class TestAlertController: AuthAlertController { - var actions: [UIAlertAction] = [] - var presented = false - var presentingViewController: UIViewController? - // The handler to trigger when present is called, to simulate an action selection. - var onPresentActionHandler: ((UIAlertAction) -> Void)? - - func addAction(_ action: UIAlertAction) { - actions.append(action) - } - - func present( - on presentingViewController: UIViewController, animated: Bool, - completion: (() -> Void)? = nil - ) { - presented = true - self.presentingViewController = presentingViewController - // The plugin does not use the passed action, so just send a dummy value. If that ever - // changes, the test will need to track the action along with the handler. - onPresentActionHandler?(UIAlertAction()) - } - } - -#endif - -final class StubAlertFactory: AuthAlertFactory { - #if os(macOS) - var alert: TestAlert = TestAlert() - #else - var alertController: TestAlertController = TestAlertController() - #endif - - #if os(macOS) - func createAlert() -> AuthAlert { - return self.alert - } - #else - func createAlertController( - title: String?, message: String?, preferredStyle: UIAlertController.Style - ) -> AuthAlertController { - return self.alertController - } - - func createAlertAction( - title: String?, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)? - ) -> UIAlertAction { - // Configure the fake controller to trigger this button when presented. This is currently an - // arbitrary button, just to ensure that the completion handler is triggered so that the - // test can wait for the full cycle of async calls to complete. - alertController.onPresentActionHandler = handler - return UIAlertAction(title: title, style: style, handler: handler) - } - #endif -} - final class StubAuthContext: NSObject, AuthContext { /// Whether calls to this stub are expected to be for biometric authentication. /// @@ -172,16 +81,14 @@ class LocalAuthPluginTests: XCTestCase { func testSuccessfullAuthWithBiometrics() throws { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.expectBiometrics = true stubAuthContext.evaluateResponse = true let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: true, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: true, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -199,16 +106,14 @@ class LocalAuthPluginTests: XCTestCase { func testSuccessfullAuthWithoutBiometrics() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateResponse = true let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -226,9 +131,7 @@ class LocalAuthPluginTests: XCTestCase { func testFailedAuthWithBiometrics() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.expectBiometrics = true @@ -237,7 +140,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: true, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: true, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -255,9 +158,7 @@ class LocalAuthPluginTests: XCTestCase { func testFailedAuthWithErrorAppCancel() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( @@ -265,7 +166,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -283,9 +184,7 @@ class LocalAuthPluginTests: XCTestCase { func testFailedAuthWithErrorSystemCancel() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( @@ -293,7 +192,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -311,9 +210,7 @@ class LocalAuthPluginTests: XCTestCase { func testFailedAuthWithErrorUserCancel() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( @@ -321,7 +218,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -339,9 +236,7 @@ class LocalAuthPluginTests: XCTestCase { func testFailedAuthWithErrorUserFallback() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( @@ -349,7 +244,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -363,42 +258,94 @@ class LocalAuthPluginTests: XCTestCase { self.waitForExpectations(timeout: timeout) } - @available(macOS 11.2, *) - @MainActor - func testFailedAuthWithErrorBiometricDisconnected() { - let stubAuthContext = StubAuthContext() - let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) - - let strings = createAuthStrings() - stubAuthContext.canEvaluateError = NSError( - domain: "LocalAuthentication", code: LAError.biometryDisconnected.rawValue) - - let expectation = expectation(description: "Result is called") - plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), - strings: strings - ) { resultDetails in - switch resultDetails { - case .success(let successDetails): - XCTAssertEqual(successDetails.result, .biometryDisconnected) - case .failure(let error): - XCTFail("Unexpected error: \(error)") + #if os(macOS) + @available(macOS 11.2, *) + @MainActor + func testFailedAuthWithErrorBiometricDisconnected() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryDisconnected.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .biometryDisconnected) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() } - expectation.fulfill() + self.waitForExpectations(timeout: timeout) + } + + @available(macOS 11.2, *) + @MainActor + func testFailedAuthWithErrorBiometricNotPaired() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryNotPaired.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .biometryNotPaired) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @available(macOS 12.0, *) + @MainActor + func testFailedAuthWithErrorBiometricInvalidDimensions() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.invalidDimensions.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .invalidDimensions) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) } - self.waitForExpectations(timeout: timeout) - } + #endif @MainActor func testFailedAuthWithErrorBiometricLockout() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.canEvaluateError = NSError( @@ -406,7 +353,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -424,9 +371,7 @@ class LocalAuthPluginTests: XCTestCase { func testFailedAuthWithErrorBiometricNotAvailable() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.canEvaluateError = NSError( @@ -434,7 +379,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -452,9 +397,7 @@ class LocalAuthPluginTests: XCTestCase { func testFailedAuthWithErrorBiometricNotEnrolled() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.canEvaluateError = NSError( @@ -462,7 +405,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -476,42 +419,11 @@ class LocalAuthPluginTests: XCTestCase { self.waitForExpectations(timeout: timeout) } - @available(macOS 11.2, *) - @MainActor - func testFailedAuthWithErrorBiometricNotPaired() { - let stubAuthContext = StubAuthContext() - let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) - - let strings = createAuthStrings() - stubAuthContext.canEvaluateError = NSError( - domain: "LocalAuthentication", code: LAError.biometryNotPaired.rawValue) - - let expectation = expectation(description: "Result is called") - plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), - strings: strings - ) { resultDetails in - switch resultDetails { - case .success(let successDetails): - XCTAssertEqual(successDetails.result, .biometryNotPaired) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - self.waitForExpectations(timeout: timeout) - } - @MainActor func testFailedAuthWithErrorBiometricInvalidContext() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.canEvaluateError = NSError( @@ -519,7 +431,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -533,42 +445,11 @@ class LocalAuthPluginTests: XCTestCase { self.waitForExpectations(timeout: timeout) } - @available(macOS 12.0, *) - @MainActor - func testFailedAuthWithErrorBiometricInvalidDimensions() { - let stubAuthContext = StubAuthContext() - let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) - - let strings = createAuthStrings() - stubAuthContext.canEvaluateError = NSError( - domain: "LocalAuthentication", code: LAError.invalidDimensions.rawValue) - - let expectation = expectation(description: "Result is called") - plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), - strings: strings - ) { resultDetails in - switch resultDetails { - case .success(let successDetails): - XCTAssertEqual(successDetails.result, .invalidDimensions) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - self.waitForExpectations(timeout: timeout) - } - @MainActor func testFailedAuthWithErrorBiometricNotInteractive() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.canEvaluateError = NSError( @@ -576,7 +457,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -594,9 +475,7 @@ class LocalAuthPluginTests: XCTestCase { func testFailedAuthWithErrorBiometricPasscodeNotSet() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.canEvaluateError = NSError( @@ -604,7 +483,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -622,16 +501,14 @@ class LocalAuthPluginTests: XCTestCase { func testFailedWithUnknownErrorCode() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError(domain: "error", code: 99) let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -649,16 +526,14 @@ class LocalAuthPluginTests: XCTestCase { func testSystemCancelledWithoutStickyAuth() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError(domain: "error", code: LAError.systemCancel.rawValue) let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -676,9 +551,7 @@ class LocalAuthPluginTests: XCTestCase { func testFailedAuthWithoutBiometrics() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( @@ -686,7 +559,7 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in switch resultDetails { @@ -700,51 +573,18 @@ class LocalAuthPluginTests: XCTestCase { self.waitForExpectations(timeout: timeout) } - @MainActor - func testFailedAuthShowsAlert() { - 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: "error", code: LAError.biometryNotEnrolled.rawValue) - - let expectation = expectation(description: "Result is called") - plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: true), - strings: strings - ) { resultDetails in - expectation.fulfill() - } - - self.waitForExpectations(timeout: timeout) - #if os(macOS) - XCTAssertEqual(alertFactory.alert.presentingWindow, viewProvider.view?.window) - #else - XCTAssertTrue(alertFactory.alertController.presented) - XCTAssertEqual(alertFactory.alertController.actions.count, 2) - #endif - } - @MainActor func testLocalizedFallbackTitle() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings(localizedFallbackTitle: "a title") stubAuthContext.evaluateResponse = true let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in XCTAssertEqual(stubAuthContext.localizedFallbackTitle, strings.localizedFallbackTitle) @@ -757,16 +597,14 @@ class LocalAuthPluginTests: XCTestCase { func testSkippedLocalizedFallbackTitle() { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings(localizedFallbackTitle: nil) stubAuthContext.evaluateResponse = true let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in XCTAssertNil(stubAuthContext.localizedFallbackTitle) @@ -778,9 +616,7 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withEnrolledHardware() throws { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true @@ -791,9 +627,7 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withNonEnrolledHardware() throws { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.canEvaluateError = NSError( @@ -806,9 +640,7 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withBiometryNotAvailable() throws { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.canEvaluateError = NSError( @@ -821,9 +653,7 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withBiometryNotAvailableWhenPermissionsDenied() throws { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.biometryType = LABiometryType.touchID @@ -837,9 +667,7 @@ class LocalAuthPluginTests: XCTestCase { func testGetEnrolledBiometricsWithFaceID() throws { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true if #available(iOS 11, macOS 10.15, *) { @@ -854,9 +682,7 @@ class LocalAuthPluginTests: XCTestCase { func testGetEnrolledBiometricsWithTouchID() throws { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.biometryType = .touchID @@ -869,9 +695,7 @@ class LocalAuthPluginTests: XCTestCase { func testGetEnrolledBiometricsWithoutEnrolledHardware() throws { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.canEvaluateError = NSError( @@ -884,9 +708,7 @@ class LocalAuthPluginTests: XCTestCase { func testIsDeviceSupportedHandlesSupported() throws { let stubAuthContext = StubAuthContext() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let result = try plugin.isDeviceSupported() XCTAssertTrue(result) @@ -897,9 +719,7 @@ class LocalAuthPluginTests: XCTestCase { // An arbitrary error to cause canEvaluatePolicy to return false. stubAuthContext.canEvaluateError = NSError(domain: "error", code: 1) let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: StubAlertFactory(), - viewProvider: StubViewProvider()) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let result = try plugin.isDeviceSupported() XCTAssertFalse(result) @@ -909,9 +729,6 @@ class LocalAuthPluginTests: XCTestCase { func createAuthStrings(localizedFallbackTitle: String? = nil) -> AuthStrings { return AuthStrings( reason: "a reason", - lockOut: "locked out", - goToSettingsButton: "Go To Settings", - goToSettingsDescription: "Settings", cancelButton: "Cancel", localizedFallbackTitle: localizedFallbackTitle) } 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 0359f874859..7ca53148fb1 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 @@ -21,80 +21,6 @@ final class DefaultAuthContextFactory: AuthContextFactory { // MARK: - -#if os(iOS) - /// A default alert controller that wraps UIAlertController. - final class DefaultAlertController: AuthAlertController { - /// The wrapped alert controller. - private let controller: UIAlertController - - /// Returns a wrapper for the given UIAlertController. - init(wrapping controller: UIAlertController) { - self.controller = controller - } - - @MainActor - func addAction(_ action: UIAlertAction) { - controller.addAction(action) - } - - @MainActor - func present( - on presentingViewController: UIViewController, - animated: Bool, - completion: (() -> Void)? = nil - ) { - presentingViewController.present(controller, animated: animated, completion: completion) - } - } -#endif // os(iOS) - -/// A default alert factory that wraps standard UIAlertController and NSAlert allocation for iOS and -/// macOS respectfully. -final class DefaultAlertFactory: AuthAlertFactory { - #if os(macOS) - func createAlert() -> AuthAlert { - return NSAlert() - } - #elseif os(iOS) - func createAlertController( - title: String?, - message: String?, - preferredStyle: UIAlertController.Style - ) -> AuthAlertController { - return DefaultAlertController( - wrapping: - UIAlertController(title: title, message: message, preferredStyle: preferredStyle)) - } - - func createAlertAction( - title: String?, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)? = nil - ) -> UIAlertAction { - return UIAlertAction(title: title, style: style, handler: handler) - } - #endif -} - -// MARK: - - -/// A default view provider that wraps the FlutterPluginRegistrar. -final class DefaultViewProvider: ViewProvider { - /// The wrapped registrar. - let registrar: FlutterPluginRegistrar - - /// Returns a wrapper for the given FlutterPluginRegistrar. - init(registrar: FlutterPluginRegistrar) { - self.registrar = registrar - } - - #if os(macOS) - var view: NSView? { - return registrar.view - } - #endif // os(macOS) -} - -// MARK: - - /// A data container for sticky auth state. struct StickyAuthState { let options: AuthOptions @@ -111,18 +37,12 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch /// The factory to create LAContexts. private let authContextFactory: AuthContextFactory - /// The factory to create alerts. - private let alertFactory: AuthAlertFactory - /// The Flutter view provider. - private let viewProvider: ViewProvider /// Manages the last call state for sticky auth. private var lastCallState: StickyAuthState? public static func register(with registrar: FlutterPluginRegistrar) { let instance = LocalAuthPlugin( - contextFactory: DefaultAuthContextFactory(), - alertFactory: DefaultAlertFactory(), - viewProvider: DefaultViewProvider(registrar: registrar)) + contextFactory: DefaultAuthContextFactory()) registrar.addApplicationDelegate(instance) // Workaround for https://github.com/flutter/flutter/issues/118103. #if os(iOS) @@ -135,13 +55,9 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch /// Returns an instance that uses the given factory to create LAContexts. init( - contextFactory: AuthContextFactory, - alertFactory: AuthAlertFactory, - viewProvider: ViewProvider + contextFactory: AuthContextFactory ) { self.authContextFactory = contextFactory - self.alertFactory = alertFactory - self.viewProvider = viewProvider } // MARK: LocalAuthApi @@ -242,67 +158,6 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch // MARK: Private Methods - @MainActor - private func showAlert( - message: String, - dismissButtonTitle: String, - openSettingsButtonTitle: String?, - completion: @escaping (Result) -> Void - ) { - #if os(macOS) - var alert = alertFactory.createAlert() - alert.messageText = message - alert.addButton(withTitle: dismissButtonTitle) - if let window = viewProvider.view?.window { - alert.beginSheetModal(for: window) { [weak self] code in - self?.handleResult(result: .showedAlert, completion: completion) - } - } else { - alert.runModal() - self.handleResult(result: .showedAlert, completion: completion) - } - #elseif os(iOS) - // TODO(stuartmorgan): Get the view controller from the view provider once it's possible. - // See https://github.com/flutter/flutter/issues/104117. - guard let controller = UIApplication.shared.delegate?.window??.rootViewController else { - completion( - .success( - AuthResultDetails( - result: .uiUnavailable, - errorMessage: "Unable to obtain root view controller", - errorDetails: nil) - )) - return - } - let alert = alertFactory.createAlertController( - title: "", - message: message, - preferredStyle: .alert) - - let defaultAction = alertFactory.createAlertAction( - title: dismissButtonTitle, - style: .default - ) { [weak self] action in - self?.handleResult(result: .showedAlert, completion: completion) - } - - alert.addAction(defaultAction) - if let openSettingsButtonTitle = openSettingsButtonTitle, - let url = URL(string: UIApplication.openSettingsURLString) - { - let additionalAction = UIAlertAction( - title: openSettingsButtonTitle, - style: .default - ) { [weak self] action in - UIApplication.shared.open(url, options: [:], completionHandler: nil) - self?.handleResult(result: .showedAlert, completion: completion) - } - alert.addAction(additionalAction) - } - alert.present(on: controller, animated: true, completion: nil) - #endif - } - private func handleAuthReply( success: Bool, error: Error?, @@ -366,30 +221,10 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch case .biometryDisconnected: result = .biometryDisconnected case .biometryLockout: - if options.useErrorDialogs { - DispatchQueue.main.async { [weak self] in - self?.showAlert( - message: strings.lockOut, - dismissButtonTitle: strings.cancelButton, - openSettingsButtonTitle: nil, - completion: completion) - } - return - } result = .biometryLockout case .biometryNotAvailable: result = .biometryNotAvailable case .biometryNotEnrolled: - if options.useErrorDialogs { - DispatchQueue.main.async { [weak self] in - self?.showAlert( - message: strings.goToSettingsDescription, - dismissButtonTitle: strings.cancelButton, - openSettingsButtonTitle: strings.goToSettingsButton, - completion: completion) - } - return - } result = .biometryNotEnrolled case .biometryNotPaired: result = .biometryNotPaired @@ -402,16 +237,6 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch case .notInteractive: result = .notInteractive case .passcodeNotSet: - if options.useErrorDialogs { - DispatchQueue.main.async { [weak self] in - self?.showAlert( - message: strings.goToSettingsDescription, - dismissButtonTitle: strings.cancelButton, - openSettingsButtonTitle: strings.goToSettingsButton, - completion: completion) - } - return - } result = .passcodeNotSet case .userFallback: result = .userFallback diff --git a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/SystemWrappers.swift b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/SystemWrappers.swift index 5b603d0cb68..6bccbc0527d 100644 --- a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/SystemWrappers.swift +++ b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/SystemWrappers.swift @@ -46,93 +46,3 @@ protocol AuthContextFactory { /// In production code, this should return an LAContext. func createAuthContext() -> AuthContext } - -// MARK: - - -#if os(macOS) - /// Protocol for interacting with NSAlert instances, abstracted to allow using mock/fake instances - /// in unit tests. - protocol AuthAlert { - /// Direct passthrough to NSAlert's messageText. - @MainActor - var messageText: String { get set } - - /// Direct passthrough to NSAlert's addButton. - @MainActor - @discardableResult func addButton(withTitle title: String) -> NSButton - - /// Direct passthrough to NSAlert's beginSheetModal. - @MainActor - func beginSheetModal( - for sheetWindow: NSWindow, - completionHandler handler: ((NSApplication.ModalResponse) -> Void)? - ) - - /// Direct passthrough to NSAlert's runModal. - @MainActor - @discardableResult func runModal() -> NSApplication.ModalResponse - } - - /// AuthAlert is intentionally a direct passthroguh to NSAlert. - extension NSAlert: AuthAlert {} -#endif // macOS - -#if os(iOS) - /// Protocol for interacting with UIAlertController instances, abstracted to allow using mock/fake - /// instances in unit tests. - protocol AuthAlertController { - /// Direct passthrough to UIAlertController's addAction. - @MainActor - func addAction(_ action: UIAlertAction) - - /// Reversed wrapper of presentViewController:... since the protocol can't be passed to the real - /// method. - @MainActor - func present( - on presentingViewController: UIViewController, - animated: Bool, - completion: (() -> Void)? - ) - } -#endif // iOS - -/// Protocol for a factory that wraps standard UIAlertController and NSAlert creation for -/// iOS and macOS. Used to allow context injection in unit tests. -protocol AuthAlertFactory { - #if os(macOS) - /// Creates a new instance of an implementation of the AuthAlert abstraction. - /// - /// In production code, this should return an NSAlert. - func createAlert() -> AuthAlert - #elseif os(iOS) - /// Creates a new instance of an implementation of the AuthAlertController abstraction. - /// - /// In production code, this should return something as close as possible to a direct passthrough - /// to UIAlertController. - func createAlertController( - title: String?, - message: String?, - preferredStyle: UIAlertController.Style - ) -> AuthAlertController - - /// Creates a new instance of a UIAlertAction. - /// - /// Abstracted to allow unit tests to capture the handler, since UIAlertAction does not provide - /// a getter for the handler. - func createAlertAction( - title: String?, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)? - ) -> UIAlertAction - #endif -} - -/// Protocol for a provider of the view containing the Flutter content, abstracted to allow using -/// mock/fake instances in unit tests. -protocol ViewProvider { - #if os(macOS) - /// Returns the view displaying the Flutter content, if any. - var view: NSView? { get } - #elseif os(iOS) - // TODO(stuartmorgan): Add a view accessor once https://github.com/flutter/flutter/issues/104117 - // is resolved, and use that in 'showAlertWithMessage:...'. - #endif -} 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 777c7e254b4..374c0887583 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 @@ -135,24 +135,22 @@ enum AuthResult: Int { case success = 0 /// Native UI needed to be displayed, but couldn't be. case uiUnavailable = 1 - /// The plugin showed an alert as the final step. - case showedAlert = 2 - case appCancel = 3 - case systemCancel = 4 - case userCancel = 5 - case biometryDisconnected = 6 - case biometryLockout = 7 - case biometryNotAvailable = 8 - case biometryNotEnrolled = 9 - case biometryNotPaired = 10 - case authenticationFailed = 11 - case invalidContext = 12 - case invalidDimensions = 13 - case notInteractive = 14 - case passcodeNotSet = 15 - case userFallback = 16 + case appCancel = 2 + case systemCancel = 3 + case userCancel = 4 + case biometryDisconnected = 5 + case biometryLockout = 6 + case biometryNotAvailable = 7 + case biometryNotEnrolled = 8 + case biometryNotPaired = 9 + case authenticationFailed = 10 + case invalidContext = 11 + case invalidDimensions = 12 + case notInteractive = 13 + case passcodeNotSet = 14 + case userFallback = 15 /// An error other than the expected types occurred. - case unknownError = 17 + case unknownError = 16 } /// Pigeon equivalent of the subset of BiometricType used by iOS. @@ -168,26 +166,17 @@ enum AuthBiometric: Int { /// Generated class from Pigeon that represents data sent in messages. struct AuthStrings: Hashable { var reason: String - var lockOut: String - var goToSettingsButton: String? = nil - var goToSettingsDescription: String var cancelButton: String var localizedFallbackTitle: String? = nil // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> AuthStrings? { let reason = pigeonVar_list[0] as! String - let lockOut = pigeonVar_list[1] as! String - let goToSettingsButton: String? = nilOrValue(pigeonVar_list[2]) - let goToSettingsDescription = pigeonVar_list[3] as! String - let cancelButton = pigeonVar_list[4] as! String - let localizedFallbackTitle: String? = nilOrValue(pigeonVar_list[5]) + let cancelButton = pigeonVar_list[1] as! String + let localizedFallbackTitle: String? = nilOrValue(pigeonVar_list[2]) return AuthStrings( reason: reason, - lockOut: lockOut, - goToSettingsButton: goToSettingsButton, - goToSettingsDescription: goToSettingsDescription, cancelButton: cancelButton, localizedFallbackTitle: localizedFallbackTitle ) @@ -195,9 +184,6 @@ struct AuthStrings: Hashable { func toList() -> [Any?] { return [ reason, - lockOut, - goToSettingsButton, - goToSettingsDescription, cancelButton, localizedFallbackTitle, ] @@ -214,25 +200,21 @@ struct AuthStrings: Hashable { struct AuthOptions: Hashable { var biometricOnly: Bool var sticky: Bool - var useErrorDialogs: Bool // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> AuthOptions? { let biometricOnly = pigeonVar_list[0] as! Bool let sticky = pigeonVar_list[1] as! Bool - let useErrorDialogs = pigeonVar_list[2] as! Bool return AuthOptions( biometricOnly: biometricOnly, - sticky: sticky, - useErrorDialogs: useErrorDialogs + sticky: sticky ) } func toList() -> [Any?] { return [ biometricOnly, sticky, - useErrorDialogs, ] } static func == (lhs: AuthOptions, rhs: AuthOptions) -> Bool { diff --git a/packages/local_auth/local_auth_darwin/example/lib/main.dart b/packages/local_auth/local_auth_darwin/example/lib/main.dart index 3ec0b5a604f..62fc11fc0d8 100644 --- a/packages/local_auth/local_auth_darwin/example/lib/main.dart +++ b/packages/local_auth/local_auth_darwin/example/lib/main.dart @@ -94,11 +94,21 @@ class _MyAppState extends State { setState(() { _isAuthenticating = false; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected Error - ${e.message}'; }); return; } @@ -131,11 +141,21 @@ class _MyAppState extends State { _isAuthenticating = false; _authorized = 'Authenticating'; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected Error - ${e.message}'; }); return; } 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 2ce9f131e00..37fc27af163 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 @@ -44,22 +44,17 @@ class LocalAuthDarwin extends LocalAuthPlatform { AuthOptions( biometricOnly: options.biometricOnly, sticky: options.stickyAuth, - useErrorDialogs: options.useErrorDialogs, ), _useMacOSAuthMessages ? _pigeonStringsFromMacOSAuthMessages(localizedReason, authMessages) : _pigeonStringsFromiOSAuthMessages(localizedReason, authMessages), ); LocalAuthExceptionCode code; - String? description = resultDetails.errorMessage; switch (resultDetails.result) { case AuthResult.success: return true; case AuthResult.authenticationFailed: return false; - case AuthResult.showedAlert: - // Temporary compat with previous return until alerts are removed. - return false; case AuthResult.appCancel: // If the plugin client intentionally canceled authentication, no need // to return a specific error. @@ -94,7 +89,7 @@ class LocalAuthDarwin extends LocalAuthPlatform { } throw LocalAuthException( code: code, - description: description, + description: resultDetails.errorMessage, details: resultDetails.errorDetails, ); } @@ -137,13 +132,7 @@ class LocalAuthDarwin extends LocalAuthPlatform { } return AuthStrings( reason: localizedReason, - lockOut: messages?.lockOut ?? iOSLockOut, - goToSettingsButton: messages?.goToSettingsButton ?? goToSettings, - goToSettingsDescription: - messages?.goToSettingsDescription ?? iOSGoToSettingsDescription, - // TODO(stuartmorgan): The default's name is confusing here for legacy - // reasons; this should be fixed as part of some future breaking change. - cancelButton: messages?.cancelButton ?? iOSOkButton, + cancelButton: messages?.cancelButton ?? iOSCancelButton, localizedFallbackTitle: messages?.localizedFallbackTitle, ); } @@ -161,9 +150,6 @@ class LocalAuthDarwin extends LocalAuthPlatform { } return AuthStrings( reason: localizedReason, - lockOut: messages?.lockOut ?? macOSLockOut, - goToSettingsDescription: - messages?.goToSettingsDescription ?? macOSGoToSettingsDescription, cancelButton: messages?.cancelButton ?? macOSCancelButton, localizedFallbackTitle: messages?.localizedFallbackTitle, ); 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 fc630518630..32902927fd0 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 @@ -43,9 +43,6 @@ enum AuthResult { /// Native UI needed to be displayed, but couldn't be. uiUnavailable, - - /// The plugin showed an alert as the final step. - showedAlert, appCancel, systemCancel, userCancel, @@ -74,34 +71,18 @@ enum AuthBiometric { face, fingerprint } class AuthStrings { AuthStrings({ required this.reason, - required this.lockOut, - this.goToSettingsButton, - required this.goToSettingsDescription, required this.cancelButton, this.localizedFallbackTitle, }); String reason; - String lockOut; - - String? goToSettingsButton; - - String goToSettingsDescription; - String cancelButton; String? localizedFallbackTitle; List _toList() { - return [ - reason, - lockOut, - goToSettingsButton, - goToSettingsDescription, - cancelButton, - localizedFallbackTitle, - ]; + return [reason, cancelButton, localizedFallbackTitle]; } Object encode() { @@ -112,11 +93,8 @@ class AuthStrings { result as List; return AuthStrings( reason: result[0]! as String, - lockOut: result[1]! as String, - goToSettingsButton: result[2] as String?, - goToSettingsDescription: result[3]! as String, - cancelButton: result[4]! as String, - localizedFallbackTitle: result[5] as String?, + cancelButton: result[1]! as String, + localizedFallbackTitle: result[2] as String?, ); } @@ -138,20 +116,14 @@ class AuthStrings { } class AuthOptions { - AuthOptions({ - required this.biometricOnly, - required this.sticky, - required this.useErrorDialogs, - }); + AuthOptions({required this.biometricOnly, required this.sticky}); bool biometricOnly; bool sticky; - bool useErrorDialogs; - List _toList() { - return [biometricOnly, sticky, useErrorDialogs]; + return [biometricOnly, sticky]; } Object encode() { @@ -163,7 +135,6 @@ class AuthOptions { return AuthOptions( biometricOnly: result[0]! as bool, sticky: result[1]! as bool, - useErrorDialogs: result[2]! as bool, ); } diff --git a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart index d5a4c317143..3a36fc53f53 100644 --- a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart +++ b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart @@ -11,25 +11,7 @@ import 'package:local_auth_platform_interface/types/auth_messages.dart'; @immutable class IOSAuthMessages extends AuthMessages { /// Constructs a new instance. - const IOSAuthMessages({ - this.lockOut, - this.goToSettingsButton, - this.goToSettingsDescription, - this.cancelButton, - this.localizedFallbackTitle, - }); - - /// Message advising the user to re-enable biometrics on their device. - final String? lockOut; - - /// Message shown on a button that the user can click to go to settings pages - /// from the current dialog. - /// Maximum 30 characters. - final String? goToSettingsButton; - - /// Message advising the user to go to the settings and configure Biometrics - /// for their device. - final String? goToSettingsDescription; + const IOSAuthMessages({this.cancelButton, this.localizedFallbackTitle}); /// Message shown on a button that the user can click to leave the current /// dialog. @@ -45,11 +27,7 @@ class IOSAuthMessages extends AuthMessages { @override Map get args { return { - 'lockOut': lockOut ?? iOSLockOut, - 'goToSetting': goToSettingsButton ?? goToSettings, - 'goToSettingDescriptionIOS': - goToSettingsDescription ?? iOSGoToSettingsDescription, - 'okButton': cancelButton ?? iOSOkButton, + 'okButton': cancelButton ?? iOSCancelButton, if (localizedFallbackTitle != null) 'localizedFallbackTitle': localizedFallbackTitle!, }; @@ -60,56 +38,20 @@ class IOSAuthMessages extends AuthMessages { identical(this, other) || other is IOSAuthMessages && runtimeType == other.runtimeType && - lockOut == other.lockOut && - goToSettingsButton == other.goToSettingsButton && - goToSettingsDescription == other.goToSettingsDescription && cancelButton == other.cancelButton && localizedFallbackTitle == other.localizedFallbackTitle; @override - int get hashCode => Object.hash( - super.hashCode, - lockOut, - goToSettingsButton, - goToSettingsDescription, - cancelButton, - localizedFallbackTitle, - ); + int get hashCode => + Object.hash(super.hashCode, cancelButton, localizedFallbackTitle); } // Default Strings for IOSAuthMessages plugin. Currently supports English. // Intl.message must be string literals. -/// Message shown on a button that the user can click to go to settings pages -/// from the current dialog. -String get goToSettings => Intl.message( - 'Go to settings', - desc: - 'Message shown on a button that the user can click to go to ' - 'settings pages from the current dialog. Maximum 30 characters.', -); - -/// Message advising the user to re-enable biometrics on their device. -/// It shows in a dialog on iOS. -String get iOSLockOut => Intl.message( - 'Biometric authentication is disabled. Please lock and unlock your screen to ' - 'enable it.', - desc: 'Message advising the user to re-enable biometrics on their device.', -); - -/// Message advising the user to go to the settings and configure Biometrics -/// for their device. -String get iOSGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Please either enable ' - 'Touch ID or Face ID on your phone.', - desc: - 'Message advising the user to go to the settings and configure Biometrics ' - 'for their device.', -); - /// Message shown on a button that the user can click to leave the current /// dialog. -String get iOSOkButton => Intl.message( +String get iOSCancelButton => Intl.message( 'OK', desc: 'Message showed on a button that the user can click to leave the ' diff --git a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart index 39fb82edc0f..30b4a2d55fc 100644 --- a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart +++ b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart @@ -11,19 +11,7 @@ import 'package:local_auth_platform_interface/types/auth_messages.dart'; @immutable class MacOSAuthMessages extends AuthMessages { /// Constructs a new instance. - const MacOSAuthMessages({ - this.lockOut, - this.goToSettingsDescription, - this.cancelButton, - this.localizedFallbackTitle, - }); - - /// Message advising the user to re-enable biometrics on their device. - final String? lockOut; - - /// Message advising the user to go to the settings and configure Biometrics - /// for their device. - final String? goToSettingsDescription; + const MacOSAuthMessages({this.cancelButton, this.localizedFallbackTitle}); /// Message shown on a button that the user can click to leave the current /// dialog. @@ -39,7 +27,6 @@ class MacOSAuthMessages extends AuthMessages { @override Map get args { return { - 'lockOut': lockOut ?? macOSLockOut, 'okButton': cancelButton ?? macOSCancelButton, if (localizedFallbackTitle != null) 'localizedFallbackTitle': localizedFallbackTitle!, @@ -51,39 +38,17 @@ class MacOSAuthMessages extends AuthMessages { identical(this, other) || other is MacOSAuthMessages && runtimeType == other.runtimeType && - lockOut == other.lockOut && cancelButton == other.cancelButton && localizedFallbackTitle == other.localizedFallbackTitle; @override - int get hashCode => Object.hash( - super.hashCode, - lockOut, - cancelButton, - localizedFallbackTitle, - ); + int get hashCode => + Object.hash(super.hashCode, cancelButton, localizedFallbackTitle); } // Default Strings for MacOSAuthMessages plugin. Currently supports English. // Intl.message must be string literals. -/// Message advising the user to re-enable biometrics on their device. -/// It shows in a dialog on macOS. -String get macOSLockOut => Intl.message( - 'Biometric authentication is disabled. Please restart your computer and try again.', - desc: 'Message advising the user to re-enable biometrics on their device.', -); - -/// Message advising the user to go to the settings and configure Biometrics -/// for their device. -String get macOSGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Please enable ' - 'Touch ID on your computer in the Settings app.', - desc: - 'Message advising the user to go to the settings and configure Biometrics ' - 'for their device.', -); - /// Message shown on a button that the user can click to leave the current /// dialog. String get macOSCancelButton => Intl.message( diff --git a/packages/local_auth/local_auth_darwin/pigeons/messages.dart b/packages/local_auth/local_auth_darwin/pigeons/messages.dart index f2a4401cd3f..f590791360b 100644 --- a/packages/local_auth/local_auth_darwin/pigeons/messages.dart +++ b/packages/local_auth/local_auth_darwin/pigeons/messages.dart @@ -19,17 +19,11 @@ class AuthStrings { /// Constructs a new instance. const AuthStrings({ required this.reason, - required this.lockOut, - this.goToSettingsButton, - required this.goToSettingsDescription, required this.cancelButton, required this.localizedFallbackTitle, }); final String reason; - final String lockOut; - final String? goToSettingsButton; - final String goToSettingsDescription; final String cancelButton; final String? localizedFallbackTitle; } @@ -42,9 +36,6 @@ enum AuthResult { /// Native UI needed to be displayed, but couldn't be. uiUnavailable, - /// The plugin showed an alert as the final step. - showedAlert, - // LAError codes; see // https://developer.apple.com/documentation/localauthentication/laerror-swift.struct/code appCancel, @@ -67,14 +58,9 @@ enum AuthResult { } class AuthOptions { - AuthOptions({ - required this.biometricOnly, - required this.sticky, - required this.useErrorDialogs, - }); + AuthOptions({required this.biometricOnly, required this.sticky}); final bool biometricOnly; final bool sticky; - final bool useErrorDialogs; } class AuthResultDetails { @@ -91,10 +77,6 @@ class AuthResultDetails { final String? errorMessage; /// System-provided error details, if any. - // TODO(stuartmorgan): Remove this when standardizing errors plugin-wide in - // a breaking change. This is here only to preserve the existing error format - // exactly for compatibility, in case clients were checking PlatformException - // details. final String? errorDetails; } 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 74cdad1830b..24de41ecc7b 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 @@ -107,10 +107,7 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_ios.dart - expect(strings.lockOut, iOSLockOut); - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, iOSGoToSettingsDescription); - expect(strings.cancelButton, iOSOkButton); + expect(strings.cancelButton, iOSCancelButton); expect(strings.localizedFallbackTitle, null); }); @@ -139,10 +136,7 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_ios.dart - expect(strings.lockOut, iOSLockOut); - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, iOSGoToSettingsDescription); - expect(strings.cancelButton, iOSOkButton); + expect(strings.cancelButton, iOSCancelButton); expect(strings.localizedFallbackTitle, null); }, ); @@ -172,8 +166,6 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_ios.dart - expect(strings.lockOut, macOSLockOut); - expect(strings.goToSettingsDescription, macOSGoToSettingsDescription); expect(strings.cancelButton, macOSCancelButton); expect(strings.localizedFallbackTitle, null); }, @@ -195,19 +187,13 @@ void main() { // - they are different from the defaults, and // - they are different from each other. const String reason = 'A'; - const String lockOut = 'B'; - const String goToSettingsButton = 'C'; - const String gotToSettingsDescription = 'D'; - const String cancel = 'E'; - const String localizedFallbackTitle = 'F'; + const String cancel = 'B'; + const String localizedFallbackTitle = 'C'; await plugin.authenticate( localizedReason: reason, authMessages: [ const IOSAuthMessages( - lockOut: lockOut, - goToSettingsButton: goToSettingsButton, - goToSettingsDescription: gotToSettingsDescription, cancelButton: cancel, localizedFallbackTitle: localizedFallbackTitle, ), @@ -220,9 +206,6 @@ void main() { ); final AuthStrings strings = result.captured[0] as AuthStrings; expect(strings.reason, reason); - expect(strings.lockOut, lockOut); - expect(strings.goToSettingsButton, goToSettingsButton); - expect(strings.goToSettingsDescription, gotToSettingsDescription); expect(strings.cancelButton, cancel); expect(strings.localizedFallbackTitle, localizedFallbackTitle); }, @@ -243,14 +226,12 @@ void main() { // - they are different from the defaults, and // - they are different from each other. const String reason = 'A'; - const String lockOut = 'B'; - const String cancel = 'E'; - const String localizedFallbackTitle = 'F'; + const String cancel = 'B'; + const String localizedFallbackTitle = 'C'; await plugin.authenticate( localizedReason: reason, authMessages: [ const MacOSAuthMessages( - lockOut: lockOut, cancelButton: cancel, localizedFallbackTitle: localizedFallbackTitle, ), @@ -263,7 +244,6 @@ void main() { ); final AuthStrings strings = result.captured[0] as AuthStrings; expect(strings.reason, reason); - expect(strings.lockOut, lockOut); expect(strings.cancelButton, cancel); expect(strings.localizedFallbackTitle, localizedFallbackTitle); }, @@ -280,16 +260,12 @@ void main() { // - they are different from the defaults, and // - they are different from each other. const String reason = 'A'; - const String lockOut = 'B'; - const String localizedFallbackTitle = 'C'; - const String cancel = 'D'; + const String localizedFallbackTitle = 'B'; await plugin.authenticate( localizedReason: reason, authMessages: [ const IOSAuthMessages( - lockOut: lockOut, localizedFallbackTitle: localizedFallbackTitle, - cancelButton: cancel, ), ], ); @@ -298,15 +274,12 @@ void main() { api.authenticate(any, captureAny), ); final AuthStrings strings = result.captured[0] as AuthStrings; - expect(strings.reason, reason); // These should all be the provided values. - expect(strings.lockOut, lockOut); + expect(strings.reason, reason); expect(strings.localizedFallbackTitle, localizedFallbackTitle); - expect(strings.cancelButton, cancel); // These were not set, so should all be the default values from // auth_messages_ios.dart - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, iOSGoToSettingsDescription); + expect(strings.cancelButton, iOSCancelButton); }); }); @@ -329,7 +302,6 @@ void main() { final AuthOptions options = result.captured[0] as AuthOptions; expect(options.biometricOnly, false); expect(options.sticky, false); - expect(options.useErrorDialogs, true); }); test('passes provided non-default values', () async { @@ -343,7 +315,6 @@ void main() { options: const AuthenticationOptions( biometricOnly: true, stickyAuth: true, - useErrorDialogs: false, ), ); @@ -353,7 +324,6 @@ void main() { final AuthOptions options = result.captured[0] as AuthOptions; expect(options.biometricOnly, true); expect(options.sticky, true); - expect(options.useErrorDialogs, false); }); }); @@ -398,19 +368,6 @@ void main() { expect(result, false); }); - test('handles showedAlert as failure', () async { - when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(result: AuthResult.showedAlert), - ); - - final bool result = await plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ); - - expect(result, false); - }); - test( 'converts uiUnavailable to LocalAuthExceptionCode.uiUnavailable', () async { diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart index b2cc9c13a69..ae8c0019ecc 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart @@ -9,18 +9,15 @@ import 'package:flutter/foundation.dart'; class AuthenticationOptions { /// Constructs a new instance. const AuthenticationOptions({ - this.useErrorDialogs = true, + @Deprecated('This option is no longer supported, and is ignored.') + this.useErrorDialogs = false, this.stickyAuth = false, this.sensitiveTransaction = true, this.biometricOnly = false, }); - /// Whether the system will attempt to handle user-fixable issues encountered - /// while authenticating. For instance, if a fingerprint reader exists on the - /// device but there's no fingerprint registered, the plugin might attempt to - /// take the user to settings to add one. Anything that is not user fixable, - /// such as no biometric sensor on device, will still result in - /// a [PlatformException]. + @Deprecated('This option is no longer supported, and is ignored.') + /// Whether to show native dialogs for some errors. final bool useErrorDialogs; /// Used when the application goes into background for any reason while the diff --git a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart index af249ce151d..a1f85bb58d3 100644 --- a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart +++ b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart @@ -99,7 +99,7 @@ void main() { 'authenticate', arguments: { 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, + 'useErrorDialogs': false, 'stickyAuth': false, 'sensitiveTransaction': true, 'biometricOnly': true, @@ -114,7 +114,6 @@ void main() { localizedReason: 'Insecure', options: const AuthenticationOptions( sensitiveTransaction: false, - useErrorDialogs: false, biometricOnly: true, ), ); @@ -144,7 +143,7 @@ void main() { 'authenticate', arguments: { 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, + 'useErrorDialogs': false, 'stickyAuth': false, 'sensitiveTransaction': true, 'biometricOnly': false, @@ -157,10 +156,7 @@ void main() { await localAuthentication.authenticate( authMessages: [], localizedReason: 'Insecure', - options: const AuthenticationOptions( - sensitiveTransaction: false, - useErrorDialogs: false, - ), + options: const AuthenticationOptions(sensitiveTransaction: false), ); expect(log, [ isMethodCall( From b7cbe2424c7abf6d0b2dcae943d76eb1febeac8c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 9 Sep 2025 09:27:57 -0400 Subject: [PATCH 09/19] More changelog and version updates --- packages/local_auth/local_auth/CHANGELOG.md | 13 +++++++++++-- packages/local_auth/local_auth/pubspec.yaml | 5 ++--- packages/local_auth/local_auth_android/CHANGELOG.md | 4 +++- packages/local_auth/local_auth_darwin/CHANGELOG.md | 4 +++- .../local_auth_platform_interface/CHANGELOG.md | 1 + 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index 80d87dda378..86bf2d7d7b0 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,7 +1,16 @@ -## NEXT +## 3.0.0 * **BREAKING CHANGES:** - * + * Throws `LocalAuthException`s rather than `PlatformException`s for most + failures cases, allowing structured error handling using the specific + `LocalAuthExceptionCode` values. + * Replaces `AuthenticationOptions` in `authenticate` with specific parameters. + * `AuthenticationOptions.stickyAuth` corresponds to + `persistAcrossBackgrounding`. + * `AuthenticationOptions.useErrorDialogs` has no replacement, as specific + error-handling UI should be up to plugin clients to determine. Callers + should use the new structured error codes to detect and handle failure + modes that used to have native dialogs. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. * Updates README to indicate that Andoid SDK <21 is no longer supported. diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index f32f89f86c9..05b89b55c89 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -1,9 +1,8 @@ name: local_auth -description: Flutter plugin for Android and iOS devices to allow local - authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. +description: Flutter plugin to allow local authentication via biometrics, passcode, pin, or pattern. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 2.3.0 +version: 3.0.0 environment: sdk: ^3.7.0 diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index 8d94402c231..a0a1b2a22d0 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,6 +1,8 @@ ## 2.0.0 -* Switches to `LocalAuthException` for error reporting. +* **BREAKING CHANGES:** + * Switches to `LocalAuthException` for error reporting. + * Removes support for `useErrorDialogs`. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.0.51 diff --git a/packages/local_auth/local_auth_darwin/CHANGELOG.md b/packages/local_auth/local_auth_darwin/CHANGELOG.md index bcbb1ba39c8..082b91c9a5b 100644 --- a/packages/local_auth/local_auth_darwin/CHANGELOG.md +++ b/packages/local_auth/local_auth_darwin/CHANGELOG.md @@ -1,6 +1,8 @@ ## 2.0.0 -* Switches to `LocalAuthException` for error reporting. +* **BREAKING CHANGES:** + * Switches to `LocalAuthException` for error reporting. + * Removes support for `useErrorDialogs`. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.6.0 diff --git a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md index 98be5f7598d..f941287c99f 100644 --- a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md +++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md @@ -2,6 +2,7 @@ * Adds `LocalAuthException` to allow for consistent, structured exceptions across platform implementations. +* Deprecates `useErrorDialogs`. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.0.10 From 48974631399604e02c31444954df933c5c7b20ff Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 9 Sep 2025 09:43:08 -0400 Subject: [PATCH 10/19] Rename Android biometricHint --- .../local_auth_android/CHANGELOG.md | 1 + .../localauth/AuthenticationHelper.java | 2 +- .../flutter/plugins/localauth/Messages.java | 30 +++++++++---------- .../localauth/AuthenticationHelperTest.java | 2 +- .../plugins/localauth/LocalAuthTest.java | 2 +- .../lib/local_auth_android.dart | 2 +- .../lib/src/auth_messages_android.dart | 19 ++++++------ .../lib/src/messages.g.dart | 8 ++--- .../local_auth_android/pigeons/messages.dart | 4 +-- .../test/local_auth_test.dart | 15 ++++------ 10 files changed, 42 insertions(+), 43 deletions(-) diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index a0a1b2a22d0..00a4c5b8663 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -3,6 +3,7 @@ * **BREAKING CHANGES:** * Switches to `LocalAuthException` for error reporting. * Removes support for `useErrorDialogs`. + * Renames `biometricHint` to `signInHint` to reflect its usage. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.0.51 diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java index 810fd557761..573da77cca4 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java @@ -62,7 +62,7 @@ interface AuthCompletionHandler { new BiometricPrompt.PromptInfo.Builder() .setDescription(strings.getReason()) .setTitle(strings.getSignInTitle()) - .setSubtitle(strings.getBiometricHint()) + .setSubtitle(strings.getSignInHint()) .setConfirmationRequired(options.getSensitiveTransaction()); int allowedAuthenticators = diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java index 4f52140ed14..7aea2e4ea08 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java @@ -146,17 +146,17 @@ public void setReason(@NonNull String setterArg) { this.reason = setterArg; } - private @NonNull String biometricHint; + private @NonNull String signInHint; - public @NonNull String getBiometricHint() { - return biometricHint; + public @NonNull String getSignInHint() { + return signInHint; } - public void setBiometricHint(@NonNull String setterArg) { + public void setSignInHint(@NonNull String setterArg) { if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"biometricHint\" is null."); + throw new IllegalStateException("Nonnull field \"signInHint\" is null."); } - this.biometricHint = setterArg; + this.signInHint = setterArg; } private @NonNull String cancelButton; @@ -198,14 +198,14 @@ public boolean equals(Object o) { } AuthStrings that = (AuthStrings) o; return reason.equals(that.reason) - && biometricHint.equals(that.biometricHint) + && signInHint.equals(that.signInHint) && cancelButton.equals(that.cancelButton) && signInTitle.equals(that.signInTitle); } @Override public int hashCode() { - return Objects.hash(reason, biometricHint, cancelButton, signInTitle); + return Objects.hash(reason, signInHint, cancelButton, signInTitle); } public static final class Builder { @@ -218,11 +218,11 @@ public static final class Builder { return this; } - private @Nullable String biometricHint; + private @Nullable String signInHint; @CanIgnoreReturnValue - public @NonNull Builder setBiometricHint(@NonNull String setterArg) { - this.biometricHint = setterArg; + public @NonNull Builder setSignInHint(@NonNull String setterArg) { + this.signInHint = setterArg; return this; } @@ -245,7 +245,7 @@ public static final class Builder { public @NonNull AuthStrings build() { AuthStrings pigeonReturn = new AuthStrings(); pigeonReturn.setReason(reason); - pigeonReturn.setBiometricHint(biometricHint); + pigeonReturn.setSignInHint(signInHint); pigeonReturn.setCancelButton(cancelButton); pigeonReturn.setSignInTitle(signInTitle); return pigeonReturn; @@ -256,7 +256,7 @@ public static final class Builder { ArrayList toList() { ArrayList toListResult = new ArrayList<>(4); toListResult.add(reason); - toListResult.add(biometricHint); + toListResult.add(signInHint); toListResult.add(cancelButton); toListResult.add(signInTitle); return toListResult; @@ -266,8 +266,8 @@ ArrayList toList() { AuthStrings pigeonResult = new AuthStrings(); Object reason = pigeonVar_list.get(0); pigeonResult.setReason((String) reason); - Object biometricHint = pigeonVar_list.get(1); - pigeonResult.setBiometricHint((String) biometricHint); + Object signInHint = pigeonVar_list.get(1); + pigeonResult.setSignInHint((String) signInHint); Object cancelButton = pigeonVar_list.get(2); pigeonResult.setCancelButton((String) cancelButton); Object signInTitle = pigeonVar_list.get(3); diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java index dcb9ce4952a..a28d13fa954 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java @@ -29,7 +29,7 @@ public class AuthenticationHelperTest { static final AuthStrings dummyStrings = new AuthStrings.Builder() .setReason("a reason") - .setBiometricHint("a hint") + .setSignInHint("a hint") .setCancelButton("cancel") .setSignInTitle("sign in") .build(); diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java index de199713a1c..d2eb57a43b1 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -47,7 +47,7 @@ public class LocalAuthTest { static final AuthStrings dummyStrings = new AuthStrings.Builder() .setReason("a reason") - .setBiometricHint("a hint") + .setSignInHint("a hint") .setCancelButton("cancel") .setSignInTitle("sign in") .build(); diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart index 90b22dec536..8eadbe938fc 100644 --- a/packages/local_auth/local_auth_android/lib/local_auth_android.dart +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart @@ -149,7 +149,7 @@ class LocalAuthAndroid extends LocalAuthPlatform { } return AuthStrings( reason: localizedReason, - biometricHint: messages?.biometricHint ?? androidBiometricHint, + signInHint: messages?.signInHint ?? androidSignInHint, cancelButton: messages?.cancelButton ?? androidCancelButton, signInTitle: messages?.signInTitle ?? androidSignInTitle, ); diff --git a/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart b/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart index fa623d82661..9331e9ab0ef 100644 --- a/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart +++ b/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart @@ -13,14 +13,14 @@ import 'package:local_auth_platform_interface/types/auth_messages.dart'; class AndroidAuthMessages extends AuthMessages { /// Constructs a new instance. const AndroidAuthMessages({ - this.biometricHint, + this.signInHint, this.cancelButton, this.signInTitle, }); - /// Hint message advising the user how to authenticate with biometrics. + /// Hint message advising the user how to authenticate. /// Maximum 60 characters. - final String? biometricHint; + final String? signInHint; /// Message shown on a button that the user can click to leave the /// current dialog. @@ -35,7 +35,8 @@ class AndroidAuthMessages extends AuthMessages { @override Map get args { return { - 'biometricHint': biometricHint ?? androidBiometricHint, + // This legacy key is kept for backwards compatibility. + 'biometricHint': signInHint ?? androidSignInHint, 'cancelButton': cancelButton ?? androidCancelButton, 'signInTitle': signInTitle ?? androidSignInTitle, }; @@ -46,23 +47,23 @@ class AndroidAuthMessages extends AuthMessages { identical(this, other) || other is AndroidAuthMessages && runtimeType == other.runtimeType && - biometricHint == other.biometricHint && + signInHint == other.signInHint && cancelButton == other.cancelButton && signInTitle == other.signInTitle; @override int get hashCode => - Object.hash(super.hashCode, biometricHint, cancelButton, signInTitle); + Object.hash(super.hashCode, signInHint, cancelButton, signInTitle); } // Default strings for AndroidAuthMessages. Currently supports English. // Intl.message must be string literals. -/// Hint message advising the user how to authenticate with biometrics. -String get androidBiometricHint => Intl.message( +/// Hint message advising the user how to authenticate . +String get androidSignInHint => Intl.message( 'Verify identity', desc: - 'Hint message advising the user how to authenticate with biometrics. ' + 'Hint message advising the user how to authenticate. ' 'Maximum 60 characters.', ); diff --git a/packages/local_auth/local_auth_android/lib/src/messages.g.dart b/packages/local_auth/local_auth_android/lib/src/messages.g.dart index bbf8c67eb61..02cf1406459 100644 --- a/packages/local_auth/local_auth_android/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_android/lib/src/messages.g.dart @@ -85,28 +85,28 @@ enum AuthClassification { weak, strong } class AuthStrings { AuthStrings({ required this.reason, - required this.biometricHint, + required this.signInHint, required this.cancelButton, required this.signInTitle, }); String reason; - String biometricHint; + String signInHint; String cancelButton; String signInTitle; Object encode() { - return [reason, biometricHint, cancelButton, signInTitle]; + return [reason, signInHint, cancelButton, signInTitle]; } static AuthStrings decode(Object result) { result as List; return AuthStrings( reason: result[0]! as String, - biometricHint: result[1]! as String, + signInHint: result[1]! as String, cancelButton: result[2]! as String, signInTitle: result[3]! as String, ); diff --git a/packages/local_auth/local_auth_android/pigeons/messages.dart b/packages/local_auth/local_auth_android/pigeons/messages.dart index 2916594b130..f4b696f828b 100644 --- a/packages/local_auth/local_auth_android/pigeons/messages.dart +++ b/packages/local_auth/local_auth_android/pigeons/messages.dart @@ -19,13 +19,13 @@ class AuthStrings { /// Constructs a new instance. const AuthStrings({ required this.reason, - required this.biometricHint, + required this.signInHint, required this.cancelButton, required this.signInTitle, }); final String reason; - final String biometricHint; + final String signInHint; final String cancelButton; final String signInTitle; } diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart index 2b83bfedde4..7b4c02890b2 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -107,7 +107,7 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_android.dart - expect(strings.biometricHint, androidBiometricHint); + expect(strings.signInHint, androidSignInHint); expect(strings.cancelButton, androidCancelButton); expect(strings.signInTitle, androidSignInTitle); }); @@ -132,7 +132,7 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_android.dart - expect(strings.biometricHint, androidBiometricHint); + expect(strings.signInHint, androidSignInHint); expect(strings.cancelButton, androidCancelButton); expect(strings.signInTitle, androidSignInTitle); }, @@ -154,7 +154,7 @@ void main() { localizedReason: reason, authMessages: [ const AndroidAuthMessages( - biometricHint: hint, + signInHint: hint, cancelButton: cancel, signInTitle: signInTitle, ), @@ -167,7 +167,7 @@ void main() { ); final AuthStrings strings = result.captured[0] as AuthStrings; expect(strings.reason, reason); - expect(strings.biometricHint, hint); + expect(strings.signInHint, hint); expect(strings.cancelButton, cancel); expect(strings.signInTitle, signInTitle); }); @@ -186,10 +186,7 @@ void main() { await plugin.authenticate( localizedReason: reason, authMessages: [ - const AndroidAuthMessages( - biometricHint: hint, - cancelButton: cancel, - ), + const AndroidAuthMessages(signInHint: hint, cancelButton: cancel), ], ); @@ -199,7 +196,7 @@ void main() { final AuthStrings strings = result.captured[0] as AuthStrings; expect(strings.reason, reason); // These should all be the provided values. - expect(strings.biometricHint, hint); + expect(strings.signInHint, hint); expect(strings.cancelButton, cancel); // These were non set, so should all be the default values from // auth_messages_android.dart From 73621826274e35f37f0cdcfe6a3dfc7bde09ba5e Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 9 Sep 2025 09:53:16 -0400 Subject: [PATCH 11/19] Throw structured exception from getEnrolledBiometrics --- .../plugins/localauth/LocalAuthPlugin.java | 3 +++ .../io/flutter/plugins/localauth/Messages.java | 5 ++++- .../flutter/plugins/localauth/LocalAuthTest.java | 8 ++++++++ .../lib/local_auth_android.dart | 8 +++++++- .../local_auth_android/lib/src/messages.g.dart | 14 ++++++-------- .../local_auth_android/pigeons/messages.dart | 5 ++++- .../local_auth_android/test/local_auth_test.dart | 15 +++++++++++++++ .../test/local_auth_test.mocks.dart | 8 +++----- 8 files changed, 50 insertions(+), 16 deletions(-) diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index be100b87db8..b5d4462ad3f 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -65,6 +65,9 @@ public LocalAuthPlugin() {} @Override public @NonNull List getEnrolledBiometrics() { + if (biometricManager == null) { + return null; + } ArrayList biometrics = new ArrayList<>(); if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS) { diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java index 7aea2e4ea08..4c12a56ba00 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java @@ -586,8 +586,11 @@ public interface LocalAuthApi { Boolean stopAuthentication(); /** * Returns the biometric types that are enrolled, and can thus be used without additional setup. + * + *

Returns null if there is no activity, in which case the enrolled biometrics can't be + * determined. */ - @NonNull + @Nullable List getEnrolledBiometrics(); /** * Attempts to authenticate the user with the provided [options], and using [strings] for any diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java index d2eb57a43b1..044501c38bd 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -274,6 +274,14 @@ public void onDetachedFromActivity_ShouldReleaseActivity() { assertNull(plugin.getActivity()); } + @Test + public void getEnrolledBiometrics_shouldReturnNullForNoActivity() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + + final List enrolled = plugin.getEnrolledBiometrics(); + assertNull(enrolled); + } + @Test public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent() { final LocalAuthPlugin plugin = new LocalAuthPlugin(); diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart index 8eadbe938fc..e45c61a3a23 100644 --- a/packages/local_auth/local_auth_android/lib/local_auth_android.dart +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart @@ -120,7 +120,13 @@ class LocalAuthAndroid extends LocalAuthPlatform { @override Future> getEnrolledBiometrics() async { - final List result = await _api.getEnrolledBiometrics(); + final List? result = await _api.getEnrolledBiometrics(); + if (result == null) { + throw const LocalAuthException( + code: LocalAuthExceptionCode.uiUnavailable, + description: 'No Activity available.', + ); + } return result.map((AuthClassification value) { switch (value) { case AuthClassification.weak: diff --git a/packages/local_auth/local_auth_android/lib/src/messages.g.dart b/packages/local_auth/local_auth_android/lib/src/messages.g.dart index 02cf1406459..b443d686390 100644 --- a/packages/local_auth/local_auth_android/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_android/lib/src/messages.g.dart @@ -323,7 +323,10 @@ class LocalAuthApi { /// Returns the biometric types that are enrolled, and can thus be used /// without additional setup. - Future> getEnrolledBiometrics() async { + /// + /// Returns null if there is no activity, in which case the enrolled + /// biometrics can't be determined. + Future?> getEnrolledBiometrics() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_android.LocalAuthApi.getEnrolledBiometrics$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -342,14 +345,9 @@ class LocalAuthApi { message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); } else { - return (pigeonVar_replyList[0] as List?)! - .cast(); + return (pigeonVar_replyList[0] as List?) + ?.cast(); } } diff --git a/packages/local_auth/local_auth_android/pigeons/messages.dart b/packages/local_auth/local_auth_android/pigeons/messages.dart index f4b696f828b..131c5a9f8d3 100644 --- a/packages/local_auth/local_auth_android/pigeons/messages.dart +++ b/packages/local_auth/local_auth_android/pigeons/messages.dart @@ -130,7 +130,10 @@ abstract class LocalAuthApi { /// Returns the biometric types that are enrolled, and can thus be used /// without additional setup. - List getEnrolledBiometrics(); + /// + /// Returns null if there is no activity, in which case the enrolled + /// biometrics can't be determined. + List? getEnrolledBiometrics(); /// Attempts to authenticate the user with the provided [options], and using /// [strings] for any UI. diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart index 7b4c02890b2..50c6988c06d 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -85,6 +85,21 @@ void main() { expect(result, []); }); + + test('throws no UI for null', () async { + when(api.getEnrolledBiometrics()).thenAnswer((_) async => null); + + expect( + () async => plugin.getEnrolledBiometrics(), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ), + ), + ); + }); }); group('authenticate', () { diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart b/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart index 2d0f81304c3..64d3e903231 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart @@ -72,14 +72,12 @@ class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi { as _i4.Future); @override - _i4.Future> getEnrolledBiometrics() => + _i4.Future?> getEnrolledBiometrics() => (super.noSuchMethod( Invocation.method(#getEnrolledBiometrics, []), - returnValue: _i4.Future>.value( - <_i2.AuthClassification>[], - ), + returnValue: _i4.Future?>.value(), ) - as _i4.Future>); + as _i4.Future?>); @override _i4.Future<_i2.AuthResult> authenticate( From 359be0c66efe0341159ab3fbf9f919b848f91814 Mon Sep 17 00:00:00 2001 From: stuartmorgan-g Date: Thu, 11 Sep 2025 10:53:13 -0400 Subject: [PATCH 12/19] Apply Gemini error formatting suggestions in app-facing package Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/local_auth/local_auth/example/lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/local_auth/local_auth/example/lib/main.dart b/packages/local_auth/local_auth/example/lib/main.dart index d2867441b07..cee3b23bcd3 100644 --- a/packages/local_auth/local_auth/example/lib/main.dart +++ b/packages/local_auth/local_auth/example/lib/main.dart @@ -97,7 +97,7 @@ class _MyAppState extends State { _isAuthenticating = false; if (e.code != LocalAuthExceptionCode.userCanceled && e.code != LocalAuthExceptionCode.systemCanceled) { - _authorized = 'Error - ${e.code.name}: ${e.description}'; + _authorized = 'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}'; } }); return; @@ -141,7 +141,7 @@ class _MyAppState extends State { _isAuthenticating = false; if (e.code != LocalAuthExceptionCode.userCanceled && e.code != LocalAuthExceptionCode.systemCanceled) { - _authorized = 'Error - ${e.code.name}: ${e.description}'; + _authorized = 'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}'; } }); return; From 5d23e5c7c29209c137f3d0366860f89dedce65ad Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 11 Sep 2025 10:54:59 -0400 Subject: [PATCH 13/19] autoformat Gemini changes --- packages/local_auth/local_auth/example/lib/main.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/local_auth/local_auth/example/lib/main.dart b/packages/local_auth/local_auth/example/lib/main.dart index cee3b23bcd3..3f10edb8458 100644 --- a/packages/local_auth/local_auth/example/lib/main.dart +++ b/packages/local_auth/local_auth/example/lib/main.dart @@ -97,7 +97,8 @@ class _MyAppState extends State { _isAuthenticating = false; if (e.code != LocalAuthExceptionCode.userCanceled && e.code != LocalAuthExceptionCode.systemCanceled) { - _authorized = 'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}'; + _authorized = + 'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}'; } }); return; @@ -141,7 +142,8 @@ class _MyAppState extends State { _isAuthenticating = false; if (e.code != LocalAuthExceptionCode.userCanceled && e.code != LocalAuthExceptionCode.systemCanceled) { - _authorized = 'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}'; + _authorized = + 'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}'; } }); return; From d6dc7838221ee0ba07cc12618f23a17a5c2022a8 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 16 Sep 2025 14:52:33 -0400 Subject: [PATCH 14/19] Typo fixes --- packages/local_auth/local_auth/lib/src/local_auth.dart | 7 ++++--- .../lib/types/auth_exception.dart | 4 ++-- .../local_auth/local_auth_windows/lib/src/messages.g.dart | 2 +- .../local_auth/local_auth_windows/pigeons/messages.dart | 2 +- .../local_auth/local_auth_windows/windows/messages.g.h | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/local_auth/local_auth/lib/src/local_auth.dart b/packages/local_auth/local_auth/lib/src/local_auth.dart index 7f1bfc3ba44..f5584bfec00 100644 --- a/packages/local_auth/local_auth/lib/src/local_auth.dart +++ b/packages/local_auth/local_auth/lib/src/local_auth.dart @@ -41,9 +41,10 @@ class LocalAuthentication { /// unlock their device. /// /// On mobile platforms, authentication may be stopped by the system when the - /// is backgrounded during an authentication. Set [persistAcrossBackgrounding] - /// to true to have the plugin automatically retry the authentication on - /// foregrounding instead of failing with an error on backgrounding. + /// app is backgrounded during an authentication. Set + /// [persistAcrossBackgrounding] to true to have the plugin automatically + /// retry the authentication on foregrounding instead of failing with an error + /// on backgrounding. Future authenticate({ required String localizedReason, Iterable authMessages = const [ diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart index d6d148a0250..946da6f5743 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart @@ -4,11 +4,11 @@ import 'package:flutter/foundation.dart'; -/// An exception thrown by the plugin when there is authenication failure, or +/// An exception thrown by the plugin when there is authentication failure, or /// some other error. @immutable class LocalAuthException implements Exception { - /// Crceates a new exception with the given information. + /// Creates a new exception with the given information. const LocalAuthException({ required this.code, this.description, diff --git a/packages/local_auth/local_auth_windows/lib/src/messages.g.dart b/packages/local_auth/local_auth_windows/lib/src/messages.g.dart index 16dc3e1d8bc..72ea86f5705 100644 --- a/packages/local_auth/local_auth_windows/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_windows/lib/src/messages.g.dart @@ -35,7 +35,7 @@ enum AuthResult { /// The biometric hardware is currently in use. deviceBusy, - /// Device poilcy does not allow using the authentication system. + /// Device policy does not allow using the authentication system. disabledByPolicy, /// Authentication is unavailable for an unknown reason. diff --git a/packages/local_auth/local_auth_windows/pigeons/messages.dart b/packages/local_auth/local_auth_windows/pigeons/messages.dart index 1261657f411..da7afee8f5c 100644 --- a/packages/local_auth/local_auth_windows/pigeons/messages.dart +++ b/packages/local_auth/local_auth_windows/pigeons/messages.dart @@ -30,7 +30,7 @@ enum AuthResult { /// The biometric hardware is currently in use. deviceBusy, - /// Device poilcy does not allow using the authentication system. + /// Device policy does not allow using the authentication system. disabledByPolicy, /// Authentication is unavailable for an unknown reason. diff --git a/packages/local_auth/local_auth_windows/windows/messages.g.h b/packages/local_auth/local_auth_windows/windows/messages.g.h index 7ba94942815..07cc72f5d63 100644 --- a/packages/local_auth/local_auth_windows/windows/messages.g.h +++ b/packages/local_auth/local_auth_windows/windows/messages.g.h @@ -70,7 +70,7 @@ enum class AuthResult { kNotEnrolled = 3, // The biometric hardware is currently in use. kDeviceBusy = 4, - // Device poilcy does not allow using the authentication system. + // Device policy does not allow using the authentication system. kDisabledByPolicy = 5, // Authentication is unavailable for an unknown reason. kUnavailable = 6 From f11d2be6340a769066ed8aee710ef95fd0e9a941 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 24 Sep 2025 11:19:59 -0400 Subject: [PATCH 15/19] Update for adjustment to auth_options --- .../local_auth/local_auth/lib/src/local_auth.dart | 3 +++ .../lib/types/auth_options.dart | 14 ++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/local_auth/local_auth/lib/src/local_auth.dart b/packages/local_auth/local_auth/lib/src/local_auth.dart index f5584bfec00..0e4d34011c5 100644 --- a/packages/local_auth/local_auth/lib/src/local_auth.dart +++ b/packages/local_auth/local_auth/lib/src/local_auth.dart @@ -63,6 +63,9 @@ class LocalAuthentication { stickyAuth: persistAcrossBackgrounding, biometricOnly: biometricOnly, sensitiveTransaction: sensitiveTransaction, + // This is a legacy option; implementations compatible with 3.x plus + // should always assume this is false, so set it accordingly. + useErrorDialogs: false, ), ); } diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart index ae8c0019ecc..24382b9a5bb 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart @@ -9,15 +9,21 @@ import 'package:flutter/foundation.dart'; class AuthenticationOptions { /// Constructs a new instance. const AuthenticationOptions({ - @Deprecated('This option is no longer supported, and is ignored.') - this.useErrorDialogs = false, + this.useErrorDialogs = true, this.stickyAuth = false, this.sensitiveTransaction = true, this.biometricOnly = false, }); - @Deprecated('This option is no longer supported, and is ignored.') - /// Whether to show native dialogs for some errors. + /// Whether the system will attempt to handle user-fixable issues encountered + /// while authenticating. For instance, if a fingerprint reader exists on the + /// device but there's no fingerprint registered, the plugin might attempt to + /// take the user to settings to add one. Anything that is not user fixable, + /// such as no biometric sensor on device, will still result in + /// a [PlatformException]. + // This parameter still exists for backwards compatibility with local_auth + // 2.x, but implementers targetting local_auth 3.x or later should ignore it, + // as it will always be false. final bool useErrorDialogs; /// Used when the application goes into background for any reason while the From 4e99fdd4379fd174bc9040004d475f5eef012910 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 30 Sep 2025 16:49:37 -0400 Subject: [PATCH 16/19] Sync with landed version of platform interface --- .../CHANGELOG.md | 1 - .../local_auth_platform_interface/LICENSE | 2 +- .../lib/default_method_channel_platform.dart | 2 +- .../lib/local_auth_platform_interface.dart | 2 +- .../lib/types/auth_exception.dart | 5 +-- .../lib/types/auth_messages.dart | 2 +- .../lib/types/auth_options.dart | 4 +-- .../lib/types/biometric_type.dart | 2 +- .../lib/types/types.dart | 2 +- .../default_method_channel_platform_test.dart | 33 ++++++++++++++++--- 10 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md index f941287c99f..98be5f7598d 100644 --- a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md +++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md @@ -2,7 +2,6 @@ * Adds `LocalAuthException` to allow for consistent, structured exceptions across platform implementations. -* Deprecates `useErrorDialogs`. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.0.10 diff --git a/packages/local_auth/local_auth_platform_interface/LICENSE b/packages/local_auth/local_auth_platform_interface/LICENSE index c6823b81eb8..29b709dac6c 100644 --- a/packages/local_auth/local_auth_platform_interface/LICENSE +++ b/packages/local_auth/local_auth_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2013 The Flutter Authors. All rights reserved. +Copyright 2013 The Flutter Authors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart b/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart index 67f3f2e040e..822b3573046 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart @@ -1,4 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart index b3d8e96d68f..a4a08717761 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart @@ -1,4 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart index 946da6f5743..dc751726a66 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_exception.dart @@ -1,4 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -25,7 +25,8 @@ class LocalAuthException implements Exception { final Object? details; @override - String toString() => 'LocalAuthException(code $code, $description, $details)'; + String toString() => + '${objectRuntimeType(this, 'LocalAuthException')}(code ${code.name}, $description, $details)'; } /// Types of [LocalAuthException]s, as indicated by [LocalAuthException.code]. diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_messages.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_messages.dart index d51980d575c..0646ed9e8b7 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/auth_messages.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_messages.dart @@ -1,4 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart index 24382b9a5bb..ad1b4e1e6db 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart @@ -1,4 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -22,7 +22,7 @@ class AuthenticationOptions { /// such as no biometric sensor on device, will still result in /// a [PlatformException]. // This parameter still exists for backwards compatibility with local_auth - // 2.x, but implementers targetting local_auth 3.x or later should ignore it, + // 2.x, but implementers targeting local_auth 3.x or later should ignore it, // as it will always be false. final bool useErrorDialogs; diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/biometric_type.dart b/packages/local_auth/local_auth_platform_interface/lib/types/biometric_type.dart index 9c335e25624..48881c859fd 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/biometric_type.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/biometric_type.dart @@ -1,4 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/types.dart b/packages/local_auth/local_auth_platform_interface/lib/types/types.dart index c8710b663a2..05c65e3a1e7 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/types.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/types.dart @@ -1,4 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart index a1f85bb58d3..fdc76a62247 100644 --- a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart +++ b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart @@ -1,4 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -99,7 +99,7 @@ void main() { 'authenticate', arguments: { 'localizedReason': 'Needs secure', - 'useErrorDialogs': false, + 'useErrorDialogs': true, 'stickyAuth': false, 'sensitiveTransaction': true, 'biometricOnly': true, @@ -122,7 +122,7 @@ void main() { 'authenticate', arguments: { 'localizedReason': 'Insecure', - 'useErrorDialogs': false, + 'useErrorDialogs': true, 'stickyAuth': false, 'sensitiveTransaction': false, 'biometricOnly': true, @@ -143,7 +143,7 @@ void main() { 'authenticate', arguments: { 'localizedReason': 'Needs secure', - 'useErrorDialogs': false, + 'useErrorDialogs': true, 'stickyAuth': false, 'sensitiveTransaction': true, 'biometricOnly': false, @@ -163,7 +163,7 @@ void main() { 'authenticate', arguments: { 'localizedReason': 'Insecure', - 'useErrorDialogs': false, + 'useErrorDialogs': true, 'stickyAuth': false, 'sensitiveTransaction': false, 'biometricOnly': false, @@ -171,6 +171,29 @@ void main() { ), ]); }); + + test( + 'legacy useErrorDialogs is passed for backward compatibility.', + () async { + await localAuthentication.authenticate( + authMessages: [], + localizedReason: 'Insecure', + options: const AuthenticationOptions(useErrorDialogs: false), + ); + expect(log, [ + isMethodCall( + 'authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }, + ), + ]); + }, + ); }); }); } From 859b046764f2dc8c76aa6c2848cb2773dff1c824 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 30 Sep 2025 16:53:17 -0400 Subject: [PATCH 17/19] iOS README update --- packages/local_auth/local_auth/CHANGELOG.md | 3 ++- packages/local_auth/local_auth/README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index 60e2876d32f..c391663da3b 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -12,7 +12,8 @@ should use the new structured error codes to detect and handle failure modes that used to have native dialogs. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. -* Updates README to reflect that only Android API 24+ is supported. +* Updates README to reflect that Android older than API 24 and iOS older than + 13.0 are no longer supported. ## 2.3.0 diff --git a/packages/local_auth/local_auth/README.md b/packages/local_auth/local_auth/README.md index 28a12579fcb..02b2d3c95dd 100644 --- a/packages/local_auth/local_auth/README.md +++ b/packages/local_auth/local_auth/README.md @@ -10,7 +10,7 @@ fingerprint or facial recognition. | | Android | iOS | macOS | Windows | |-------------|---------|-------|--------|-------------| -| **Support** | SDK 24+ | 12.0+ | 10.14+ | Windows 10+ | +| **Support** | SDK 24+ | 13.0+ | 10.14+ | Windows 10+ | ## Usage From 3955e48120fc235014213df927b87d89c5c813f1 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 30 Sep 2025 16:53:52 -0400 Subject: [PATCH 18/19] Revert app-facing package --- packages/local_auth/local_auth/CHANGELOG.md | 18 +--- packages/local_auth/local_auth/README.md | 94 ++++++++++++++----- .../local_auth/example/lib/main.dart | 34 ++----- .../example/lib/readme_excerpts.dart | 52 +++++++++- .../local_auth/example/pubspec.yaml | 4 - .../local_auth/lib/error_codes.dart | 29 ++++++ .../local_auth/local_auth/lib/local_auth.dart | 6 +- .../local_auth/lib/src/local_auth.dart | 47 +++------- packages/local_auth/local_auth/pubspec.yaml | 9 +- 9 files changed, 179 insertions(+), 114 deletions(-) create mode 100644 packages/local_auth/local_auth/lib/error_codes.dart diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index c391663da3b..d8cf7652a76 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,19 +1,7 @@ -## 3.0.0 - -* **BREAKING CHANGES:** - * Throws `LocalAuthException`s rather than `PlatformException`s for most - failures cases, allowing structured error handling using the specific - `LocalAuthExceptionCode` values. - * Replaces `AuthenticationOptions` in `authenticate` with specific parameters. - * `AuthenticationOptions.stickyAuth` corresponds to - `persistAcrossBackgrounding`. - * `AuthenticationOptions.useErrorDialogs` has no replacement, as specific - error-handling UI should be up to plugin clients to determine. Callers - should use the new structured error codes to detect and handle failure - modes that used to have native dialogs. +## NEXT + * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. -* Updates README to reflect that Android older than API 24 and iOS older than - 13.0 are no longer supported. +* Updates README to reflect that only Android API 24+ is supported. ## 2.3.0 diff --git a/packages/local_auth/local_auth/README.md b/packages/local_auth/local_auth/README.md index 02b2d3c95dd..e09626472e7 100644 --- a/packages/local_auth/local_auth/README.md +++ b/packages/local_auth/local_auth/README.md @@ -10,7 +10,7 @@ fingerprint or facial recognition. | | Android | iOS | macOS | Windows | |-------------|---------|-------|--------|-------------| -| **Support** | SDK 24+ | 13.0+ | 10.14+ | Windows 10+ | +| **Support** | SDK 24+ | 12.0+ | 10.14+ | Windows 10+ | ## Usage @@ -66,34 +66,72 @@ if (availableBiometrics.contains(BiometricType.strong) || ### Options -#### Requiring Biometrics - The `authenticate()` method uses biometric authentication when possible, but -by default also allows fallback to pin, pattern, or passcode. To require -biometric authentication, set `biometricOnly` to `true`. +also allows fallback to pin, pattern, or passcode. + + +```dart +try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + ); + // ··· +} on PlatformException { + // ... +} +``` + +To require biometric authentication, pass `AuthenticationOptions` with +`biometricOnly` set to `true`. ```dart final bool didAuthenticate = await auth.authenticate( localizedReason: 'Please authenticate to show account balance', - biometricOnly: true, + options: const AuthenticationOptions(biometricOnly: true), ); ``` *Note*: `biometricOnly` is not supported on Windows since the Windows implementation's underlying API (Windows Hello) doesn't support selecting the authentication method. -#### Background Handling +#### Dialogs -On mobile platforms, authentication may be canceled by the system if the app -is backgrounded. This might happen if the user receives a phone call before -they get a chance to authenticate, for example. Setting -`persistAcrossBackgrounding` to true will cause the plugin to instead wait until -the app is foregrounded again, retry the authentication, and only return once -that new attempt completes. +The plugin provides default dialogs for the following cases: -#### Dialog customization +1. Passcode/PIN/Pattern Not Set: The user has not yet configured a passcode on + iOS or PIN/pattern on Android. +2. Biometrics Not Enrolled: The user has not enrolled any biometrics on the + device. + +If a user does not have the necessary authentication enrolled when +`authenticate` is called, they will be given the option to enroll at that point, +or cancel authentication. + +If you don't want to use the default dialogs, set the `useErrorDialogs` option +to `false` to have `authenticate` immediately return an error in those cases. + + +```dart +import 'package:local_auth/error_codes.dart' as auth_error; +// ··· + try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(useErrorDialogs: false), + ); + // ··· + } on PlatformException catch (e) { + if (e.code == auth_error.notAvailable) { + // Add handling of no hardware here. + } else if (e.code == auth_error.notEnrolled) { + // ... + } else { + // ... + } + } +``` -If you want to customize the messages in the system dialogs, you can pass +If you want to customize the messages in the dialogs, you can pass `AuthMessages` for each platform you support. These are platform-specific, so you will need to import the platform-specific implementation packages. For instance, to customize Android and iOS: @@ -120,12 +158,14 @@ each platform. ### Exceptions -`authenticate` throws `LocalAuthException`s in most failure cases. See -`LocalAuthExceptionCodes` for known error codes that you may want to have -specific handling for. For example: +`authenticate` throws `PlatformException`s in many error cases. See +`error_codes.dart` for known error codes that you may want to have specific +handling for. For example: ```dart +import 'package:flutter/services.dart'; +import 'package:local_auth/error_codes.dart' as auth_error; import 'package:local_auth/local_auth.dart'; // ··· final LocalAuthentication auth = LocalAuthentication(); @@ -133,13 +173,14 @@ import 'package:local_auth/local_auth.dart'; try { final bool didAuthenticate = await auth.authenticate( localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(useErrorDialogs: false), ); // ··· - } on LocalAuthException catch (e) { - if (e.code == LocalAuthExceptionCode.noBiometricHardware) { + } on PlatformException catch (e) { + if (e.code == auth_error.notEnrolled) { // Add handling of no hardware here. - } else if (e.code == LocalAuthExceptionCode.temporaryLockout || - e.code == LocalAuthExceptionCode.biometricLockout) { + } else if (e.code == auth_error.lockedOut || + e.code == auth_error.permanentlyLockedOut) { // ... } else { // ... @@ -246,3 +287,12 @@ the Android theme directly in `android/app/src/main/AndroidManifest.xml`: ... ``` + +## Sticky Auth + +You can set the `stickyAuth` option on the plugin to true so that plugin does not +return failure if the app is put to background by the system. This might happen +if the user receives a phone call before they get a chance to authenticate. With +`stickyAuth` set to false, this would result in plugin returning failure result +to the Dart app. If set to true, the plugin will retry authenticating when the +app resumes. diff --git a/packages/local_auth/local_auth/example/lib/main.dart b/packages/local_auth/local_auth/example/lib/main.dart index 470ae92a127..cbc4bac8533 100644 --- a/packages/local_auth/local_auth/example/lib/main.dart +++ b/packages/local_auth/local_auth/example/lib/main.dart @@ -86,27 +86,16 @@ class _MyAppState extends State { }); authenticated = await auth.authenticate( localizedReason: 'Let OS determine authentication method', - persistAcrossBackgrounding: true, + options: const AuthenticationOptions(stickyAuth: true), ); setState(() { _isAuthenticating = false; }); - } on LocalAuthException catch (e) { - print(e); - setState(() { - _isAuthenticating = false; - if (e.code != LocalAuthExceptionCode.userCanceled && - e.code != LocalAuthExceptionCode.systemCanceled) { - _authorized = - 'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}'; - } - }); - return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Unexpected error - ${e.message}'; + _authorized = 'Error - ${e.message}'; }); return; } @@ -129,29 +118,20 @@ class _MyAppState extends State { authenticated = await auth.authenticate( localizedReason: 'Scan your fingerprint (or face or whatever) to authenticate', - persistAcrossBackgrounding: true, - biometricOnly: true, + options: const AuthenticationOptions( + stickyAuth: true, + biometricOnly: true, + ), ); setState(() { _isAuthenticating = false; _authorized = 'Authenticating'; }); - } on LocalAuthException catch (e) { - print(e); - setState(() { - _isAuthenticating = false; - if (e.code != LocalAuthExceptionCode.userCanceled && - e.code != LocalAuthExceptionCode.systemCanceled) { - _authorized = - 'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}'; - } - }); - return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Unexpected Error - ${e.message}'; + _authorized = 'Error - ${e.message}'; }); return; } diff --git a/packages/local_auth/local_auth/example/lib/readme_excerpts.dart b/packages/local_auth/local_auth/example/lib/readme_excerpts.dart index e67fcb40263..9958593ac52 100644 --- a/packages/local_auth/local_auth/example/lib/readme_excerpts.dart +++ b/packages/local_auth/local_auth/example/lib/readme_excerpts.dart @@ -9,6 +9,10 @@ import 'package:flutter/material.dart'; // #docregion ErrorHandling +import 'package:flutter/services.dart'; +// #docregion NoErrorDialogs +import 'package:local_auth/error_codes.dart' as auth_error; +// #enddocregion NoErrorDialogs // #docregion CanCheck import 'package:local_auth/local_auth.dart'; // #enddocregion CanCheck @@ -75,30 +79,68 @@ class _MyAppState extends State { // #enddocregion Enrolled } + Future authenticate() async { + // #docregion AuthAny + try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + ); + // #enddocregion AuthAny + print(didAuthenticate); + // #docregion AuthAny + } on PlatformException { + // ... + } + // #enddocregion AuthAny + } + Future authenticateWithBiometrics() async { // #docregion AuthBioOnly final bool didAuthenticate = await auth.authenticate( localizedReason: 'Please authenticate to show account balance', - biometricOnly: true, + options: const AuthenticationOptions(biometricOnly: true), ); // #enddocregion AuthBioOnly print(didAuthenticate); } + Future authenticateWithoutDialogs() async { + // #docregion NoErrorDialogs + try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(useErrorDialogs: false), + ); + // #enddocregion NoErrorDialogs + print(didAuthenticate ? 'Success!' : 'Failure'); + // #docregion NoErrorDialogs + } on PlatformException catch (e) { + if (e.code == auth_error.notAvailable) { + // Add handling of no hardware here. + } else if (e.code == auth_error.notEnrolled) { + // ... + } else { + // ... + } + } + // #enddocregion NoErrorDialogs + } + Future authenticateWithErrorHandling() async { // #docregion ErrorHandling try { final bool didAuthenticate = await auth.authenticate( localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(useErrorDialogs: false), ); // #enddocregion ErrorHandling print(didAuthenticate ? 'Success!' : 'Failure'); // #docregion ErrorHandling - } on LocalAuthException catch (e) { - if (e.code == LocalAuthExceptionCode.noBiometricHardware) { + } on PlatformException catch (e) { + if (e.code == auth_error.notEnrolled) { // Add handling of no hardware here. - } else if (e.code == LocalAuthExceptionCode.temporaryLockout || - e.code == LocalAuthExceptionCode.biometricLockout) { + } else if (e.code == auth_error.lockedOut || + e.code == auth_error.permanentlyLockedOut) { // ... } else { // ... diff --git a/packages/local_auth/local_auth/example/pubspec.yaml b/packages/local_auth/local_auth/example/pubspec.yaml index e1bab96dd5d..48d1809f2e9 100644 --- a/packages/local_auth/local_auth/example/pubspec.yaml +++ b/packages/local_auth/local_auth/example/pubspec.yaml @@ -28,7 +28,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth/lib/error_codes.dart b/packages/local_auth/local_auth/lib/error_codes.dart new file mode 100644 index 00000000000..9b8e83c63a4 --- /dev/null +++ b/packages/local_auth/local_auth/lib/error_codes.dart @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Exception codes for `PlatformException` returned by +// `authenticate`. + +/// Indicates that the user has not yet configured a passcode (iOS) or +/// PIN/pattern/password (Android) on the device. +const String passcodeNotSet = 'PasscodeNotSet'; + +/// Indicates the user has not enrolled any biometrics on the device. +const String notEnrolled = 'NotEnrolled'; + +/// Indicates the device does not have hardware support for biometrics. +const String notAvailable = 'NotAvailable'; + +/// Indicates the device operating system is unsupported. +const String otherOperatingSystem = 'OtherOperatingSystem'; + +/// Indicates the API is temporarily locked out due to too many attempts. +const String lockedOut = 'LockedOut'; + +/// Indicates the API is locked out more persistently than [lockedOut]. +/// Strong authentication like PIN/Pattern/Password is required to unlock. +const String permanentlyLockedOut = 'PermanentlyLockedOut'; + +/// Indicates that the biometricOnly parameter can't be true on Windows +const String biometricOnlyNotSupported = 'biometricOnlyNotSupported'; diff --git a/packages/local_auth/local_auth/lib/local_auth.dart b/packages/local_auth/local_auth/lib/local_auth.dart index 8a749f339fc..df408b7b5e3 100644 --- a/packages/local_auth/local_auth/lib/local_auth.dart +++ b/packages/local_auth/local_auth/lib/local_auth.dart @@ -3,5 +3,7 @@ // found in the LICENSE file. export 'package:local_auth/src/local_auth.dart' show LocalAuthentication; -export 'package:local_auth_platform_interface/local_auth_platform_interface.dart' - show BiometricType, LocalAuthException, LocalAuthExceptionCode; +export 'package:local_auth_platform_interface/types/auth_options.dart' + show AuthenticationOptions; +export 'package:local_auth_platform_interface/types/biometric_type.dart' + show BiometricType; diff --git a/packages/local_auth/local_auth/lib/src/local_auth.dart b/packages/local_auth/local_auth/lib/src/local_auth.dart index b2eddb82382..5376bef72d8 100644 --- a/packages/local_auth/local_auth/lib/src/local_auth.dart +++ b/packages/local_auth/local_auth/lib/src/local_auth.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:flutter/services.dart'; import 'package:local_auth_android/local_auth_android.dart'; import 'package:local_auth_darwin/local_auth_darwin.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; @@ -20,31 +21,21 @@ class LocalAuthentication { /// Authenticates the user with biometrics available on the device while also /// allowing the user to use device authentication - pin, pattern, passcode. /// - /// Returns true if the user successfully authenticated. - /// - /// If the user fails the authentication challenge without any side effects, - /// returns false. For other other failures cases, throws a - /// [LocalAuthException] with details about the reason it did not succeed. + /// Returns true if the user successfully authenticated, false otherwise. /// /// [localizedReason] is the message to show to user while prompting them /// for authentication. This is typically along the lines of: 'Authenticate /// to access MyApp.'. This must not be empty. /// - /// Provide [authMessages] if you want to customize messages in the dialogs. - /// - /// Set [biometricOnly] to true to prevent authentications from using - /// non-biometric local authentication such as pin, passcode, or pattern. + /// Provide [authMessages] if you want to + /// customize messages in the dialogs. /// - /// [sensitiveTransaction], which defaults to true, controls whether - /// platform-specific precautions are enabled, such as showing a confirmation - /// dialog after face unlock is recognized to make sure the user meant to - /// unlock their device. + /// Provide [options] for configuring further authentication related options. /// - /// On mobile platforms, authentication may be stopped by the system when the - /// app is backgrounded during an authentication. Set - /// [persistAcrossBackgrounding] to true to have the plugin automatically - /// retry the authentication on foregrounding instead of failing with an error - /// on backgrounding. + /// Throws a [PlatformException] if there were technical problems with local + /// authentication (e.g. lack of relevant hardware). This might throw + /// [PlatformException] with error code [otherOperatingSystem] on the iOS + /// simulator. Future authenticate({ required String localizedReason, Iterable authMessages = const [ @@ -52,30 +43,20 @@ class LocalAuthentication { AndroidAuthMessages(), WindowsAuthMessages(), ], - bool biometricOnly = false, - bool sensitiveTransaction = true, - bool persistAcrossBackgrounding = false, + AuthenticationOptions options = const AuthenticationOptions(), }) { return LocalAuthPlatform.instance.authenticate( localizedReason: localizedReason, authMessages: authMessages, - options: AuthenticationOptions( - stickyAuth: persistAcrossBackgrounding, - biometricOnly: biometricOnly, - sensitiveTransaction: sensitiveTransaction, - // This is a legacy option; implementations compatible with 3.x plus - // should always assume this is false, so set it accordingly. - useErrorDialogs: false, - ), + options: options, ); } /// Cancels any in-progress authentication, returning true if auth was - /// canceled successfully. - /// - /// This API may not be supported by all platforms. + /// cancelled successfully. /// - /// Returns false if there was some error, no authentication is in progress, + /// This API is not supported by all platforms. + /// Returns false if there was some error, no authentication in progress, /// or the current platform lacks support. Future stopAuthentication() async { return LocalAuthPlatform.instance.stopAuthentication(); diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index 05b89b55c89..eda0fdd07a8 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -1,8 +1,9 @@ name: local_auth -description: Flutter plugin to allow local authentication via biometrics, passcode, pin, or pattern. +description: Flutter plugin for Android and iOS devices to allow local + authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 3.0.0 +version: 2.3.0 environment: sdk: ^3.7.0 @@ -38,7 +39,3 @@ topics: - authentication - biometrics - local-auth -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} From bb453305534724c96def893f1cd8daa32f5d2990 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 30 Sep 2025 16:57:45 -0400 Subject: [PATCH 19/19] Update interface dependency --- packages/local_auth/local_auth_android/example/pubspec.yaml | 6 +----- packages/local_auth/local_auth_android/pubspec.yaml | 6 +----- packages/local_auth/local_auth_darwin/example/pubspec.yaml | 6 +----- packages/local_auth/local_auth_darwin/pubspec.yaml | 6 +----- packages/local_auth/local_auth_windows/example/pubspec.yaml | 6 +----- packages/local_auth/local_auth_windows/pubspec.yaml | 6 +----- 6 files changed, 6 insertions(+), 30 deletions(-) diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml index 29e4310d617..c273068d84c 100644 --- a/packages/local_auth/local_auth_android/example/pubspec.yaml +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - local_auth_platform_interface: ^1.0.0 + local_auth_platform_interface: ^1.1.0 dev_dependencies: flutter_test: @@ -26,7 +26,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index b89696d7258..d3fd59b04f0 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 intl: ">=0.17.0 <0.21.0" - local_auth_platform_interface: ^1.0.1 + local_auth_platform_interface: ^1.1.0 dev_dependencies: build_runner: ^2.3.3 @@ -35,7 +35,3 @@ topics: - authentication - biometrics - local-auth -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_darwin/example/pubspec.yaml b/packages/local_auth/local_auth_darwin/example/pubspec.yaml index b3e8659e34a..99959c4cb2f 100644 --- a/packages/local_auth/local_auth_darwin/example/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - local_auth_platform_interface: ^1.0.0 + local_auth_platform_interface: ^1.1.0 dev_dependencies: flutter_test: @@ -26,7 +26,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_darwin/pubspec.yaml b/packages/local_auth/local_auth_darwin/pubspec.yaml index dcbeb807a71..1a8fad8a31b 100644 --- a/packages/local_auth/local_auth_darwin/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: flutter: sdk: flutter intl: ">=0.17.0 <0.21.0" - local_auth_platform_interface: ^1.0.1 + local_auth_platform_interface: ^1.1.0 dev_dependencies: build_runner: ^2.3.3 @@ -38,7 +38,3 @@ topics: - authentication - biometrics - local-auth -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml index ea37698ad85..1cc184db6a3 100644 --- a/packages/local_auth/local_auth_windows/example/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - local_auth_platform_interface: ^1.0.0 + local_auth_platform_interface: ^1.1.0 local_auth_windows: # When depending on this package from a real application you should use: # local_auth_windows: ^x.y.z @@ -26,7 +26,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index b4e6b715e8b..7dedea02fe2 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - local_auth_platform_interface: ^1.0.1 + local_auth_platform_interface: ^1.1.0 dev_dependencies: flutter_test: @@ -30,7 +30,3 @@ topics: - authentication - biometrics - local-auth -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface}