diff --git a/packages/local_auth/local_auth/example/pubspec.yaml b/packages/local_auth/local_auth/example/pubspec.yaml index f39c76d15a71..0bd660845642 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_darwin: {path: ../../../../packages/local_auth/local_auth_darwin} diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index bf35d018e959..79a16c3ea133 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/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_darwin: {path: ../../../packages/local_auth/local_auth_darwin} diff --git a/packages/local_auth/local_auth_darwin/CHANGELOG.md b/packages/local_auth/local_auth_darwin/CHANGELOG.md index d5a5766e0f71..b639c2753845 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.1.0 +* Add `stopAuthentication` implementation. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 2.0.3 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 492a949dacd1..2b75d6f4d2c1 100644 --- a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift +++ b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift @@ -48,6 +48,9 @@ final class StubAuthContext: NSObject, AuthContext, @unchecked Sendable { var biometryType: LABiometryType = .none var localizedFallbackTitle: String? + /// Tracks if invalidate() was called during the test. + var invalidated = false + func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool { #expect( policy @@ -75,6 +78,8 @@ final class StubAuthContext: NSObject, AuthContext, @unchecked Sendable { reply(self.evaluateResponse, self.evaluateError) } } + + func invalidate() { invalidated = true } } // MARK: - @@ -454,6 +459,53 @@ struct LocalAuthPluginTests { #expect(!result) } + @Test + func stopAuthenticationHandlesNoActiveContext() async { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let result = await withCheckedContinuation { continuation in + plugin.stopAuthentication { response in + switch response { + case .success(let val): + continuation.resume(returning: val) + case .failure: + continuation.resume(returning: false) + } + } + } + + #expect(!result) + #expect(!stubAuthContext.invalidated) + } + + @Test + func stopAuthenticationHandlesActiveContext() async { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: createAuthStrings() + ) { _ in } + + let result = await withCheckedContinuation { continuation in + plugin.stopAuthentication { response in + switch response { + case .success(let val): + continuation.resume(returning: val) + case .failure: + continuation.resume(returning: false) + } + } + } + + #expect(result) + #expect(stubAuthContext.invalidated) + } + // Creates an AuthStrings with placeholder values. func createAuthStrings(localizedFallbackTitle: String? = nil) -> AuthStrings { return AuthStrings( 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 e93b1977f3c5..d3ca59e6435b 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 @@ -39,6 +39,8 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch private let authContextFactory: AuthContextFactory /// Manages the last call state for sticky auth. private var lastCallState: StickyAuthState? + /// The active context for the current authentication session. + private var activeContext: AuthContext? public static func register(with registrar: FlutterPluginRegistrar) { let instance = LocalAuthPlugin( @@ -74,6 +76,7 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch completion: @escaping (Result) -> Void ) { var context = authContextFactory.createAuthContext() + activeContext = context lastCallState = nil context.localizedFallbackTitle = strings.localizedFallbackTitle @@ -88,6 +91,11 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch localizedReason: strings.reason ) { [weak self] (success: Bool, error: Error?) in DispatchQueue.main.async { + if let currentActive = self?.activeContext, + currentActive as AnyObject === context as AnyObject + { + self?.activeContext = nil + } self?.handleAuthReply( success: success, error: error, @@ -97,6 +105,7 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch } } } else { + activeContext = nil if let authError = authError { self.handleError(authError, options: options, strings: strings, completion: completion) } else { @@ -159,6 +168,17 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch return context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) } + func stopAuthentication(completion: @escaping (Result) -> Void) { + guard let context = activeContext else { + completion(.success(false)) + return + } + + context.invalidate() + activeContext = nil + completion(.success(true)) + } + // MARK: Private Methods private func handleAuthReply( 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 f9bec200ba61..f5122488080c 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 @@ -33,6 +33,9 @@ protocol AuthContext { localizedReason: String, reply: @escaping @Sendable (Bool, Error?) -> Void ) + + /// Direct passthrough to LAContext's invalidate. + func invalidate() } /// AuthContext is intentionally a direct passthroguh to LAContext. 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 85805a809169..0e2b5cecab0c 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 // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -53,7 +53,7 @@ private func wrapError(_ error: Any) -> [Any?] { } return [ "\(error)", - "\(type(of: error))", + "\(Swift.type(of: error))", "Stacktrace: \(Thread.callStackSymbols)", ] } @@ -67,6 +67,19 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } +private func doubleEqualsmessages(_ lhs: Double, _ rhs: Double) -> Bool { + return (lhs.isNaN && rhs.isNaN) || lhs == rhs +} + +private func doubleHashmessages(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8_0000_0000_0000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } +} + func deepEqualsmessages(_ lhs: Any?, _ rhs: Any?) -> Bool { let cleanLhs = nilOrValue(lhs) as Any? let cleanRhs = nilOrValue(rhs) as Any? @@ -77,56 +90,90 @@ func deepEqualsmessages(_ lhs: Any?, _ rhs: Any?) -> Bool { case (nil, _), (_, nil): return false - case is (Void, Void): + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: return true - case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): - return cleanLhsHashable == cleanRhsHashable + case is (Void, Void): + return true - case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): - guard cleanLhsArray.count == cleanRhsArray.count else { return false } - for (index, element) in cleanLhsArray.enumerated() { - if !deepEqualsmessages(element, cleanRhsArray[index]) { + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEqualsmessages(element, rhsArray[index]) { return false } } return true - case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } - for (key, cleanLhsValue) in cleanLhsDictionary { - guard cleanRhsDictionary.index(forKey: key) != nil else { return false } - if !deepEqualsmessages(cleanLhsValue, cleanRhsDictionary[key]!) { + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEqualsmessages(element, rhsArray[index]) { return false } } return true + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEqualsmessages(lhsKey, rhsKey) { + if deepEqualsmessages(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true + + case (let lhs as Double, let rhs as Double): + return doubleEqualsmessages(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + default: - // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. return false } } func deepHashmessages(value: Any?, hasher: inout Hasher) { - if let valueList = value as? [AnyHashable] { - for item in valueList { deepHashmessages(value: item, hasher: &hasher) } - return - } - - if let valueDict = value as? [AnyHashable: AnyHashable] { - for key in valueDict.keys { - hasher.combine(key) - deepHashmessages(value: valueDict[key]!, hasher: &hasher) + let cleanValue = nilOrValue(value) as Any? + if let cleanValue = cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHashmessages(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHashmessages(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHashmessages(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHashmessages(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHashmessages(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) } - return - } - - if let hashableValue = value as? AnyHashable { - hasher.combine(hashableValue.hashValue) + } else { + hasher.combine(0) } - - return hasher.combine(String(describing: value)) } /// Possible outcomes of an authentication attempt. @@ -189,10 +236,19 @@ struct AuthStrings: Hashable { ] } static func == (lhs: AuthStrings, rhs: AuthStrings) -> Bool { - return deepEqualsmessages(lhs.toList(), rhs.toList()) + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsmessages(lhs.reason, rhs.reason) + && deepEqualsmessages(lhs.cancelButton, rhs.cancelButton) + && deepEqualsmessages(lhs.localizedFallbackTitle, rhs.localizedFallbackTitle) } + func hash(into hasher: inout Hasher) { - deepHashmessages(value: toList(), hasher: &hasher) + hasher.combine("AuthStrings") + deepHashmessages(value: reason, hasher: &hasher) + deepHashmessages(value: cancelButton, hasher: &hasher) + deepHashmessages(value: localizedFallbackTitle, hasher: &hasher) } } @@ -218,10 +274,17 @@ struct AuthOptions: Hashable { ] } static func == (lhs: AuthOptions, rhs: AuthOptions) -> Bool { - return deepEqualsmessages(lhs.toList(), rhs.toList()) + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsmessages(lhs.biometricOnly, rhs.biometricOnly) + && deepEqualsmessages(lhs.sticky, rhs.sticky) } + func hash(into hasher: inout Hasher) { - deepHashmessages(value: toList(), hasher: &hasher) + hasher.combine("AuthOptions") + deepHashmessages(value: biometricOnly, hasher: &hasher) + deepHashmessages(value: sticky, hasher: &hasher) } } @@ -254,10 +317,19 @@ struct AuthResultDetails: Hashable { ] } static func == (lhs: AuthResultDetails, rhs: AuthResultDetails) -> Bool { - return deepEqualsmessages(lhs.toList(), rhs.toList()) + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsmessages(lhs.result, rhs.result) + && deepEqualsmessages(lhs.errorMessage, rhs.errorMessage) + && deepEqualsmessages(lhs.errorDetails, rhs.errorDetails) } + func hash(into hasher: inout Hasher) { - deepHashmessages(value: toList(), hasher: &hasher) + hasher.combine("AuthResultDetails") + deepHashmessages(value: result, hasher: &hasher) + deepHashmessages(value: errorMessage, hasher: &hasher) + deepHashmessages(value: errorDetails, hasher: &hasher) } } @@ -340,6 +412,12 @@ protocol LocalAuthApi { func authenticate( options: AuthOptions, strings: AuthStrings, completion: @escaping (Result) -> Void) + /// Stops any in-progress authentication. + /// + /// Returns true if auth was cancelled successfully. + /// Returns false if there was no authentication in progress, + /// or an error occurred. + func stopAuthentication(completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -424,5 +502,27 @@ class LocalAuthApiSetup { } else { authenticateChannel.setMessageHandler(nil) } + /// Stops any in-progress authentication. + /// + /// Returns true if auth was cancelled successfully. + /// Returns false if there was no authentication in progress, + /// or an error occurred. + let stopAuthenticationChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.stopAuthentication\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + stopAuthenticationChannel.setMessageHandler { _, reply in + api.stopAuthentication { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + stopAuthenticationChannel.setMessageHandler(nil) + } } } 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 726f2e1ded95..9ec723e6f280 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 @@ -112,9 +112,9 @@ class LocalAuthDarwin extends LocalAuthPlatform { @override Future isDeviceSupported() async => _api.isDeviceSupported(); - /// Always returns false as this method is not supported on iOS or macOS. + /// Stops any in-progress authentication @override - Future stopAuthentication() async => false; + Future stopAuthentication() async => _api.stopAuthentication(); AuthStrings _pigeonStringsFromiOSAuthMessages( String localizedReason, 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 b3c7f88d67a5..95890465192d 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,44 +1,109 @@ // 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. -// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// Autogenerated from Pigeon (v26.3.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 +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; - -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; + +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == b; + } if (a is List && b is List) { return a.length == b.length && - a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key]), - ); + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; } return a == b; } +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + + /// Possible outcomes of an authentication attempt. enum AuthResult { /// The user authenticated successfully. success, - /// Native UI needed to be displayed, but couldn't be. uiUnavailable, appCancel, @@ -55,19 +120,25 @@ enum AuthResult { notInteractive, passcodeNotSet, userFallback, - /// An error other than the expected types occurred. unknownError, } /// Pigeon equivalent of the subset of BiometricType used by iOS. -enum AuthBiometric { face, fingerprint } +enum AuthBiometric { + face, + fingerprint, +} /// Pigeon version of IOSAuthMessages, plus the authorization reason. /// /// See auth_messages_ios.dart for details. class AuthStrings { - AuthStrings({required this.reason, required this.cancelButton, this.localizedFallbackTitle}); + AuthStrings({ + required this.reason, + required this.cancelButton, + this.localizedFallbackTitle, + }); String reason; @@ -76,12 +147,15 @@ class AuthStrings { String? localizedFallbackTitle; List _toList() { - return [reason, cancelButton, localizedFallbackTitle]; + return [ + reason, + cancelButton, + localizedFallbackTitle, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static AuthStrings decode(Object result) { result as List; @@ -101,32 +175,40 @@ class AuthStrings { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(reason, other.reason) && _deepEquals(cancelButton, other.cancelButton) && _deepEquals(localizedFallbackTitle, other.localizedFallbackTitle); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class AuthOptions { - AuthOptions({required this.biometricOnly, required this.sticky}); + AuthOptions({ + required this.biometricOnly, + required this.sticky, + }); bool biometricOnly; bool sticky; List _toList() { - return [biometricOnly, sticky]; + return [ + biometricOnly, + sticky, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static AuthOptions decode(Object result) { result as List; - return AuthOptions(biometricOnly: result[0]! as bool, sticky: result[1]! as bool); + return AuthOptions( + biometricOnly: result[0]! as bool, + sticky: result[1]! as bool, + ); } @override @@ -138,16 +220,20 @@ class AuthOptions { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(biometricOnly, other.biometricOnly) && _deepEquals(sticky, other.sticky); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class AuthResultDetails { - AuthResultDetails({required this.result, this.errorMessage, this.errorDetails}); + AuthResultDetails({ + required this.result, + this.errorMessage, + this.errorDetails, + }); /// The result of authenticating. AuthResult result; @@ -159,12 +245,15 @@ class AuthResultDetails { String? errorDetails; List _toList() { - return [result, errorMessage, errorDetails]; + return [ + result, + errorMessage, + errorDetails, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static AuthResultDetails decode(Object result) { result as List; @@ -184,14 +273,15 @@ class AuthResultDetails { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(result, other.result) && _deepEquals(errorMessage, other.errorMessage) && _deepEquals(errorDetails, other.errorDetails); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -199,19 +289,19 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is AuthResult) { + } else if (value is AuthResult) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is AuthBiometric) { + } else if (value is AuthBiometric) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is AuthStrings) { + } else if (value is AuthStrings) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is AuthOptions) { + } else if (value is AuthOptions) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is AuthResultDetails) { + } else if (value is AuthResultDetails) { buffer.putUint8(133); writeValue(buffer, value.encode()); } else { @@ -223,10 +313,10 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: - final int? value = readValue(buffer) as int?; + final value = readValue(buffer) as int?; return value == null ? null : AuthResult.values[value]; case 130: - final int? value = readValue(buffer) as int?; + final value = readValue(buffer) as int?; return value == null ? null : AuthBiometric.values[value]; case 131: return AuthStrings.decode(readValue(buffer)!); @@ -245,10 +335,8 @@ class LocalAuthApi { /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. LocalAuthApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -257,123 +345,108 @@ class LocalAuthApi { /// Returns true if this device supports authentication. Future isDeviceSupported() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.isDeviceSupported$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.isDeviceSupported$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); 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: pigeonVar_replyList[0]! as String, - 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 bool?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as bool; } /// Returns true if this device can support biometric authentication, whether /// any biometrics are enrolled or not. Future deviceCanSupportBiometrics() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.deviceCanSupportBiometrics$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.deviceCanSupportBiometrics$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); 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: pigeonVar_replyList[0]! as String, - 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 bool?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as bool; } /// Returns the biometric types that are enrolled, and can thus be used /// without additional setup. Future> getEnrolledBiometrics() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.getEnrolledBiometrics$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.getEnrolledBiometrics$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); 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: pigeonVar_replyList[0]! as String, - 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(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return (pigeonVar_replyValue! as List).cast(); } /// Attempts to authenticate the user with the provided [options], and using /// [strings] for any UI. Future authenticate(AuthOptions options, AuthStrings strings) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.authenticate$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.authenticate$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([ - options, - strings, - ]); - 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: pigeonVar_replyList[0]! as String, - 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 AuthResultDetails?)!; - } + final Future pigeonVar_sendFuture = pigeonVar_channel.send([options, strings]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as AuthResultDetails; + } + + /// Stops any in-progress authentication. + /// + /// Returns true if auth was cancelled successfully. + /// Returns false if there was no authentication in progress, + /// or an error occurred. + Future stopAuthentication() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_darwin.LocalAuthApi.stopAuthentication$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as bool; } } diff --git a/packages/local_auth/local_auth_darwin/pigeons/messages.dart b/packages/local_auth/local_auth_darwin/pigeons/messages.dart index ee7709033ded..2c7b1b3fae38 100644 --- a/packages/local_auth/local_auth_darwin/pigeons/messages.dart +++ b/packages/local_auth/local_auth_darwin/pigeons/messages.dart @@ -95,4 +95,12 @@ abstract class LocalAuthApi { /// [strings] for any UI. @async AuthResultDetails authenticate(AuthOptions options, AuthStrings strings); + + /// Stops any in-progress authentication. + /// + /// Returns true if auth was cancelled successfully. + /// Returns false if there was no authentication in progress, + /// or an error occurred. + @async + bool stopAuthentication(); } diff --git a/packages/local_auth/local_auth_darwin/pubspec.yaml b/packages/local_auth/local_auth_darwin/pubspec.yaml index 976f2a614523..da8a39d6f4e0 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: 2.0.3 +version: 2.1.0 environment: sdk: ^3.10.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 5df711388d6e..19edf1f38893 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 @@ -52,9 +52,15 @@ void main() { }); group('stopAuthentication', () { - test('always returns false', () async { + test('handles false', () async { + when(api.stopAuthentication()).thenAnswer((_) async => false); expect(await plugin.stopAuthentication(), false); }); + + test('handles true', () async { + when(api.stopAuthentication()).thenAnswer((_) async => true); + expect(await plugin.stopAuthentication(), true); + }); }); group('getEnrolledBiometrics', () { 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 9920df0f72ee..d9efd0c6633f 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 @@ -22,8 +22,10 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member -class _FakeAuthResultDetails_0 extends _i1.SmartFake implements _i2.AuthResultDetails { +class _FakeAuthResultDetails_0 extends _i1.SmartFake + implements _i2.AuthResultDetails { _FakeAuthResultDetails_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } @@ -67,7 +69,9 @@ class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi { _i4.Future> getEnrolledBiometrics() => (super.noSuchMethod( Invocation.method(#getEnrolledBiometrics, []), - returnValue: _i4.Future>.value(<_i2.AuthBiometric>[]), + returnValue: _i4.Future>.value( + <_i2.AuthBiometric>[], + ), ) as _i4.Future>); @@ -79,8 +83,19 @@ class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi { (super.noSuchMethod( Invocation.method(#authenticate, [options, strings]), returnValue: _i4.Future<_i2.AuthResultDetails>.value( - _FakeAuthResultDetails_0(this, Invocation.method(#authenticate, [options, strings])), + _FakeAuthResultDetails_0( + this, + Invocation.method(#authenticate, [options, strings]), + ), ), ) as _i4.Future<_i2.AuthResultDetails>); + + @override + _i4.Future stopAuthentication() => + (super.noSuchMethod( + Invocation.method(#stopAuthentication, []), + returnValue: _i4.Future.value(false), + ) + as _i4.Future); }