From 086e8adcf949b4ad29cf28b77a6ae5dad9453521 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 11 May 2023 15:42:30 -0400 Subject: [PATCH 01/14] Remove useless check --- .../local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 08903d06f19..7b74a1de508 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -91,13 +91,11 @@ - (void)alertMessage:(NSString *)message actionWithTitle:secondButton style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - if (UIApplicationOpenSettingsURLString != NULL) { - NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; - [[UIApplication sharedApplication] openURL:url - options:@{} - completionHandler:NULL]; - result(@NO); - } + NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; + [[UIApplication sharedApplication] openURL:url + options:@{} + completionHandler:NULL]; + result(@NO); }]; [alert addAction:additionalAction]; } From 093b0a2947147cd02f2f285b60dba8a0ff2ed7f4 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 11 May 2023 15:52:33 -0400 Subject: [PATCH 02/14] Add todo for incorrect implementation to mark it as staying native --- .../local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 7b74a1de508..6ac7d95550a 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -62,6 +62,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) { [self deviceSupportsBiometrics:result]; } else if ([@"isDeviceSupported" isEqualToString:call.method]) { + // TODO(stuartmorgan): Fix this to check for biometrics or passcode; see + // https://github.com/flutter/flutter/issues/116179 result(@YES); } else { result(FlutterMethodNotImplemented); From 5b7888b5e747c9247ad0f1d4e1bbf32073872bd3 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 11 May 2023 15:58:42 -0400 Subject: [PATCH 03/14] Initial Pigeon definition, modified from Android --- .../local_auth_ios/pigeons/copyright.txt | 3 + .../local_auth_ios/pigeons/messages.dart | 101 ++++++++++++++++++ .../local_auth/local_auth_ios/pubspec.yaml | 2 + 3 files changed, 106 insertions(+) create mode 100644 packages/local_auth/local_auth_ios/pigeons/copyright.txt create mode 100644 packages/local_auth/local_auth_ios/pigeons/messages.dart diff --git a/packages/local_auth/local_auth_ios/pigeons/copyright.txt b/packages/local_auth/local_auth_ios/pigeons/copyright.txt new file mode 100644 index 00000000000..1236b63caf3 --- /dev/null +++ b/packages/local_auth/local_auth_ios/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/local_auth/local_auth_ios/pigeons/messages.dart b/packages/local_auth/local_auth_ios/pigeons/messages.dart new file mode 100644 index 00000000000..7ae23c43beb --- /dev/null +++ b/packages/local_auth/local_auth_ios/pigeons/messages.dart @@ -0,0 +1,101 @@ +// 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:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + objcHeaderOut: 'ios/Classes/messages.g.h', + objcSourceOut: 'ios/Classes/messages.g.m', + objcOptions: ObjcOptions( + prefix: 'FLA', + ), + copyrightHeader: 'pigeons/copyright.txt', +)) + +/// Pigeon version of IOSAuthMessages, plus the authorization reason. +/// +/// See auth_messages_ios.dart for details. +class AuthStrings { + /// Constructs a new instance. + const AuthStrings({ + required this.reason, + required this.lockOut, + required 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; +} + +/// Possible outcomes of an authentication attempt. +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, +} + +class AuthOptions { + AuthOptions( + {required this.biometricOnly, + required this.sticky, + required this.useErrorDialgs}); + final bool biometricOnly; + final bool sticky; + final bool useErrorDialgs; +} + +// TODO(stuartmorgan): Remove this when +// https://github.com/flutter/flutter/issues/87307 is implemented. +class AuthResultWrapper { + AuthResultWrapper({required this.value}); + final AuthResult value; +} + +/// Pigeon equivalent of the subset of BiometricType used by iOS. +enum AuthBiometrics { weak, strong } + +// TODO(stuartmorgan): Remove this when +// https://github.com/flutter/flutter/issues/87307 is implemented. +class AuthClassificationWrapper { + AuthClassificationWrapper({required this.value}); + final AuthBiometrics value; +} + +@HostApi() +abstract class LocalAuthApi { + /// Returns true if this device supports authentication. + bool isDeviceSupported(); + + /// Returns true if this device can support biometric authentication, whether + /// any biometrics are enrolled or not. + bool deviceCanSupportBiometrics(); + + /// Returns the biometric types that are enrolled, and can thus be used + /// without additional setup. + List getEnrolledBiometrics(); + + /// Attempts to authenticate the user with the provided [options], and using + /// [strings] for any UI. + @async + AuthResultWrapper authenticate(AuthOptions options, AuthStrings strings); +} diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index 79654a0039c..199461e6e08 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -23,5 +23,7 @@ dependencies: local_auth_platform_interface: ^1.0.1 dev_dependencies: + build_runner: ^2.3.3 flutter_test: sdk: flutter + pigeon: ^9.2.4 From 10a3ab46ba41583b9afe627127cd8d7fd62d0491 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 12 May 2023 13:04:31 -0400 Subject: [PATCH 04/14] Adjust native implementation to conform to Pigeon API, adjusting Pigeon API as needed --- .../ios/Classes/FLTLocalAuthPlugin.h | 4 +- .../ios/Classes/FLTLocalAuthPlugin.m | 293 +++++++-------- .../local_auth_ios/ios/Classes/messages.g.h | 123 +++++++ .../local_auth_ios/ios/Classes/messages.g.m | 333 ++++++++++++++++++ .../local_auth_ios/pigeons/messages.dart | 34 +- 5 files changed, 632 insertions(+), 155 deletions(-) create mode 100644 packages/local_auth/local_auth_ios/ios/Classes/messages.g.h create mode 100644 packages/local_auth/local_auth_ios/ios/Classes/messages.g.m diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h index 1a1446fb27b..d023ba3ed14 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h @@ -4,5 +4,7 @@ #import -@interface FLTLocalAuthPlugin : NSObject +#import "messages.g.h" + +@interface FLTLocalAuthPlugin : NSObject @end diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 6ac7d95550a..04ddfbbdb62 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -6,6 +6,8 @@ #import +typedef void (^AuthCompletion)(FLAAuthResultDetails *_Nullable, FlutterError *_Nullable); + /** * A default context factory that wraps standard LAContext allocation. */ @@ -20,21 +22,45 @@ - (LAContext *)createAuthContext { #pragma mark - +/** + * A data container for sticky auth state. + */ +@interface FLAStickyAuthState : NSObject +@property(nonatomic, strong, nonnull) FLAAuthOptions *options; +@property(nonatomic, strong, nonnull) FLAAuthStrings *strings; +@property(nonatomic, strong, nonnull) AuthCompletion resultHandler; +- (instancetype)initWithOptions:(nonnull FLAAuthOptions *)options + strings:(nonnull FLAAuthStrings *)strings + resultHandler:(nonnull AuthCompletion)resultHandler; +@end + +@implementation FLAStickyAuthState +- (instancetype)initWithOptions:(nonnull FLAAuthOptions *)options + strings:(nonnull FLAAuthStrings *)strings + resultHandler:(nonnull AuthCompletion)resultHandler { + self = [super init]; + if (self) { + _options = options; + _strings = strings; + _resultHandler = resultHandler; + } + return self; +} +@end + +#pragma mark - + @interface FLTLocalAuthPlugin () -@property(nonatomic, copy, nullable) NSDictionary *lastCallArgs; -@property(nonatomic, nullable) FlutterResult lastResult; +@property(nonatomic, strong, nullable) FLAStickyAuthState *lastCallState; @property(nonatomic, strong) NSObject *authContextFactory; @end @implementation FLTLocalAuthPlugin + (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth_ios" - binaryMessenger:[registrar messenger]]; FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; [registrar addApplicationDelegate:instance]; + FLALocalAuthApiSetup([registrar messenger], instance); } - (instancetype)init { @@ -49,161 +75,128 @@ - (instancetype)initWithContextFactory:(NSObject *)factor return self; } -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"authenticate" isEqualToString:call.method]) { - bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; - if (isBiometricOnly) { - [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; - } else { - [self authenticate:call.arguments withFlutterResult:result]; - } - } else if ([@"getEnrolledBiometrics" isEqualToString:call.method]) { - [self getEnrolledBiometrics:result]; - } else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) { - [self deviceSupportsBiometrics:result]; - } else if ([@"isDeviceSupported" isEqualToString:call.method]) { - // TODO(stuartmorgan): Fix this to check for biometrics or passcode; see - // https://github.com/flutter/flutter/issues/116179 - result(@YES); - } else { - result(FlutterMethodNotImplemented); - } -} +#pragma mark FLALocalAuthApi -#pragma mark Private Methods - -- (void)alertMessage:(NSString *)message - firstButton:(NSString *)firstButton - flutterResult:(FlutterResult)result - additionalButton:(NSString *)secondButton { - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:@"" - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - result(@NO); - }]; +- (void)authenticateWithOptions:(nonnull FLAAuthOptions *)options + strings:(nonnull FLAAuthStrings *)strings + completion:(nonnull void (^)(FLAAuthResultDetails *_Nullable, + FlutterError *_Nullable))completion { + LAContext *context = [self.authContextFactory createAuthContext]; + NSError *authError = nil; + self.lastCallState = nil; + context.localizedFallbackTitle = strings.localizedFallbackTitle; - [alert addAction:defaultAction]; - if (secondButton != nil) { - UIAlertAction *additionalAction = [UIAlertAction - actionWithTitle:secondButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; - [[UIApplication sharedApplication] openURL:url - options:@{} - completionHandler:NULL]; - result(@NO); - }]; - [alert addAction:additionalAction]; + LAPolicy policy = options.biometricOnly.boolValue + ? LAPolicyDeviceOwnerAuthenticationWithBiometrics + : LAPolicyDeviceOwnerAuthentication; + if ([context canEvaluatePolicy:policy error:&authError]) { + [context evaluatePolicy:policy + localizedReason:strings.reason + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + options:options + strings:strings + completion:completion]; + }); + }]; + } else { + [self handleError:authError withOptions:options strings:strings completion:completion]; } - [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert - animated:YES - completion:nil]; } -- (void)deviceSupportsBiometrics:(FlutterResult)result { +- (nullable NSNumber *)deviceCanSupportBiometricsWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { LAContext *context = [self.authContextFactory createAuthContext]; NSError *authError = nil; // Check if authentication with biometrics is possible. if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { if (authError == nil) { - result(@YES); - return; + return @YES; } } // If not, check if it is because no biometrics are enrolled (but still present). if (authError != nil) { if (authError.code == LAErrorBiometryNotEnrolled) { - result(@YES); - return; + return @YES; } } - result(@NO); + return @NO; } -- (void)getEnrolledBiometrics:(FlutterResult)result { +- (nullable NSArray *)getEnrolledBiometricsWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { LAContext *context = [self.authContextFactory createAuthContext]; NSError *authError = nil; - NSMutableArray *biometrics = [[NSMutableArray alloc] init]; + NSMutableArray *biometrics = [[NSMutableArray alloc] init]; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { if (authError == nil) { if (context.biometryType == LABiometryTypeFaceID) { - [biometrics addObject:@"face"]; + [biometrics addObject:[FLAAuthBiometricWrapper makeWithValue:FLAAuthBiometricFace]]; } else if (context.biometryType == LABiometryTypeTouchID) { - [biometrics addObject:@"fingerprint"]; + [biometrics addObject:[FLAAuthBiometricWrapper makeWithValue:FLAAuthBiometricFingerprint]]; } } } - result(biometrics); + return biometrics; } -- (void)authenticateWithBiometrics:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { - LAContext *context = [self.authContextFactory createAuthContext]; - NSError *authError = nil; - self.lastCallArgs = nil; - self.lastResult = nil; - context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] - ? nil - : arguments[@"localizedFallbackTitle"]; - - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - error:&authError]) { - [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; - } +- (nullable NSNumber *)isDeviceSupportedWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + // TODO(stuartmorgan): Fix this to check for biometrics or passcode; see + // https://github.com/flutter/flutter/issues/116179 + return @YES; } -- (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = [self.authContextFactory createAuthContext]; - NSError *authError = nil; - _lastCallArgs = nil; - _lastResult = nil; - context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] - ? nil - : arguments[@"localizedFallbackTitle"]; +#pragma mark Private Methods - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { - [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; +- (void)showAlertWithMessage:(NSString *)message + firstButton:(NSString *)firstButton + additionalButton:(NSString *)secondButton + completion:(AuthCompletion)completion { + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self handleSucceeded:NO + withCompletion:completion]; + }]; + + [alert addAction:defaultAction]; + if (secondButton != nil) { + UIAlertAction *additionalAction = [UIAlertAction + actionWithTitle:secondButton + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; + [[UIApplication sharedApplication] openURL:url + options:@{} + completionHandler:NULL]; + [self handleSucceeded:NO withCompletion:completion]; + }]; + [alert addAction:additionalAction]; } + [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert + animated:YES + completion:nil]; } - (void)handleAuthReplyWithSuccess:(BOOL)success error:(NSError *)error - flutterArguments:(NSDictionary *)arguments - flutterResult:(FlutterResult)result { + options:(FLAAuthOptions *)options + strings:(FLAAuthStrings *)strings + completion:(nonnull AuthCompletion)completion { NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); if (success) { - result(@YES); + [self handleSucceeded:YES withCompletion:completion]; } else { switch (error.code) { case LAErrorBiometryNotAvailable: @@ -212,54 +205,68 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success case LAErrorUserFallback: case LAErrorPasscodeNotSet: case LAErrorAuthenticationFailed: - [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; + [self handleError:error withOptions:options strings:strings completion:completion]; return; case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; + if ([options.sticky boolValue]) { + _lastCallState = [[FLAStickyAuthState alloc] initWithOptions:options + strings:strings + resultHandler:completion]; } else { - result(@NO); + [self handleSucceeded:NO withCompletion:completion]; } return; } - [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; + [self handleError:error withOptions:options strings:strings completion:completion]; } } -- (void)handleErrors:(NSError *)authError - flutterArguments:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { - NSString *errorCode = @"NotAvailable"; +- (void)handleSucceeded:(BOOL)succeeded withCompletion:(nonnull AuthCompletion)completion { + completion( + [FLAAuthResultDetails makeWithValue:(succeeded ? FLAAuthResultSuccess : FLAAuthResultFailure) + errorMessage:nil + errorDetails:nil], + nil); +} + +- (void)handleError:(NSError *)authError + withOptions:(FLAAuthOptions *)options + strings:(FLAAuthStrings *)strings + completion:(nonnull AuthCompletion)completion { + FLAAuthResult result = FLAAuthResultErrorNotAvailable; switch (authError.code) { case LAErrorPasscodeNotSet: case LAErrorBiometryNotEnrolled: - if ([arguments[@"useErrorDialogs"] boolValue]) { - [self alertMessage:arguments[@"goToSettingDescriptionIOS"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:arguments[@"goToSetting"]]; + if (options.useErrorDialgs.boolValue) { + [self showAlertWithMessage:strings.goToSettingsDescription + firstButton:strings.cancelButton + additionalButton:strings.goToSettingsButton + completion:completion]; return; } - errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; + result = authError.code == LAErrorPasscodeNotSet ? FLAAuthResultErrorPasscodeNotSet + : FLAAuthResultErrorNotEnrolled; break; case LAErrorBiometryLockout: - [self alertMessage:arguments[@"lockOut"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:nil]; + [self showAlertWithMessage:strings.lockOut + firstButton:strings.cancelButton + additionalButton:nil + completion:completion]; return; } - result([FlutterError errorWithCode:errorCode - message:authError.localizedDescription - details:authError.domain]); + completion([FLAAuthResultDetails makeWithValue:result + errorMessage:authError.localizedDescription + errorDetails:authError.domain], + nil); } #pragma mark - AppDelegate - (void)applicationDidBecomeActive:(UIApplication *)application { - if (self.lastCallArgs != nil && self.lastResult != nil) { - [self authenticateWithBiometrics:_lastCallArgs withFlutterResult:self.lastResult]; + if (self.lastCallState != nil) { + [self authenticateWithOptions:_lastCallState.options + strings:_lastCallState.strings + completion:_lastCallState.resultHandler]; } } diff --git a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h new file mode 100644 index 00000000000..6614c77b8fc --- /dev/null +++ b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h @@ -0,0 +1,123 @@ +// 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 (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#import + +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +/// Possible outcomes of an authentication attempt. +typedef NS_ENUM(NSUInteger, FLAAuthResult) { + /// The user authenticated successfully. + FLAAuthResultSuccess = 0, + /// The user failed to successfully authenticate. + FLAAuthResultFailure = 1, + /// The authentication system was not available. + FLAAuthResultErrorNotAvailable = 2, + /// No biometrics are enrolled. + FLAAuthResultErrorNotEnrolled = 3, + /// No passcode is set. + FLAAuthResultErrorPasscodeNotSet = 4, +}; + +/// Pigeon equivalent of the subset of BiometricType used by iOS. +typedef NS_ENUM(NSUInteger, FLAAuthBiometric) { + FLAAuthBiometricFace = 0, + FLAAuthBiometricFingerprint = 1, +}; + +@class FLAAuthStrings; +@class FLAAuthOptions; +@class FLAAuthResultDetails; +@class FLAAuthBiometricWrapper; + +/// Pigeon version of IOSAuthMessages, plus the authorization reason. +/// +/// See auth_messages_ios.dart for details. +@interface FLAAuthStrings : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithReason:(NSString *)reason + lockOut:(NSString *)lockOut + goToSettingsButton:(NSString *)goToSettingsButton + goToSettingsDescription:(NSString *)goToSettingsDescription + cancelButton:(NSString *)cancelButton + localizedFallbackTitle:(nullable NSString *)localizedFallbackTitle; +@property(nonatomic, copy) NSString *reason; +@property(nonatomic, copy) NSString *lockOut; +@property(nonatomic, copy) NSString *goToSettingsButton; +@property(nonatomic, copy) NSString *goToSettingsDescription; +@property(nonatomic, copy) NSString *cancelButton; +@property(nonatomic, copy, nullable) NSString *localizedFallbackTitle; +@end + +@interface FLAAuthOptions : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithBiometricOnly:(NSNumber *)biometricOnly + sticky:(NSNumber *)sticky + useErrorDialgs:(NSNumber *)useErrorDialgs; +@property(nonatomic, strong) NSNumber *biometricOnly; +@property(nonatomic, strong) NSNumber *sticky; +@property(nonatomic, strong) NSNumber *useErrorDialgs; +@end + +@interface FLAAuthResultDetails : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FLAAuthResult)value + errorMessage:(nullable NSString *)errorMessage + errorDetails:(nullable NSString *)errorDetails; +/// The result of authenticating. +@property(nonatomic, assign) FLAAuthResult value; +/// A system-provided error message, if any. +@property(nonatomic, copy, nullable) NSString *errorMessage; +/// System-provided error details, if any. +@property(nonatomic, copy, nullable) NSString *errorDetails; +@end + +@interface FLAAuthBiometricWrapper : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FLAAuthBiometric)value; +@property(nonatomic, assign) FLAAuthBiometric value; +@end + +/// The codec used by FLALocalAuthApi. +NSObject *FLALocalAuthApiGetCodec(void); + +@protocol FLALocalAuthApi +/// Returns true if this device supports authentication. +/// +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)isDeviceSupportedWithError:(FlutterError *_Nullable *_Nonnull)error; +/// Returns true if this device can support biometric authentication, whether +/// any biometrics are enrolled or not. +/// +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)deviceCanSupportBiometricsWithError:(FlutterError *_Nullable *_Nonnull)error; +/// Returns the biometric types that are enrolled, and can thus be used +/// without additional setup. +/// +/// @return `nil` only when `error != nil`. +- (nullable NSArray *)getEnrolledBiometricsWithError: + (FlutterError *_Nullable *_Nonnull)error; +/// Attempts to authenticate the user with the provided [options], and using +/// [strings] for any UI. +- (void)authenticateWithOptions:(FLAAuthOptions *)options + strings:(FLAAuthStrings *)strings + completion:(void (^)(FLAAuthResultDetails *_Nullable, + FlutterError *_Nullable))completion; +@end + +extern void FLALocalAuthApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m new file mode 100644 index 00000000000..0965706b911 --- /dev/null +++ b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m @@ -0,0 +1,333 @@ +// 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 (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#import "messages.g.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSArray *wrapResult(id result, FlutterError *error) { + if (error) { + return @[ + error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] + ]; + } + return @[ result ?: [NSNull null] ]; +} +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@interface FLAAuthStrings () ++ (FLAAuthStrings *)fromList:(NSArray *)list; ++ (nullable FLAAuthStrings *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FLAAuthOptions () ++ (FLAAuthOptions *)fromList:(NSArray *)list; ++ (nullable FLAAuthOptions *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FLAAuthResultDetails () ++ (FLAAuthResultDetails *)fromList:(NSArray *)list; ++ (nullable FLAAuthResultDetails *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FLAAuthBiometricWrapper () ++ (FLAAuthBiometricWrapper *)fromList:(NSArray *)list; ++ (nullable FLAAuthBiometricWrapper *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@implementation FLAAuthStrings ++ (instancetype)makeWithReason:(NSString *)reason + lockOut:(NSString *)lockOut + goToSettingsButton:(NSString *)goToSettingsButton + goToSettingsDescription:(NSString *)goToSettingsDescription + cancelButton:(NSString *)cancelButton + localizedFallbackTitle:(nullable NSString *)localizedFallbackTitle { + FLAAuthStrings *pigeonResult = [[FLAAuthStrings alloc] init]; + pigeonResult.reason = reason; + pigeonResult.lockOut = lockOut; + pigeonResult.goToSettingsButton = goToSettingsButton; + pigeonResult.goToSettingsDescription = goToSettingsDescription; + pigeonResult.cancelButton = cancelButton; + pigeonResult.localizedFallbackTitle = localizedFallbackTitle; + return pigeonResult; +} ++ (FLAAuthStrings *)fromList:(NSArray *)list { + FLAAuthStrings *pigeonResult = [[FLAAuthStrings alloc] init]; + pigeonResult.reason = GetNullableObjectAtIndex(list, 0); + NSAssert(pigeonResult.reason != nil, @""); + pigeonResult.lockOut = GetNullableObjectAtIndex(list, 1); + NSAssert(pigeonResult.lockOut != nil, @""); + pigeonResult.goToSettingsButton = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.goToSettingsButton != nil, @""); + pigeonResult.goToSettingsDescription = GetNullableObjectAtIndex(list, 3); + NSAssert(pigeonResult.goToSettingsDescription != nil, @""); + pigeonResult.cancelButton = GetNullableObjectAtIndex(list, 4); + NSAssert(pigeonResult.cancelButton != nil, @""); + pigeonResult.localizedFallbackTitle = GetNullableObjectAtIndex(list, 5); + return pigeonResult; +} ++ (nullable FLAAuthStrings *)nullableFromList:(NSArray *)list { + return (list) ? [FLAAuthStrings fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.reason ?: [NSNull null]), + (self.lockOut ?: [NSNull null]), + (self.goToSettingsButton ?: [NSNull null]), + (self.goToSettingsDescription ?: [NSNull null]), + (self.cancelButton ?: [NSNull null]), + (self.localizedFallbackTitle ?: [NSNull null]), + ]; +} +@end + +@implementation FLAAuthOptions ++ (instancetype)makeWithBiometricOnly:(NSNumber *)biometricOnly + sticky:(NSNumber *)sticky + useErrorDialgs:(NSNumber *)useErrorDialgs { + FLAAuthOptions *pigeonResult = [[FLAAuthOptions alloc] init]; + pigeonResult.biometricOnly = biometricOnly; + pigeonResult.sticky = sticky; + pigeonResult.useErrorDialgs = useErrorDialgs; + return pigeonResult; +} ++ (FLAAuthOptions *)fromList:(NSArray *)list { + FLAAuthOptions *pigeonResult = [[FLAAuthOptions alloc] init]; + pigeonResult.biometricOnly = GetNullableObjectAtIndex(list, 0); + NSAssert(pigeonResult.biometricOnly != nil, @""); + pigeonResult.sticky = GetNullableObjectAtIndex(list, 1); + NSAssert(pigeonResult.sticky != nil, @""); + pigeonResult.useErrorDialgs = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.useErrorDialgs != nil, @""); + return pigeonResult; +} ++ (nullable FLAAuthOptions *)nullableFromList:(NSArray *)list { + return (list) ? [FLAAuthOptions fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.biometricOnly ?: [NSNull null]), + (self.sticky ?: [NSNull null]), + (self.useErrorDialgs ?: [NSNull null]), + ]; +} +@end + +@implementation FLAAuthResultDetails ++ (instancetype)makeWithValue:(FLAAuthResult)value + errorMessage:(nullable NSString *)errorMessage + errorDetails:(nullable NSString *)errorDetails { + FLAAuthResultDetails *pigeonResult = [[FLAAuthResultDetails alloc] init]; + pigeonResult.value = value; + pigeonResult.errorMessage = errorMessage; + pigeonResult.errorDetails = errorDetails; + return pigeonResult; +} ++ (FLAAuthResultDetails *)fromList:(NSArray *)list { + FLAAuthResultDetails *pigeonResult = [[FLAAuthResultDetails alloc] init]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.errorMessage = GetNullableObjectAtIndex(list, 1); + pigeonResult.errorDetails = GetNullableObjectAtIndex(list, 2); + return pigeonResult; +} ++ (nullable FLAAuthResultDetails *)nullableFromList:(NSArray *)list { + return (list) ? [FLAAuthResultDetails fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.value), + (self.errorMessage ?: [NSNull null]), + (self.errorDetails ?: [NSNull null]), + ]; +} +@end + +@implementation FLAAuthBiometricWrapper ++ (instancetype)makeWithValue:(FLAAuthBiometric)value { + FLAAuthBiometricWrapper *pigeonResult = [[FLAAuthBiometricWrapper alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FLAAuthBiometricWrapper *)fromList:(NSArray *)list { + FLAAuthBiometricWrapper *pigeonResult = [[FLAAuthBiometricWrapper alloc] init]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FLAAuthBiometricWrapper *)nullableFromList:(NSArray *)list { + return (list) ? [FLAAuthBiometricWrapper fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.value), + ]; +} +@end + +@interface FLALocalAuthApiCodecReader : FlutterStandardReader +@end +@implementation FLALocalAuthApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FLAAuthBiometricWrapper fromList:[self readValue]]; + case 129: + return [FLAAuthOptions fromList:[self readValue]]; + case 130: + return [FLAAuthResultDetails fromList:[self readValue]]; + case 131: + return [FLAAuthStrings fromList:[self readValue]]; + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FLALocalAuthApiCodecWriter : FlutterStandardWriter +@end +@implementation FLALocalAuthApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FLAAuthBiometricWrapper class]]) { + [self writeByte:128]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FLAAuthOptions class]]) { + [self writeByte:129]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FLAAuthResultDetails class]]) { + [self writeByte:130]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FLAAuthStrings class]]) { + [self writeByte:131]; + [self writeValue:[value toList]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FLALocalAuthApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FLALocalAuthApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FLALocalAuthApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FLALocalAuthApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FLALocalAuthApiGetCodec(void) { + static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; + dispatch_once(&sPred, ^{ + FLALocalAuthApiCodecReaderWriter *readerWriter = + [[FLALocalAuthApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FLALocalAuthApiSetup(id binaryMessenger, + NSObject *api) { + /// Returns true if this device supports authentication. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.LocalAuthApi.isDeviceSupported" + binaryMessenger:binaryMessenger + codec:FLALocalAuthApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(isDeviceSupportedWithError:)], + @"FLALocalAuthApi api (%@) doesn't respond to @selector(isDeviceSupportedWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api isDeviceSupportedWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Returns true if this device can support biometric authentication, whether + /// any biometrics are enrolled or not. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.LocalAuthApi.deviceCanSupportBiometrics" + binaryMessenger:binaryMessenger + codec:FLALocalAuthApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(deviceCanSupportBiometricsWithError:)], + @"FLALocalAuthApi api (%@) doesn't respond to " + @"@selector(deviceCanSupportBiometricsWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api deviceCanSupportBiometricsWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Returns the biometric types that are enrolled, and can thus be used + /// without additional setup. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.LocalAuthApi.getEnrolledBiometrics" + binaryMessenger:binaryMessenger + codec:FLALocalAuthApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(getEnrolledBiometricsWithError:)], + @"FLALocalAuthApi api (%@) doesn't respond to @selector(getEnrolledBiometricsWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSArray *output = [api getEnrolledBiometricsWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Attempts to authenticate the user with the provided [options], and using + /// [strings] for any UI. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.LocalAuthApi.authenticate" + binaryMessenger:binaryMessenger + codec:FLALocalAuthApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(authenticateWithOptions:strings:completion:)], + @"FLALocalAuthApi api (%@) doesn't respond to " + @"@selector(authenticateWithOptions:strings:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLAAuthOptions *arg_options = GetNullableObjectAtIndex(args, 0); + FLAAuthStrings *arg_strings = GetNullableObjectAtIndex(args, 1); + [api authenticateWithOptions:arg_options + strings:arg_strings + completion:^(FLAAuthResultDetails *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/local_auth/local_auth_ios/pigeons/messages.dart b/packages/local_auth/local_auth_ios/pigeons/messages.dart index 7ae23c43beb..4ae1663dd16 100644 --- a/packages/local_auth/local_auth_ios/pigeons/messages.dart +++ b/packages/local_auth/local_auth_ios/pigeons/messages.dart @@ -33,7 +33,7 @@ class AuthStrings { final String goToSettingsButton; final String goToSettingsDescription; final String cancelButton; - final String localizedFallbackTitle; + final String? localizedFallbackTitle; } /// Possible outcomes of an authentication attempt. @@ -64,21 +64,32 @@ class AuthOptions { final bool useErrorDialgs; } -// TODO(stuartmorgan): Remove this when -// https://github.com/flutter/flutter/issues/87307 is implemented. -class AuthResultWrapper { - AuthResultWrapper({required this.value}); +class AuthResultDetails { + AuthResultDetails( + {required this.value, this.errorMessage, this.errorDetails}); + + /// The result of authenticating. final AuthResult value; + + /// A system-provided error message, if any. + 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; } /// Pigeon equivalent of the subset of BiometricType used by iOS. -enum AuthBiometrics { weak, strong } +enum AuthBiometric { face, fingerprint } // TODO(stuartmorgan): Remove this when // https://github.com/flutter/flutter/issues/87307 is implemented. -class AuthClassificationWrapper { - AuthClassificationWrapper({required this.value}); - final AuthBiometrics value; +class AuthBiometricWrapper { + AuthBiometricWrapper({required this.value}); + final AuthBiometric value; } @HostApi() @@ -92,10 +103,11 @@ abstract class LocalAuthApi { /// Returns the biometric types that are enrolled, and can thus be used /// without additional setup. - List getEnrolledBiometrics(); + List getEnrolledBiometrics(); /// Attempts to authenticate the user with the provided [options], and using /// [strings] for any UI. @async - AuthResultWrapper authenticate(AuthOptions options, AuthStrings strings); + @ObjCSelector('authenticateWithOptions:strings:') + AuthResultDetails authenticate(AuthOptions options, AuthStrings strings); } From e6a6a61de47fa1903157982ca1a3c5e188df5ffd Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 12 May 2023 13:43:58 -0400 Subject: [PATCH 05/14] Update native tests, add missing test --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 340 ++++++++---------- 1 file changed, 150 insertions(+), 190 deletions(-) diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index f6e6c0ad422..4c673d4885c 100644 --- a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -56,7 +56,7 @@ - (void)testSuccessfullAuthWithBiometrics { initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -69,23 +69,20 @@ - (void)testSuccessfullAuthWithBiometrics { reply(YES, nil); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(YES), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@YES + sticky:@NO + useErrorDialgs:@NO] + strings:strings + completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(result.value, FLAAuthResultSuccess); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -96,7 +93,7 @@ - (void)testSuccessfullAuthWithoutBiometrics { initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -109,23 +106,20 @@ - (void)testSuccessfullAuthWithoutBiometrics { reply(YES, nil); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialgs:@NO] + strings:strings + completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(result.value, FLAAuthResultSuccess); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -136,7 +130,7 @@ - (void)testFailedAuthWithBiometrics { initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -149,22 +143,23 @@ - (void)testFailedAuthWithBiometrics { reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(YES), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@YES + sticky:@NO + useErrorDialgs:@NO] + strings:strings + completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { + XCTAssertTrue([NSThread 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. + XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -175,7 +170,7 @@ - (void)testFailedWithUnknownErrorCode { initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -188,22 +183,20 @@ - (void)testFailedWithUnknownErrorCode { reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialgs:@NO] + strings:strings + completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -214,7 +207,7 @@ - (void)testSystemCancelledWithoutStickyAuth { initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -227,24 +220,20 @@ - (void)testSystemCancelledWithoutStickyAuth { reply(NO, [NSError errorWithDomain:@"error" code:LAErrorSystemCancel userInfo:nil]); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - @"stickyAuth" : @(NO) - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialgs:@NO] + strings:strings + completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(result.value, FLAAuthResultFailure); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -255,7 +244,7 @@ - (void)testFailedAuthWithoutBiometrics { initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -268,22 +257,23 @@ - (void)testFailedAuthWithoutBiometrics { reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialgs:@NO] + strings:strings + completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { + XCTAssertTrue([NSThread 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. + XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -294,8 +284,8 @@ - (void)testLocalizedFallbackTitle { initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; - NSString *localizedFallbackTitle = @"a title"; + FLAAuthStrings *strings = [self createAuthStrings]; + strings.localizedFallbackTitle = @"a title"; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -308,23 +298,18 @@ - (void)testLocalizedFallbackTitle { reply(YES, nil); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - @"localizedFallbackTitle" : localizedFallbackTitle, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialgs:@NO] + strings:strings + completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { + OCMVerify([mockAuthContext setLocalizedFallbackTitle:strings.localizedFallbackTitle]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -335,7 +320,8 @@ - (void)testSkippedLocalizedFallbackTitle { initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; + strings.localizedFallbackTitle = nil; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -348,21 +334,18 @@ - (void)testSkippedLocalizedFallbackTitle { reply(YES, nil); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialgs:@NO] + strings:strings + completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { + OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -375,18 +358,10 @@ - (void)testDeviceSupportsBiometrics_withEnrolledHardware { const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + FlutterError *error; + NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + XCTAssertNil(error); } - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { @@ -410,18 +385,10 @@ - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) .andDo(canEvaluatePolicyHandler); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + FlutterError *error; + NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + XCTAssertNil(error); } - (void)testDeviceSupportsBiometrics_withNoBiometricHardware { @@ -445,21 +412,13 @@ - (void)testDeviceSupportsBiometrics_withNoBiometricHardware { error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) .andDo(canEvaluatePolicyHandler); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + FlutterError *error; + NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; XCTAssertFalse([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + XCTAssertNil(error); } -- (void)testGetEnrolledBiometrics_withFaceID { +- (void)testGetEnrolledBiometricsWithFaceID { id mockAuthContext = OCMClassMock([LAContext class]); FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] @@ -469,22 +428,14 @@ - (void)testGetEnrolledBiometrics_withFaceID { OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"face"); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FlutterError *error; + NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; + XCTAssertEqual([result count], 1); + XCTAssertEqual(result[0].value, FLAAuthBiometricFace); + XCTAssertNil(error); } -- (void)testGetEnrolledBiometrics_withTouchID { +- (void)testGetEnrolledBiometricsWithTouchID { id mockAuthContext = OCMClassMock([LAContext class]); FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] @@ -494,22 +445,14 @@ - (void)testGetEnrolledBiometrics_withTouchID { OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"fingerprint"); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FlutterError *error; + NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; + XCTAssertEqual([result count], 1); + XCTAssertEqual(result[0].value, FLAAuthBiometricFingerprint); + XCTAssertNil(error); } -- (void)testGetEnrolledBiometrics_withoutEnrolledHardware { +- (void)testGetEnrolledBiometricsWithoutEnrolledHardware { id mockAuthContext = OCMClassMock([LAContext class]); FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] @@ -530,17 +473,34 @@ - (void)testGetEnrolledBiometrics_withoutEnrolledHardware { error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) .andDo(canEvaluatePolicyHandler); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 0); - [expectation fulfill]; - }]; + FlutterError *error; + NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; + XCTAssertEqual([result count], 0); + XCTAssertNil(error); +} + +// TODO(stuartmorgan): Make this multiple tests when fixing +// https://github.com/flutter/flutter/issues/116179 +// Currently it just always returns true. +- (void)testIsDeviceSupported { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ ]]]; + + FlutterError *error; + NSNumber *result = [plugin isDeviceSupportedWithError:&error]; + XCTAssertTrue([result boolValue]); + XCTAssertNil(error); +} + +// Creates an FLAAuthStrings with placeholder values. +- (FLAAuthStrings *)createAuthStrings { + return [FLAAuthStrings makeWithReason:@"a reason" + lockOut:@"locked out" + goToSettingsButton:@"Go To Settings" + goToSettingsDescription:@"Settings" + cancelButton:@"Cancel" + localizedFallbackTitle:nil]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @end From a97c41a395d9f442c950b1cab341481534891fe6 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 12 May 2023 14:10:06 -0400 Subject: [PATCH 06/14] Update Dart implmeentation, modeled on Android --- .../local_auth_ios/lib/local_auth_ios.dart | 114 +++--- .../local_auth_ios/lib/src/messages.g.dart | 333 ++++++++++++++++++ 2 files changed, 407 insertions(+), 40 deletions(-) create mode 100644 packages/local_auth/local_auth_ios/lib/src/messages.g.dart diff --git a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart index 217fd39d990..c521dd17dc7 100644 --- a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart +++ b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart @@ -2,9 +2,11 @@ // 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' show visibleForTesting; import 'package:flutter/services.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; +import 'src/messages.g.dart'; import 'types/auth_messages_ios.dart'; export 'package:local_auth_ios/types/auth_messages_ios.dart'; @@ -12,16 +14,20 @@ export 'package:local_auth_platform_interface/types/auth_messages.dart'; export 'package:local_auth_platform_interface/types/auth_options.dart'; export 'package:local_auth_platform_interface/types/biometric_type.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/local_auth_ios'); - /// The implementation of [LocalAuthPlatform] for iOS. class LocalAuthIOS extends LocalAuthPlatform { + /// Creates a new plugin implementation instance. + LocalAuthIOS({ + @visibleForTesting LocalAuthApi? api, + }) : _api = api ?? LocalAuthApi(); + /// Registers this class as the default instance of [LocalAuthPlatform]. static void registerWith() { LocalAuthPlatform.instance = LocalAuthIOS(); } + final LocalAuthApi _api; + @override Future authenticate({ required String localizedReason, @@ -29,58 +35,86 @@ class LocalAuthIOS extends LocalAuthPlatform { AuthenticationOptions options = const AuthenticationOptions(), }) async { assert(localizedReason.isNotEmpty); - final Map args = { - 'localizedReason': localizedReason, - 'useErrorDialogs': options.useErrorDialogs, - 'stickyAuth': options.stickyAuth, - 'sensitiveTransaction': options.sensitiveTransaction, - 'biometricOnly': options.biometricOnly, - }; - args.addAll(const IOSAuthMessages().args); - for (final AuthMessages messages in authMessages) { - if (messages is IOSAuthMessages) { - args.addAll(messages.args); - } + final AuthResultDetails result = await _api.authenticate( + AuthOptions( + biometricOnly: options.biometricOnly, + sticky: options.stickyAuth, + useErrorDialgs: options.useErrorDialogs), + _pigeonStringsFromAuthMessages(localizedReason, authMessages)); + // TODO(stuartmorgan): Replace this with structured errors, coordinated + // across all platform implementations, per + // https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#platform-exception-handling + // The PlatformExceptions thrown here are for compatibiilty with the + // previous Objective-C implementation. + switch (result.value) { + case AuthResult.success: + return true; + case AuthResult.failure: + return false; + case AuthResult.errorNotAvailable: + throw PlatformException( + code: 'NotAvailable', + message: result.errorMessage, + details: result.errorDetails); + case AuthResult.errorNotEnrolled: + throw PlatformException( + code: 'NotEnrolled', + message: result.errorMessage, + details: result.errorDetails); + case AuthResult.errorPasscodeNotSet: + throw PlatformException( + code: 'PasscodeNotSet', + message: result.errorMessage, + details: result.errorDetails); } - return (await _channel.invokeMethod('authenticate', args)) ?? false; } @override Future deviceSupportsBiometrics() async { - return (await _channel.invokeMethod('deviceSupportsBiometrics')) ?? - false; + return _api.deviceCanSupportBiometrics(); } @override Future> getEnrolledBiometrics() async { - final List result = (await _channel.invokeListMethod( - 'getEnrolledBiometrics', - )) ?? - []; - final List biometrics = []; - for (final String value in result) { - switch (value) { - case 'face': - biometrics.add(BiometricType.face); - break; - case 'fingerprint': - biometrics.add(BiometricType.fingerprint); - break; - case 'iris': - biometrics.add(BiometricType.iris); - break; + final List result = + await _api.getEnrolledBiometrics(); + return result + .cast() + .map((AuthBiometricWrapper entry) { + switch (entry.value) { + case AuthBiometric.face: + return BiometricType.face; + case AuthBiometric.fingerprint: + return BiometricType.fingerprint; } - } - return biometrics; + }).toList(); } @override - Future isDeviceSupported() async => - (await _channel.invokeMethod('isDeviceSupported')) ?? false; + Future isDeviceSupported() async => _api.isDeviceSupported(); /// Always returns false as this method is not supported on iOS. @override - Future stopAuthentication() async { - return false; + Future stopAuthentication() async => false; + + AuthStrings _pigeonStringsFromAuthMessages( + String localizedReason, Iterable messagesList) { + IOSAuthMessages? messages; + for (final AuthMessages entry in messagesList) { + if (entry is IOSAuthMessages) { + messages = entry; + } + } + 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, + localizedFallbackTitle: messages?.localizedFallbackTitle, + ); } } diff --git a/packages/local_auth/local_auth_ios/lib/src/messages.g.dart b/packages/local_auth/local_auth_ios/lib/src/messages.g.dart new file mode 100644 index 00000000000..554ade82c63 --- /dev/null +++ b/packages/local_auth/local_auth_ios/lib/src/messages.g.dart @@ -0,0 +1,333 @@ +// 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 (v9.2.5), 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 + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +/// Possible outcomes of an authentication attempt. +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, +} + +/// Pigeon equivalent of the subset of BiometricType used by iOS. +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.lockOut, + required this.goToSettingsButton, + required this.goToSettingsDescription, + required this.cancelButton, + this.localizedFallbackTitle, + }); + + String reason; + + String lockOut; + + String goToSettingsButton; + + String goToSettingsDescription; + + String cancelButton; + + String? localizedFallbackTitle; + + Object encode() { + return [ + reason, + lockOut, + goToSettingsButton, + goToSettingsDescription, + cancelButton, + localizedFallbackTitle, + ]; + } + + static AuthStrings decode(Object result) { + 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?, + ); + } +} + +class AuthOptions { + AuthOptions({ + required this.biometricOnly, + required this.sticky, + required this.useErrorDialgs, + }); + + bool biometricOnly; + + bool sticky; + + bool useErrorDialgs; + + Object encode() { + return [ + biometricOnly, + sticky, + useErrorDialgs, + ]; + } + + static AuthOptions decode(Object result) { + result as List; + return AuthOptions( + biometricOnly: result[0]! as bool, + sticky: result[1]! as bool, + useErrorDialgs: result[2]! as bool, + ); + } +} + +class AuthResultDetails { + AuthResultDetails({ + required this.value, + this.errorMessage, + this.errorDetails, + }); + + /// The result of authenticating. + AuthResult value; + + /// A system-provided error message, if any. + String? errorMessage; + + /// System-provided error details, if any. + String? errorDetails; + + Object encode() { + return [ + value.index, + errorMessage, + errorDetails, + ]; + } + + static AuthResultDetails decode(Object result) { + result as List; + return AuthResultDetails( + value: AuthResult.values[result[0]! as int], + errorMessage: result[1] as String?, + errorDetails: result[2] as String?, + ); + } +} + +class AuthBiometricWrapper { + AuthBiometricWrapper({ + required this.value, + }); + + AuthBiometric value; + + Object encode() { + return [ + value.index, + ]; + } + + static AuthBiometricWrapper decode(Object result) { + result as List; + return AuthBiometricWrapper( + value: AuthBiometric.values[result[0]! as int], + ); + } +} + +class _LocalAuthApiCodec extends StandardMessageCodec { + const _LocalAuthApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is AuthBiometricWrapper) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is AuthOptions) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is AuthResultDetails) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is AuthStrings) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return AuthBiometricWrapper.decode(readValue(buffer)!); + case 129: + return AuthOptions.decode(readValue(buffer)!); + case 130: + return AuthResultDetails.decode(readValue(buffer)!); + case 131: + return AuthStrings.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class LocalAuthApi { + /// Constructor for [LocalAuthApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + LocalAuthApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _LocalAuthApiCodec(); + + /// Returns true if this device supports authentication. + Future isDeviceSupported() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.isDeviceSupported', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Returns true if this device can support biometric authentication, whether + /// any biometrics are enrolled or not. + Future deviceCanSupportBiometrics() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.deviceCanSupportBiometrics', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Returns the biometric types that are enrolled, and can thus be used + /// without additional setup. + Future> getEnrolledBiometrics() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.getEnrolledBiometrics', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } + + /// Attempts to authenticate the user with the provided [options], and using + /// [strings] for any UI. + Future authenticate( + AuthOptions arg_options, AuthStrings arg_strings) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.authenticate', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_options, arg_strings]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as AuthResultDetails?)!; + } + } +} From ecd6e936b00779d835f9a01aca3ebcc3536d9a5a Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 12 May 2023 14:25:17 -0400 Subject: [PATCH 07/14] Replace Dart unit tests, based on Android --- .../local_auth/local_auth_ios/pubspec.yaml | 1 + .../test/local_auth_ios_test.dart | 330 ++++++++++++++++++ .../test/local_auth_ios_test.mocks.dart | 92 +++++ .../local_auth_ios/test/local_auth_test.dart | 191 ---------- 4 files changed, 423 insertions(+), 191 deletions(-) create mode 100644 packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart create mode 100644 packages/local_auth/local_auth_ios/test/local_auth_ios_test.mocks.dart delete mode 100644 packages/local_auth/local_auth_ios/test/local_auth_test.dart diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index 199461e6e08..88a59d06389 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -26,4 +26,5 @@ dev_dependencies: build_runner: ^2.3.3 flutter_test: sdk: flutter + mockito: 5.4.0 pigeon: ^9.2.4 diff --git a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart new file mode 100644 index 00000000000..f9612256477 --- /dev/null +++ b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart @@ -0,0 +1,330 @@ +// 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/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +import 'package:local_auth_ios/src/messages.g.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'local_auth_ios_test.mocks.dart'; + +@GenerateMocks([LocalAuthApi]) +void main() { + late MockLocalAuthApi api; + late LocalAuthIOS plugin; + + setUp(() { + api = MockLocalAuthApi(); + plugin = LocalAuthIOS(api: api); + }); + + test('registers instance', () { + LocalAuthIOS.registerWith(); + expect(LocalAuthPlatform.instance, isA()); + }); + + group('deviceSupportsBiometrics', () { + test('handles true', () async { + when(api.deviceCanSupportBiometrics()).thenAnswer((_) async => true); + expect(await plugin.deviceSupportsBiometrics(), true); + }); + + test('handles false', () async { + when(api.deviceCanSupportBiometrics()).thenAnswer((_) async => false); + expect(await plugin.deviceSupportsBiometrics(), false); + }); + }); + + group('isDeviceSupported', () { + test('handles true', () async { + when(api.isDeviceSupported()).thenAnswer((_) async => true); + expect(await plugin.isDeviceSupported(), true); + }); + + test('handles false', () async { + when(api.isDeviceSupported()).thenAnswer((_) async => false); + expect(await plugin.isDeviceSupported(), false); + }); + }); + + group('stopAuthentication', () { + test('always returns false', () async { + expect(await plugin.stopAuthentication(), false); + }); + }); + + group('getEnrolledBiometrics', () { + test('translates values', () async { + when(api.getEnrolledBiometrics()) + .thenAnswer((_) async => [ + AuthBiometricWrapper(value: AuthBiometric.face), + AuthBiometricWrapper(value: AuthBiometric.fingerprint), + ]); + + final List result = await plugin.getEnrolledBiometrics(); + + expect(result, [ + BiometricType.face, + BiometricType.fingerprint, + ]); + }); + + test('handles emtpy', () async { + when(api.getEnrolledBiometrics()) + .thenAnswer((_) async => []); + + final List result = await plugin.getEnrolledBiometrics(); + + expect(result, []); + }); + }); + + group('authenticate', () { + group('strings', () { + test('passes default values when nothing is provided', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(value: AuthResult.success)); + + const String reason = 'test reason'; + await plugin.authenticate( + localizedReason: reason, authMessages: []); + + final VerificationResult result = + verify(api.authenticate(any, captureAny)); + final AuthStrings strings = result.captured[0] as AuthStrings; + 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.localizedFallbackTitle, null); + }); + + test('passes default values when only other platform values are provided', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(value: AuthResult.success)); + + const String reason = 'test reason'; + await plugin.authenticate( + localizedReason: reason, + authMessages: [AnotherPlatformAuthMessages()]); + + final VerificationResult result = + verify(api.authenticate(any, captureAny)); + final AuthStrings strings = result.captured[0] as AuthStrings; + 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.localizedFallbackTitle, null); + }); + + test('passes all non-default values correctly', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(value: AuthResult.success)); + + // These are arbitrary values; all that matters is that: + // - 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'; + await plugin + .authenticate(localizedReason: reason, authMessages: [ + const IOSAuthMessages( + lockOut: lockOut, + goToSettingsButton: goToSettingsButton, + goToSettingsDescription: gotToSettingsDescription, + cancelButton: cancel, + localizedFallbackTitle: localizedFallbackTitle, + ), + AnotherPlatformAuthMessages(), + ]); + + final VerificationResult result = + verify(api.authenticate(any, captureAny)); + 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); + }); + + test('passes provided messages with default fallbacks', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(value: AuthResult.success)); + + // These are arbitrary values; all that matters is that: + // - 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'; + await plugin + .authenticate(localizedReason: reason, authMessages: [ + const IOSAuthMessages( + lockOut: lockOut, + localizedFallbackTitle: localizedFallbackTitle, + cancelButton: cancel, + ), + ]); + + final VerificationResult result = + verify(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.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); + }); + }); + + group('options', () { + test('passes default values', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(value: AuthResult.success)); + + await plugin.authenticate( + localizedReason: 'reason', authMessages: []); + + final VerificationResult result = + verify(api.authenticate(captureAny, any)); + final AuthOptions options = result.captured[0] as AuthOptions; + expect(options.biometricOnly, false); + expect(options.sticky, false); + expect(options.useErrorDialgs, true); + }); + + test('passes provided non-default values', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(value: AuthResult.success)); + + await plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + options: const AuthenticationOptions( + biometricOnly: true, + stickyAuth: true, + useErrorDialogs: false, + )); + + final VerificationResult result = + verify(api.authenticate(captureAny, any)); + final AuthOptions options = result.captured[0] as AuthOptions; + expect(options.biometricOnly, true); + expect(options.sticky, true); + expect(options.useErrorDialgs, false); + }); + }); + + group('return values', () { + test('handles success', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(value: AuthResult.success)); + + final bool result = await plugin.authenticate( + localizedReason: 'reason', authMessages: []); + + expect(result, true); + }); + + test('handles failure', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(value: AuthResult.failure)); + + final bool result = await plugin.authenticate( + localizedReason: 'reason', authMessages: []); + + expect(result, false); + }); + + test('converts errorNotAvailable to legacy PlatformException', () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer((_) async => + AuthResultDetails( + value: AuthResult.errorNotAvailable, + errorMessage: errorMessage, + errorDetails: errorDetails)); + + 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))); + }); + + test('converts errorNotEnrolled to legacy PlatformException', () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer((_) async => + AuthResultDetails( + value: AuthResult.errorNotEnrolled, + errorMessage: errorMessage, + errorDetails: errorDetails)); + + 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))); + }); + + test('converts errorPasscodeNotSet to legacy PlatformException', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer((_) async => + AuthResultDetails( + value: AuthResult.errorPasscodeNotSet, + errorMessage: errorMessage, + errorDetails: errorDetails)); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', authMessages: []), + throwsA(isA() + .having( + (PlatformException e) => e.code, 'code', 'PasscodeNotSet') + .having( + (PlatformException e) => e.message, 'message', errorMessage) + .having((PlatformException e) => e.details, 'details', + errorDetails))); + }); + }); + }); +} + +class AnotherPlatformAuthMessages extends AuthMessages { + @override + Map get args => throw UnimplementedError(); +} diff --git a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.mocks.dart b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.mocks.dart new file mode 100644 index 00000000000..97e0a9dba40 --- /dev/null +++ b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.mocks.dart @@ -0,0 +1,92 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in local_auth_ios/test/local_auth_ios_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:local_auth_ios/src/messages.g.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// 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 _FakeAuthResultDetails_0 extends _i1.SmartFake + implements _i2.AuthResultDetails { + _FakeAuthResultDetails_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [LocalAuthApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi { + MockLocalAuthApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future isDeviceSupported() => (super.noSuchMethod( + Invocation.method( + #isDeviceSupported, + [], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); + @override + _i3.Future deviceCanSupportBiometrics() => (super.noSuchMethod( + Invocation.method( + #deviceCanSupportBiometrics, + [], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); + @override + _i3.Future> getEnrolledBiometrics() => + (super.noSuchMethod( + Invocation.method( + #getEnrolledBiometrics, + [], + ), + returnValue: _i3.Future>.value( + <_i2.AuthBiometricWrapper?>[]), + ) as _i3.Future>); + @override + _i3.Future<_i2.AuthResultDetails> authenticate( + _i2.AuthOptions? arg_options, + _i2.AuthStrings? arg_strings, + ) => + (super.noSuchMethod( + Invocation.method( + #authenticate, + [ + arg_options, + arg_strings, + ], + ), + returnValue: + _i3.Future<_i2.AuthResultDetails>.value(_FakeAuthResultDetails_0( + this, + Invocation.method( + #authenticate, + [ + arg_options, + arg_strings, + ], + ), + )), + ) as _i3.Future<_i2.AuthResultDetails>); +} diff --git a/packages/local_auth/local_auth_ios/test/local_auth_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_test.dart deleted file mode 100644 index 0d7f56d5da9..00000000000 --- a/packages/local_auth/local_auth_ios/test/local_auth_test.dart +++ /dev/null @@ -1,191 +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. - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:local_auth_ios/local_auth_ios.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('LocalAuth', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/local_auth_ios', - ); - - final List log = []; - late LocalAuthIOS localAuthentication; - - setUp(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) { - log.add(methodCall); - switch (methodCall.method) { - case 'getEnrolledBiometrics': - return Future>.value( - ['face', 'fingerprint', 'iris', 'undefined']); - default: - return Future.value(true); - } - }); - localAuthentication = LocalAuthIOS(); - log.clear(); - }); - - test('deviceSupportsBiometrics calls platform', () async { - final bool result = await localAuthentication.deviceSupportsBiometrics(); - - expect( - log, - [ - isMethodCall('deviceSupportsBiometrics', arguments: null), - ], - ); - expect(result, true); - }); - - test('getEnrolledBiometrics calls platform', () async { - final List result = - await localAuthentication.getEnrolledBiometrics(); - - expect( - log, - [ - isMethodCall('getEnrolledBiometrics', arguments: null), - ], - ); - expect(result, [ - BiometricType.face, - BiometricType.fingerprint, - BiometricType.iris - ]); - }); - - test('isDeviceSupported calls platform', () async { - await localAuthentication.isDeviceSupported(); - - expect( - log, - [ - isMethodCall('isDeviceSupported', arguments: null), - ], - ); - }); - - test('stopAuthentication returns false', () async { - final bool result = await localAuthentication.stopAuthentication(); - expect(result, false); - }); - - group('With device auth fail over', () { - test('authenticate with no args.', () async { - await localAuthentication.authenticate( - authMessages: [const IOSAuthMessages()], - localizedReason: 'Needs secure', - options: const AuthenticationOptions(biometricOnly: true), - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - - test('authenticate with no localizedReason.', () async { - await expectLater( - localAuthentication.authenticate( - authMessages: [const IOSAuthMessages()], - localizedReason: '', - options: const AuthenticationOptions(biometricOnly: true), - ), - throwsAssertionError, - ); - }); - }); - - group('With biometrics only', () { - test('authenticate with no args.', () async { - await localAuthentication.authenticate( - authMessages: [const IOSAuthMessages()], - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - - test('authenticate with `localizedFallbackTitle`', () async { - await localAuthentication.authenticate( - authMessages: [ - const IOSAuthMessages(localizedFallbackTitle: 'Enter PIN'), - ], - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - 'localizedFallbackTitle': 'Enter PIN', - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - - test('authenticate with no sensitive transaction.', () async { - await localAuthentication.authenticate( - authMessages: [const IOSAuthMessages()], - localizedReason: 'Insecure', - options: const AuthenticationOptions( - sensitiveTransaction: false, - useErrorDialogs: false, - ), - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': false, - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - }); - }); -} - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; From 99c29631983394a36185f428c2d7bd88334c8ec9 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 12 May 2023 14:35:29 -0400 Subject: [PATCH 08/14] Version bump --- packages/local_auth/local_auth_ios/CHANGELOG.md | 3 ++- packages/local_auth/local_auth_ios/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md index 570f8db0b31..2d47acf2f01 100644 --- a/packages/local_auth/local_auth_ios/CHANGELOG.md +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 1.1.3 +* Migrates internal implementation to Pigeon. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. ## 1.1.2 diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index 88a59d06389..0fa01c1470d 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_ios description: iOS implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.1.2 +version: 1.1.3 environment: sdk: ">=2.18.0 <4.0.0" From 6bd50db7f0660179a27977a154a6fd8386df068a Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 12 May 2023 14:54:43 -0400 Subject: [PATCH 09/14] Autoformat --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 123 ++++++++++-------- 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index 4c673d4885c..e1816a38d7d 100644 --- a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -77,12 +77,13 @@ - (void)testSuccessfullAuthWithBiometrics { sticky:@NO useErrorDialgs:@NO] strings:strings - completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(result.value, FLAAuthResultSuccess); - XCTAssertNil(error); - [expectation fulfill]; - }]; + completion:^(FLAAuthResultDetails *_Nullable result, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(result.value, FLAAuthResultSuccess); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -114,12 +115,13 @@ - (void)testSuccessfullAuthWithoutBiometrics { sticky:@NO useErrorDialgs:@NO] strings:strings - completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(result.value, FLAAuthResultSuccess); - XCTAssertNil(error); - [expectation fulfill]; - }]; + completion:^(FLAAuthResultDetails *_Nullable result, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(result.value, FLAAuthResultSuccess); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -151,15 +153,17 @@ - (void)testFailedAuthWithBiometrics { sticky:@NO useErrorDialgs:@NO] strings:strings - completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { - XCTAssertTrue([NSThread 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. - XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); - XCTAssertNil(error); - [expectation fulfill]; - }]; + completion:^(FLAAuthResultDetails *_Nullable result, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread 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. + XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -191,12 +195,13 @@ - (void)testFailedWithUnknownErrorCode { sticky:@NO useErrorDialgs:@NO] strings:strings - completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); - XCTAssertNil(error); - [expectation fulfill]; - }]; + completion:^(FLAAuthResultDetails *_Nullable result, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -228,12 +233,13 @@ - (void)testSystemCancelledWithoutStickyAuth { sticky:@NO useErrorDialgs:@NO] strings:strings - completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(result.value, FLAAuthResultFailure); - XCTAssertNil(error); - [expectation fulfill]; - }]; + completion:^(FLAAuthResultDetails *_Nullable result, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(result.value, FLAAuthResultFailure); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -265,15 +271,17 @@ - (void)testFailedAuthWithoutBiometrics { sticky:@NO useErrorDialgs:@NO] strings:strings - completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { - XCTAssertTrue([NSThread 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. - XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); - XCTAssertNil(error); - [expectation fulfill]; - }]; + completion:^(FLAAuthResultDetails *_Nullable result, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread 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. + XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -306,10 +314,12 @@ - (void)testLocalizedFallbackTitle { sticky:@NO useErrorDialgs:@NO] strings:strings - completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:strings.localizedFallbackTitle]); - [expectation fulfill]; - }]; + completion:^(FLAAuthResultDetails *_Nullable result, + FlutterError *_Nullable error) { + OCMVerify([mockAuthContext + setLocalizedFallbackTitle:strings.localizedFallbackTitle]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -342,10 +352,11 @@ - (void)testSkippedLocalizedFallbackTitle { sticky:@NO useErrorDialgs:@NO] strings:strings - completion:^(FLAAuthResultDetails * _Nullable result, FlutterError * _Nullable error) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); - [expectation fulfill]; - }]; + completion:^(FLAAuthResultDetails *_Nullable result, + FlutterError *_Nullable error) { + OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } @@ -360,7 +371,7 @@ - (void)testDeviceSupportsBiometrics_withEnrolledHardware { FlutterError *error; NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; - XCTAssertTrue([result boolValue]); + XCTAssertTrue([result boolValue]); XCTAssertNil(error); } @@ -387,7 +398,7 @@ - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { FlutterError *error; NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; - XCTAssertTrue([result boolValue]); + XCTAssertTrue([result boolValue]); XCTAssertNil(error); } @@ -414,7 +425,7 @@ - (void)testDeviceSupportsBiometrics_withNoBiometricHardware { FlutterError *error; NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; - XCTAssertFalse([result boolValue]); + XCTAssertFalse([result boolValue]); XCTAssertNil(error); } @@ -484,12 +495,11 @@ - (void)testGetEnrolledBiometricsWithoutEnrolledHardware { // Currently it just always returns true. - (void)testIsDeviceSupported { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ ]]]; + initWithContextFactory:[[StubAuthContextFactory alloc] initWithContexts:@[]]]; FlutterError *error; NSNumber *result = [plugin isDeviceSupportedWithError:&error]; - XCTAssertTrue([result boolValue]); + XCTAssertTrue([result boolValue]); XCTAssertNil(error); } @@ -501,6 +511,5 @@ - (FLAAuthStrings *)createAuthStrings { goToSettingsDescription:@"Settings" cancelButton:@"Cancel" localizedFallbackTitle:nil]; - } @end From 830a8df3f24cb329b68d16d69de31a4d409f3a58 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 17 May 2023 10:13:52 -0400 Subject: [PATCH 10/14] Typo fix --- .../local_auth/local_auth_ios/test/local_auth_ios_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart index f9612256477..3ba8ac75b86 100644 --- a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart +++ b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart @@ -73,7 +73,7 @@ void main() { ]); }); - test('handles emtpy', () async { + test('handles empty', () async { when(api.getEnrolledBiometrics()) .thenAnswer((_) async => []); From 97948518d5730c62f6784933ae5cfe9524fb3c05 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 18 May 2023 09:43:56 -0400 Subject: [PATCH 11/14] Typo fix --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 16 ++++++++-------- .../ios/Classes/FLTLocalAuthPlugin.m | 2 +- .../local_auth_ios/ios/Classes/messages.g.h | 4 ++-- .../local_auth_ios/ios/Classes/messages.g.m | 10 +++++----- .../local_auth_ios/lib/local_auth_ios.dart | 2 +- .../local_auth_ios/lib/src/messages.g.dart | 8 ++++---- .../local_auth_ios/pigeons/messages.dart | 4 ++-- .../local_auth_ios/test/local_auth_ios_test.dart | 4 ++-- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index e1816a38d7d..70bb4178a9e 100644 --- a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -75,7 +75,7 @@ - (void)testSuccessfullAuthWithBiometrics { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@YES sticky:@NO - useErrorDialgs:@NO] + useErrorDialogs:@NO] strings:strings completion:^(FLAAuthResultDetails *_Nullable result, FlutterError *_Nullable error) { @@ -113,7 +113,7 @@ - (void)testSuccessfullAuthWithoutBiometrics { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO sticky:@NO - useErrorDialgs:@NO] + useErrorDialogs:@NO] strings:strings completion:^(FLAAuthResultDetails *_Nullable result, FlutterError *_Nullable error) { @@ -151,7 +151,7 @@ - (void)testFailedAuthWithBiometrics { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@YES sticky:@NO - useErrorDialgs:@NO] + useErrorDialogs:@NO] strings:strings completion:^(FLAAuthResultDetails *_Nullable result, FlutterError *_Nullable error) { @@ -193,7 +193,7 @@ - (void)testFailedWithUnknownErrorCode { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO sticky:@NO - useErrorDialgs:@NO] + useErrorDialogs:@NO] strings:strings completion:^(FLAAuthResultDetails *_Nullable result, FlutterError *_Nullable error) { @@ -231,7 +231,7 @@ - (void)testSystemCancelledWithoutStickyAuth { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO sticky:@NO - useErrorDialgs:@NO] + useErrorDialogs:@NO] strings:strings completion:^(FLAAuthResultDetails *_Nullable result, FlutterError *_Nullable error) { @@ -269,7 +269,7 @@ - (void)testFailedAuthWithoutBiometrics { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO sticky:@NO - useErrorDialgs:@NO] + useErrorDialogs:@NO] strings:strings completion:^(FLAAuthResultDetails *_Nullable result, FlutterError *_Nullable error) { @@ -312,7 +312,7 @@ - (void)testLocalizedFallbackTitle { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO sticky:@NO - useErrorDialgs:@NO] + useErrorDialogs:@NO] strings:strings completion:^(FLAAuthResultDetails *_Nullable result, FlutterError *_Nullable error) { @@ -350,7 +350,7 @@ - (void)testSkippedLocalizedFallbackTitle { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO sticky:@NO - useErrorDialgs:@NO] + useErrorDialogs:@NO] strings:strings completion:^(FLAAuthResultDetails *_Nullable result, FlutterError *_Nullable error) { diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 04ddfbbdb62..867ba5dc878 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -237,7 +237,7 @@ - (void)handleError:(NSError *)authError switch (authError.code) { case LAErrorPasscodeNotSet: case LAErrorBiometryNotEnrolled: - if (options.useErrorDialgs.boolValue) { + if (options.useErrorDialogs.boolValue) { [self showAlertWithMessage:strings.goToSettingsDescription firstButton:strings.cancelButton additionalButton:strings.goToSettingsButton diff --git a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h index 6614c77b8fc..9ae0a5cf9f8 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h +++ b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h @@ -63,10 +63,10 @@ typedef NS_ENUM(NSUInteger, FLAAuthBiometric) { - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithBiometricOnly:(NSNumber *)biometricOnly sticky:(NSNumber *)sticky - useErrorDialgs:(NSNumber *)useErrorDialgs; + useErrorDialogs:(NSNumber *)useErrorDialogs; @property(nonatomic, strong) NSNumber *biometricOnly; @property(nonatomic, strong) NSNumber *sticky; -@property(nonatomic, strong) NSNumber *useErrorDialgs; +@property(nonatomic, strong) NSNumber *useErrorDialogs; @end @interface FLAAuthResultDetails : NSObject diff --git a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m index 0965706b911..ce2050727c8 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m @@ -97,11 +97,11 @@ - (NSArray *)toList { @implementation FLAAuthOptions + (instancetype)makeWithBiometricOnly:(NSNumber *)biometricOnly sticky:(NSNumber *)sticky - useErrorDialgs:(NSNumber *)useErrorDialgs { + useErrorDialogs:(NSNumber *)useErrorDialogs { FLAAuthOptions *pigeonResult = [[FLAAuthOptions alloc] init]; pigeonResult.biometricOnly = biometricOnly; pigeonResult.sticky = sticky; - pigeonResult.useErrorDialgs = useErrorDialgs; + pigeonResult.useErrorDialogs = useErrorDialogs; return pigeonResult; } + (FLAAuthOptions *)fromList:(NSArray *)list { @@ -110,8 +110,8 @@ + (FLAAuthOptions *)fromList:(NSArray *)list { NSAssert(pigeonResult.biometricOnly != nil, @""); pigeonResult.sticky = GetNullableObjectAtIndex(list, 1); NSAssert(pigeonResult.sticky != nil, @""); - pigeonResult.useErrorDialgs = GetNullableObjectAtIndex(list, 2); - NSAssert(pigeonResult.useErrorDialgs != nil, @""); + pigeonResult.useErrorDialogs = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.useErrorDialogs != nil, @""); return pigeonResult; } + (nullable FLAAuthOptions *)nullableFromList:(NSArray *)list { @@ -121,7 +121,7 @@ - (NSArray *)toList { return @[ (self.biometricOnly ?: [NSNull null]), (self.sticky ?: [NSNull null]), - (self.useErrorDialgs ?: [NSNull null]), + (self.useErrorDialogs ?: [NSNull null]), ]; } @end diff --git a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart index c521dd17dc7..9bf45d0900a 100644 --- a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart +++ b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart @@ -39,7 +39,7 @@ class LocalAuthIOS extends LocalAuthPlatform { AuthOptions( biometricOnly: options.biometricOnly, sticky: options.stickyAuth, - useErrorDialgs: options.useErrorDialogs), + useErrorDialogs: options.useErrorDialogs), _pigeonStringsFromAuthMessages(localizedReason, authMessages)); // TODO(stuartmorgan): Replace this with structured errors, coordinated // across all platform implementations, per diff --git a/packages/local_auth/local_auth_ios/lib/src/messages.g.dart b/packages/local_auth/local_auth_ios/lib/src/messages.g.dart index 554ade82c63..b95d8b62498 100644 --- a/packages/local_auth/local_auth_ios/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_ios/lib/src/messages.g.dart @@ -88,20 +88,20 @@ class AuthOptions { AuthOptions({ required this.biometricOnly, required this.sticky, - required this.useErrorDialgs, + required this.useErrorDialogs, }); bool biometricOnly; bool sticky; - bool useErrorDialgs; + bool useErrorDialogs; Object encode() { return [ biometricOnly, sticky, - useErrorDialgs, + useErrorDialogs, ]; } @@ -110,7 +110,7 @@ class AuthOptions { return AuthOptions( biometricOnly: result[0]! as bool, sticky: result[1]! as bool, - useErrorDialgs: result[2]! as bool, + useErrorDialogs: result[2]! as bool, ); } } diff --git a/packages/local_auth/local_auth_ios/pigeons/messages.dart b/packages/local_auth/local_auth_ios/pigeons/messages.dart index 4ae1663dd16..a4675074aaf 100644 --- a/packages/local_auth/local_auth_ios/pigeons/messages.dart +++ b/packages/local_auth/local_auth_ios/pigeons/messages.dart @@ -58,10 +58,10 @@ class AuthOptions { AuthOptions( {required this.biometricOnly, required this.sticky, - required this.useErrorDialgs}); + required this.useErrorDialogs}); final bool biometricOnly; final bool sticky; - final bool useErrorDialgs; + final bool useErrorDialogs; } class AuthResultDetails { diff --git a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart index 3ba8ac75b86..e6ed0f1bb63 100644 --- a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart +++ b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart @@ -213,7 +213,7 @@ void main() { final AuthOptions options = result.captured[0] as AuthOptions; expect(options.biometricOnly, false); expect(options.sticky, false); - expect(options.useErrorDialgs, true); + expect(options.useErrorDialogs, true); }); test('passes provided non-default values', () async { @@ -234,7 +234,7 @@ void main() { final AuthOptions options = result.captured[0] as AuthOptions; expect(options.biometricOnly, true); expect(options.sticky, true); - expect(options.useErrorDialgs, false); + expect(options.useErrorDialogs, false); }); }); From 99f60e9edcb50318ac51f510a625d81cf31c4164 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 18 May 2023 09:52:03 -0400 Subject: [PATCH 12/14] Rename result enum field --- .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 28 +++++++++---------- .../ios/Classes/FLTLocalAuthPlugin.m | 12 ++++---- .../local_auth_ios/ios/Classes/messages.g.h | 8 +++--- .../local_auth_ios/ios/Classes/messages.g.m | 12 ++++---- .../local_auth_ios/lib/local_auth_ios.dart | 17 +++++------ .../local_auth_ios/lib/src/messages.g.dart | 8 +++--- .../local_auth_ios/pigeons/messages.dart | 4 +-- .../test/local_auth_ios_test.dart | 22 +++++++-------- 8 files changed, 56 insertions(+), 55 deletions(-) diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index 70bb4178a9e..dff4c4c11ad 100644 --- a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -77,10 +77,10 @@ - (void)testSuccessfullAuthWithBiometrics { sticky:@NO useErrorDialogs:@NO] strings:strings - completion:^(FLAAuthResultDetails *_Nullable result, + completion:^(FLAAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(result.value, FLAAuthResultSuccess); + XCTAssertEqual(resultDetails.result, FLAAuthResultSuccess); XCTAssertNil(error); [expectation fulfill]; }]; @@ -115,10 +115,10 @@ - (void)testSuccessfullAuthWithoutBiometrics { sticky:@NO useErrorDialogs:@NO] strings:strings - completion:^(FLAAuthResultDetails *_Nullable result, + completion:^(FLAAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(result.value, FLAAuthResultSuccess); + XCTAssertEqual(resultDetails.result, FLAAuthResultSuccess); XCTAssertNil(error); [expectation fulfill]; }]; @@ -153,14 +153,14 @@ - (void)testFailedAuthWithBiometrics { sticky:@NO useErrorDialogs:@NO] strings:strings - completion:^(FLAAuthResultDetails *_Nullable result, + completion:^(FLAAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { XCTAssertTrue([NSThread 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. - XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); + XCTAssertEqual(resultDetails.result, FLAAuthResultErrorNotAvailable); XCTAssertNil(error); [expectation fulfill]; }]; @@ -195,10 +195,10 @@ - (void)testFailedWithUnknownErrorCode { sticky:@NO useErrorDialogs:@NO] strings:strings - completion:^(FLAAuthResultDetails *_Nullable result, + completion:^(FLAAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); + XCTAssertEqual(resultDetails.result, FLAAuthResultErrorNotAvailable); XCTAssertNil(error); [expectation fulfill]; }]; @@ -233,10 +233,10 @@ - (void)testSystemCancelledWithoutStickyAuth { sticky:@NO useErrorDialogs:@NO] strings:strings - completion:^(FLAAuthResultDetails *_Nullable result, + completion:^(FLAAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(result.value, FLAAuthResultFailure); + XCTAssertEqual(resultDetails.result, FLAAuthResultFailure); XCTAssertNil(error); [expectation fulfill]; }]; @@ -271,14 +271,14 @@ - (void)testFailedAuthWithoutBiometrics { sticky:@NO useErrorDialogs:@NO] strings:strings - completion:^(FLAAuthResultDetails *_Nullable result, + completion:^(FLAAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { XCTAssertTrue([NSThread 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. - XCTAssertEqual(result.value, FLAAuthResultErrorNotAvailable); + XCTAssertEqual(resultDetails.result, FLAAuthResultErrorNotAvailable); XCTAssertNil(error); [expectation fulfill]; }]; @@ -314,7 +314,7 @@ - (void)testLocalizedFallbackTitle { sticky:@NO useErrorDialogs:@NO] strings:strings - completion:^(FLAAuthResultDetails *_Nullable result, + completion:^(FLAAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { OCMVerify([mockAuthContext setLocalizedFallbackTitle:strings.localizedFallbackTitle]); @@ -352,7 +352,7 @@ - (void)testSkippedLocalizedFallbackTitle { sticky:@NO useErrorDialogs:@NO] strings:strings - completion:^(FLAAuthResultDetails *_Nullable result, + completion:^(FLAAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); [expectation fulfill]; diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 867ba5dc878..61315c309c2 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -223,9 +223,9 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success - (void)handleSucceeded:(BOOL)succeeded withCompletion:(nonnull AuthCompletion)completion { completion( - [FLAAuthResultDetails makeWithValue:(succeeded ? FLAAuthResultSuccess : FLAAuthResultFailure) - errorMessage:nil - errorDetails:nil], + [FLAAuthResultDetails makeWithResult:(succeeded ? FLAAuthResultSuccess : FLAAuthResultFailure) + errorMessage:nil + errorDetails:nil], nil); } @@ -254,9 +254,9 @@ - (void)handleError:(NSError *)authError completion:completion]; return; } - completion([FLAAuthResultDetails makeWithValue:result - errorMessage:authError.localizedDescription - errorDetails:authError.domain], + completion([FLAAuthResultDetails makeWithResult:result + errorMessage:authError.localizedDescription + errorDetails:authError.domain], nil); } diff --git a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h index 9ae0a5cf9f8..eda93cd93e6 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h +++ b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h @@ -72,11 +72,11 @@ typedef NS_ENUM(NSUInteger, FLAAuthBiometric) { @interface FLAAuthResultDetails : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithValue:(FLAAuthResult)value - errorMessage:(nullable NSString *)errorMessage - errorDetails:(nullable NSString *)errorDetails; ++ (instancetype)makeWithResult:(FLAAuthResult)result + errorMessage:(nullable NSString *)errorMessage + errorDetails:(nullable NSString *)errorDetails; /// The result of authenticating. -@property(nonatomic, assign) FLAAuthResult value; +@property(nonatomic, assign) FLAAuthResult result; /// A system-provided error message, if any. @property(nonatomic, copy, nullable) NSString *errorMessage; /// System-provided error details, if any. diff --git a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m index ce2050727c8..550e7ada9e2 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m @@ -127,18 +127,18 @@ - (NSArray *)toList { @end @implementation FLAAuthResultDetails -+ (instancetype)makeWithValue:(FLAAuthResult)value - errorMessage:(nullable NSString *)errorMessage - errorDetails:(nullable NSString *)errorDetails { ++ (instancetype)makeWithResult:(FLAAuthResult)result + errorMessage:(nullable NSString *)errorMessage + errorDetails:(nullable NSString *)errorDetails { FLAAuthResultDetails *pigeonResult = [[FLAAuthResultDetails alloc] init]; - pigeonResult.value = value; + pigeonResult.result = result; pigeonResult.errorMessage = errorMessage; pigeonResult.errorDetails = errorDetails; return pigeonResult; } + (FLAAuthResultDetails *)fromList:(NSArray *)list { FLAAuthResultDetails *pigeonResult = [[FLAAuthResultDetails alloc] init]; - pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.result = [GetNullableObjectAtIndex(list, 0) integerValue]; pigeonResult.errorMessage = GetNullableObjectAtIndex(list, 1); pigeonResult.errorDetails = GetNullableObjectAtIndex(list, 2); return pigeonResult; @@ -148,7 +148,7 @@ + (nullable FLAAuthResultDetails *)nullableFromList:(NSArray *)list { } - (NSArray *)toList { return @[ - @(self.value), + @(self.result), (self.errorMessage ?: [NSNull null]), (self.errorDetails ?: [NSNull null]), ]; diff --git a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart index 9bf45d0900a..439b1b30839 100644 --- a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart +++ b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart @@ -35,7 +35,7 @@ class LocalAuthIOS extends LocalAuthPlatform { AuthenticationOptions options = const AuthenticationOptions(), }) async { assert(localizedReason.isNotEmpty); - final AuthResultDetails result = await _api.authenticate( + final AuthResultDetails resultDetails = await _api.authenticate( AuthOptions( biometricOnly: options.biometricOnly, sticky: options.stickyAuth, @@ -46,7 +46,7 @@ class LocalAuthIOS extends LocalAuthPlatform { // https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#platform-exception-handling // The PlatformExceptions thrown here are for compatibiilty with the // previous Objective-C implementation. - switch (result.value) { + switch (resultDetails.result) { case AuthResult.success: return true; case AuthResult.failure: @@ -54,18 +54,18 @@ class LocalAuthIOS extends LocalAuthPlatform { case AuthResult.errorNotAvailable: throw PlatformException( code: 'NotAvailable', - message: result.errorMessage, - details: result.errorDetails); + message: resultDetails.errorMessage, + details: resultDetails.errorDetails); case AuthResult.errorNotEnrolled: throw PlatformException( code: 'NotEnrolled', - message: result.errorMessage, - details: result.errorDetails); + message: resultDetails.errorMessage, + details: resultDetails.errorDetails); case AuthResult.errorPasscodeNotSet: throw PlatformException( code: 'PasscodeNotSet', - message: result.errorMessage, - details: result.errorDetails); + message: resultDetails.errorMessage, + details: resultDetails.errorDetails); } } @@ -103,6 +103,7 @@ class LocalAuthIOS extends LocalAuthPlatform { for (final AuthMessages entry in messagesList) { if (entry is IOSAuthMessages) { messages = entry; + break; } } return AuthStrings( diff --git a/packages/local_auth/local_auth_ios/lib/src/messages.g.dart b/packages/local_auth/local_auth_ios/lib/src/messages.g.dart index b95d8b62498..48ab4e53232 100644 --- a/packages/local_auth/local_auth_ios/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_ios/lib/src/messages.g.dart @@ -117,13 +117,13 @@ class AuthOptions { class AuthResultDetails { AuthResultDetails({ - required this.value, + required this.result, this.errorMessage, this.errorDetails, }); /// The result of authenticating. - AuthResult value; + AuthResult result; /// A system-provided error message, if any. String? errorMessage; @@ -133,7 +133,7 @@ class AuthResultDetails { Object encode() { return [ - value.index, + result.index, errorMessage, errorDetails, ]; @@ -142,7 +142,7 @@ class AuthResultDetails { static AuthResultDetails decode(Object result) { result as List; return AuthResultDetails( - value: AuthResult.values[result[0]! as int], + result: AuthResult.values[result[0]! as int], errorMessage: result[1] as String?, errorDetails: result[2] as String?, ); diff --git a/packages/local_auth/local_auth_ios/pigeons/messages.dart b/packages/local_auth/local_auth_ios/pigeons/messages.dart index a4675074aaf..b230597684f 100644 --- a/packages/local_auth/local_auth_ios/pigeons/messages.dart +++ b/packages/local_auth/local_auth_ios/pigeons/messages.dart @@ -66,10 +66,10 @@ class AuthOptions { class AuthResultDetails { AuthResultDetails( - {required this.value, this.errorMessage, this.errorDetails}); + {required this.result, this.errorMessage, this.errorDetails}); /// The result of authenticating. - final AuthResult value; + final AuthResult result; /// A system-provided error message, if any. final String? errorMessage; diff --git a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart index e6ed0f1bb63..3edfcd7899e 100644 --- a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart +++ b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart @@ -87,7 +87,7 @@ void main() { group('strings', () { test('passes default values when nothing is provided', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(value: AuthResult.success)); + (_) async => AuthResultDetails(result: AuthResult.success)); const String reason = 'test reason'; await plugin.authenticate( @@ -109,7 +109,7 @@ void main() { test('passes default values when only other platform values are provided', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(value: AuthResult.success)); + (_) async => AuthResultDetails(result: AuthResult.success)); const String reason = 'test reason'; await plugin.authenticate( @@ -131,7 +131,7 @@ void main() { test('passes all non-default values correctly', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(value: AuthResult.success)); + (_) async => AuthResultDetails(result: AuthResult.success)); // These are arbitrary values; all that matters is that: // - they are different from the defaults, and @@ -167,7 +167,7 @@ void main() { test('passes provided messages with default fallbacks', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(value: AuthResult.success)); + (_) async => AuthResultDetails(result: AuthResult.success)); // These are arbitrary values; all that matters is that: // - they are different from the defaults, and @@ -203,7 +203,7 @@ void main() { group('options', () { test('passes default values', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(value: AuthResult.success)); + (_) async => AuthResultDetails(result: AuthResult.success)); await plugin.authenticate( localizedReason: 'reason', authMessages: []); @@ -218,7 +218,7 @@ void main() { test('passes provided non-default values', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(value: AuthResult.success)); + (_) async => AuthResultDetails(result: AuthResult.success)); await plugin.authenticate( localizedReason: 'reason', @@ -241,7 +241,7 @@ void main() { group('return values', () { test('handles success', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(value: AuthResult.success)); + (_) async => AuthResultDetails(result: AuthResult.success)); final bool result = await plugin.authenticate( localizedReason: 'reason', authMessages: []); @@ -251,7 +251,7 @@ void main() { test('handles failure', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(value: AuthResult.failure)); + (_) async => AuthResultDetails(result: AuthResult.failure)); final bool result = await plugin.authenticate( localizedReason: 'reason', authMessages: []); @@ -264,7 +264,7 @@ void main() { const String errorDetails = 'some details'; when(api.authenticate(any, any)).thenAnswer((_) async => AuthResultDetails( - value: AuthResult.errorNotAvailable, + result: AuthResult.errorNotAvailable, errorMessage: errorMessage, errorDetails: errorDetails)); @@ -284,7 +284,7 @@ void main() { const String errorDetails = 'some details'; when(api.authenticate(any, any)).thenAnswer((_) async => AuthResultDetails( - value: AuthResult.errorNotEnrolled, + result: AuthResult.errorNotEnrolled, errorMessage: errorMessage, errorDetails: errorDetails)); @@ -305,7 +305,7 @@ void main() { const String errorDetails = 'some details'; when(api.authenticate(any, any)).thenAnswer((_) async => AuthResultDetails( - value: AuthResult.errorPasscodeNotSet, + result: AuthResult.errorPasscodeNotSet, errorMessage: errorMessage, errorDetails: errorDetails)); From 933c0d4167b1f5803918bf086dc3b1359a8072e3 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 18 May 2023 09:56:39 -0400 Subject: [PATCH 13/14] Other review fixes --- .../ios/Classes/FLTLocalAuthPlugin.m | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 61315c309c2..21b72f3b1e7 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -6,7 +6,7 @@ #import -typedef void (^AuthCompletion)(FLAAuthResultDetails *_Nullable, FlutterError *_Nullable); +typedef void (^FLAAuthCompletion)(FLAAuthResultDetails *_Nullable, FlutterError *_Nullable); /** * A default context factory that wraps standard LAContext allocation. @@ -28,16 +28,16 @@ - (LAContext *)createAuthContext { @interface FLAStickyAuthState : NSObject @property(nonatomic, strong, nonnull) FLAAuthOptions *options; @property(nonatomic, strong, nonnull) FLAAuthStrings *strings; -@property(nonatomic, strong, nonnull) AuthCompletion resultHandler; +@property(nonatomic, copy, nonnull) FLAAuthCompletion resultHandler; - (instancetype)initWithOptions:(nonnull FLAAuthOptions *)options strings:(nonnull FLAAuthStrings *)strings - resultHandler:(nonnull AuthCompletion)resultHandler; + resultHandler:(nonnull FLAAuthCompletion)resultHandler; @end @implementation FLAStickyAuthState - (instancetype)initWithOptions:(nonnull FLAAuthOptions *)options strings:(nonnull FLAAuthStrings *)strings - resultHandler:(nonnull AuthCompletion)resultHandler { + resultHandler:(nonnull FLAAuthCompletion)resultHandler { self = [super init]; if (self) { _options = options; @@ -155,15 +155,15 @@ - (nullable NSNumber *)isDeviceSupportedWithError: #pragma mark Private Methods - (void)showAlertWithMessage:(NSString *)message - firstButton:(NSString *)firstButton - additionalButton:(NSString *)secondButton - completion:(AuthCompletion)completion { + dismissButtonTitle:(NSString *)dismissButtonTitle + openSettingsButtonTitle:(NSString *)openSettingsButtonTitle + completion:(FLAAuthCompletion)completion { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:dismissButtonTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self handleSucceeded:NO @@ -171,9 +171,9 @@ - (void)showAlertWithMessage:(NSString *)message }]; [alert addAction:defaultAction]; - if (secondButton != nil) { + if (openSettingsButtonTitle != nil) { UIAlertAction *additionalAction = [UIAlertAction - actionWithTitle:secondButton + actionWithTitle:openSettingsButtonTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; @@ -193,7 +193,7 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success error:(NSError *)error options:(FLAAuthOptions *)options strings:(FLAAuthStrings *)strings - completion:(nonnull AuthCompletion)completion { + completion:(nonnull FLAAuthCompletion)completion { NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); if (success) { [self handleSucceeded:YES withCompletion:completion]; @@ -221,7 +221,7 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success } } -- (void)handleSucceeded:(BOOL)succeeded withCompletion:(nonnull AuthCompletion)completion { +- (void)handleSucceeded:(BOOL)succeeded withCompletion:(nonnull FLAAuthCompletion)completion { completion( [FLAAuthResultDetails makeWithResult:(succeeded ? FLAAuthResultSuccess : FLAAuthResultFailure) errorMessage:nil @@ -232,7 +232,7 @@ - (void)handleSucceeded:(BOOL)succeeded withCompletion:(nonnull AuthCompletion)c - (void)handleError:(NSError *)authError withOptions:(FLAAuthOptions *)options strings:(FLAAuthStrings *)strings - completion:(nonnull AuthCompletion)completion { + completion:(nonnull FLAAuthCompletion)completion { FLAAuthResult result = FLAAuthResultErrorNotAvailable; switch (authError.code) { case LAErrorPasscodeNotSet: From 3e707a89f568dde056d672582e8edeb31811821c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 18 May 2023 10:25:43 -0400 Subject: [PATCH 14/14] Fix call sites --- .../local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 21b72f3b1e7..ea105f3943c 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -239,9 +239,9 @@ - (void)handleError:(NSError *)authError case LAErrorBiometryNotEnrolled: if (options.useErrorDialogs.boolValue) { [self showAlertWithMessage:strings.goToSettingsDescription - firstButton:strings.cancelButton - additionalButton:strings.goToSettingsButton - completion:completion]; + dismissButtonTitle:strings.cancelButton + openSettingsButtonTitle:strings.goToSettingsButton + completion:completion]; return; } result = authError.code == LAErrorPasscodeNotSet ? FLAAuthResultErrorPasscodeNotSet @@ -249,9 +249,9 @@ - (void)handleError:(NSError *)authError break; case LAErrorBiometryLockout: [self showAlertWithMessage:strings.lockOut - firstButton:strings.cancelButton - additionalButton:nil - completion:completion]; + dismissButtonTitle:strings.cancelButton + openSettingsButtonTitle:nil + completion:completion]; return; } completion([FLAAuthResultDetails makeWithResult:result