diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index 5178a9e3b22..edca3d412bd 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.0.0 + +* **BREAKING CHANGES:** + * Switches to `LocalAuthException` for error reporting. + * Removes support for `useErrorDialogs`. + * Renames `biometricHint` to `signInHint` to reflect its usage. + ## 1.0.53 * Removes obsolete code related to supporting SDK <24. diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java index a76b4c769c8..4da8d5f2c36 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java @@ -5,19 +5,10 @@ import android.annotation.SuppressLint; import android.app.Activity; -import android.app.AlertDialog; import android.app.Application; -import android.content.Context; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.provider.Settings; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; @@ -25,6 +16,8 @@ import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; +import io.flutter.plugins.localauth.Messages.AuthResult; +import io.flutter.plugins.localauth.Messages.AuthResultCode; import java.util.concurrent.Executor; /** @@ -38,14 +31,12 @@ class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback /** The callback that handles the result of this authentication process. */ interface AuthCompletionHandler { /** Called when authentication attempt is complete. */ - void complete(Messages.AuthResult authResult); + void complete(AuthResult authResult); } - // This is null when not using v2 embedding; private final Lifecycle lifecycle; private final FragmentActivity activity; private final AuthCompletionHandler completionHandler; - private final boolean useErrorDialogs; private final Messages.AuthStrings strings; private final BiometricPrompt.PromptInfo promptInfo; private final boolean isAuthSticky; @@ -65,14 +56,13 @@ interface AuthCompletionHandler { this.completionHandler = completionHandler; this.strings = strings; this.isAuthSticky = options.getSticky(); - this.useErrorDialogs = options.getUseErrorDialgs(); this.uiThreadExecutor = new UiThreadExecutor(); BiometricPrompt.PromptInfo.Builder promptBuilder = new BiometricPrompt.PromptInfo.Builder() .setDescription(strings.getReason()) .setTitle(strings.getSignInTitle()) - .setSubtitle(strings.getBiometricHint()) + .setSubtitle(strings.getSignInHint()) .setConfirmationRequired(options.getSensitiveTransaction()); int allowedAuthenticators = @@ -120,58 +110,69 @@ private void stop() { @SuppressLint("SwitchIntDef") @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + AuthResultCode code; switch (errorCode) { + case BiometricPrompt.ERROR_USER_CANCELED: + code = AuthResultCode.USER_CANCELED; + break; + case BiometricPrompt.ERROR_NEGATIVE_BUTTON: + code = AuthResultCode.NEGATIVE_BUTTON; + break; case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL: - if (useErrorDialogs) { - showGoToSettingsDialog( - strings.getDeviceCredentialsRequiredTitle(), - strings.getDeviceCredentialsSetupDescription()); - return; - } - completionHandler.complete(Messages.AuthResult.ERROR_NOT_AVAILABLE); + code = AuthResultCode.NO_CREDENTIALS; break; - case BiometricPrompt.ERROR_NO_SPACE: case BiometricPrompt.ERROR_NO_BIOMETRICS: - if (useErrorDialogs) { - showGoToSettingsDialog( - strings.getBiometricRequiredTitle(), strings.getGoToSettingsDescription()); - return; - } - completionHandler.complete(Messages.AuthResult.ERROR_NOT_ENROLLED); + code = AuthResultCode.NOT_ENROLLED; break; case BiometricPrompt.ERROR_HW_UNAVAILABLE: + code = AuthResultCode.HARDWARE_UNAVAILABLE; + break; case BiometricPrompt.ERROR_HW_NOT_PRESENT: - completionHandler.complete(Messages.AuthResult.ERROR_NOT_AVAILABLE); + code = AuthResultCode.NO_HARDWARE; break; case BiometricPrompt.ERROR_LOCKOUT: - completionHandler.complete(Messages.AuthResult.ERROR_LOCKED_OUT_TEMPORARILY); + code = AuthResultCode.LOCKED_OUT_TEMPORARILY; break; case BiometricPrompt.ERROR_LOCKOUT_PERMANENT: - completionHandler.complete(Messages.AuthResult.ERROR_LOCKED_OUT_PERMANENTLY); + code = AuthResultCode.LOCKED_OUT_PERMANENTLY; break; case BiometricPrompt.ERROR_CANCELED: // If we are doing sticky auth and the activity has been paused, // ignore this error. We will start listening again when resumed. if (activityPaused && isAuthSticky) { return; - } else { - completionHandler.complete(Messages.AuthResult.FAILURE); } + code = AuthResultCode.SYSTEM_CANCELED; + break; + case BiometricPrompt.ERROR_TIMEOUT: + code = AuthResultCode.TIMEOUT; + break; + case BiometricPrompt.ERROR_NO_SPACE: + code = AuthResultCode.NO_SPACE; + break; + case BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED: + code = AuthResultCode.SECURITY_UPDATE_REQUIRED; break; default: - completionHandler.complete(Messages.AuthResult.FAILURE); + code = AuthResultCode.UNKNOWN_ERROR; + break; } + completionHandler.complete( + new AuthResult.Builder().setCode(code).setErrorMessage(errString.toString()).build()); stop(); } @Override public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { - completionHandler.complete(Messages.AuthResult.SUCCESS); + completionHandler.complete(new AuthResult.Builder().setCode(AuthResultCode.SUCCESS).build()); stop(); } @Override - public void onAuthenticationFailed() {} + public void onAuthenticationFailed() { + // No-op; this is called for incremental failures. Wait for a final + // resolution via the success or error callbacks. + } /** * If the activity is paused, we keep track because biometric dialog simply returns "User @@ -205,34 +206,6 @@ public void onResume(@NonNull LifecycleOwner owner) { onActivityResumed(null); } - // Suppress inflateParams lint because dialogs do not need to attach to a parent view. - @SuppressLint("InflateParams") - private void showGoToSettingsDialog(String title, String descriptionText) { - View view = LayoutInflater.from(activity).inflate(R.layout.go_to_setting, null, false); - TextView message = view.findViewById(R.id.fingerprint_required); - TextView description = view.findViewById(R.id.go_to_setting_description); - message.setText(title); - description.setText(descriptionText); - Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom); - OnClickListener goToSettingHandler = - (dialog, which) -> { - completionHandler.complete(Messages.AuthResult.FAILURE); - stop(); - activity.startActivity(new Intent(Settings.ACTION_SECURITY_SETTINGS)); - }; - OnClickListener cancelHandler = - (dialog, which) -> { - completionHandler.complete(Messages.AuthResult.FAILURE); - stop(); - }; - new AlertDialog.Builder(context) - .setView(view) - .setPositiveButton(strings.getGoToSettingsButton(), goToSettingHandler) - .setNegativeButton(strings.getCancelButton(), cancelHandler) - .setCancelable(false) - .show(); - } - // Unused methods for activity lifecycle. @Override diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index 10b87611343..7b02af92382 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -4,13 +4,11 @@ package io.flutter.plugins.localauth; -import static android.app.Activity.RESULT_OK; import static android.content.Context.KEYGUARD_SERVICE; import android.app.Activity; import android.app.KeyguardManager; import android.content.Context; -import android.content.Intent; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -21,11 +19,11 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; -import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; import io.flutter.plugins.localauth.Messages.AuthClassification; import io.flutter.plugins.localauth.Messages.AuthOptions; import io.flutter.plugins.localauth.Messages.AuthResult; +import io.flutter.plugins.localauth.Messages.AuthResultCode; import io.flutter.plugins.localauth.Messages.AuthStrings; import io.flutter.plugins.localauth.Messages.LocalAuthApi; import io.flutter.plugins.localauth.Messages.Result; @@ -39,32 +37,14 @@ *

Instantiate this in an add to app scenario to gracefully handle activity and context changes. */ public class LocalAuthPlugin implements FlutterPlugin, ActivityAware, LocalAuthApi { - private static final int LOCK_REQUEST_CODE = 221; private Activity activity; private AuthenticationHelper authHelper; @VisibleForTesting final AtomicBoolean authInProgress = new AtomicBoolean(false); - // These are null when not using v2 embedding. private Lifecycle lifecycle; private BiometricManager biometricManager; private KeyguardManager keyguardManager; - Result lockRequestResult; - private final PluginRegistry.ActivityResultListener resultListener = - new PluginRegistry.ActivityResultListener() { - @Override - public boolean onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == LOCK_REQUEST_CODE) { - if (resultCode == RESULT_OK && lockRequestResult != null) { - onAuthenticationCompleted(lockRequestResult, AuthResult.SUCCESS); - } else { - onAuthenticationCompleted(lockRequestResult, AuthResult.FAILURE); - } - lockRequestResult = null; - } - return false; - } - }; /** * Default constructor for LocalAuthPlugin. @@ -73,15 +53,21 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { */ public LocalAuthPlugin() {} + @Override public @NonNull Boolean isDeviceSupported() { return isDeviceSecure() || canAuthenticateWithBiometrics(); } + @Override public @NonNull Boolean deviceCanSupportBiometrics() { return hasBiometricHardware(); } + @Override public @NonNull List getEnrolledBiometrics() { + if (biometricManager == null) { + return null; + } ArrayList biometrics = new ArrayList<>(); if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS) { @@ -94,6 +80,7 @@ public LocalAuthPlugin() {} return biometrics; } + @Override public @NonNull Boolean stopAuthentication() { try { if (authHelper != null && authInProgress.get()) { @@ -107,27 +94,29 @@ public LocalAuthPlugin() {} } } + @Override public void authenticate( @NonNull AuthOptions options, @NonNull AuthStrings strings, @NonNull Result result) { if (authInProgress.get()) { - result.success(AuthResult.ERROR_ALREADY_IN_PROGRESS); + result.success(new AuthResult.Builder().setCode(AuthResultCode.ALREADY_IN_PROGRESS).build()); return; } if (activity == null || activity.isFinishing()) { - result.success(AuthResult.ERROR_NO_ACTIVITY); + result.success(new AuthResult.Builder().setCode(AuthResultCode.NO_ACTIVITY).build()); return; } if (!(activity instanceof FragmentActivity)) { - result.success(AuthResult.ERROR_NOT_FRAGMENT_ACTIVITY); + result.success( + new AuthResult.Builder().setCode(AuthResultCode.NOT_FRAGMENT_ACTIVITY).build()); return; } if (!isDeviceSupported()) { - result.success(AuthResult.ERROR_NOT_AVAILABLE); + result.success(new AuthResult.Builder().setCode(AuthResultCode.NO_CREDENTIALS).build()); return; } @@ -221,7 +210,6 @@ private void setServicesFromActivity(Activity activity) { @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { - binding.addActivityResultListener(resultListener); setServicesFromActivity(binding.getActivity()); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); } @@ -234,7 +222,6 @@ public void onDetachedFromActivityForConfigChanges() { @Override public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { - binding.addActivityResultListener(resultListener); setServicesFromActivity(binding.getActivity()); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); } diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java index 5ed7cf45567..667d6656992 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.localauth; @@ -66,29 +66,49 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { @interface CanIgnoreReturnValue {} /** Possible outcomes of an authentication attempt. */ - public enum AuthResult { + public enum AuthResultCode { /** The user authenticated successfully. */ SUCCESS(0), - /** The user failed to successfully authenticate. */ - FAILURE(1), + /** The user pressed the negative button, which corresponds to [AuthStrings.cancelButton]. */ + NEGATIVE_BUTTON(1), + /** + * The user canceled authentication without pressing the negative button. + * + *

This may be triggered by a swipe or a back button, for example. + */ + USER_CANCELED(2), + /** Authentication was caneceled by the system. */ + SYSTEM_CANCELED(3), + /** Authentication timed out. */ + TIMEOUT(4), /** An authentication was already in progress. */ - ERROR_ALREADY_IN_PROGRESS(2), + ALREADY_IN_PROGRESS(5), /** There is no foreground activity. */ - ERROR_NO_ACTIVITY(3), + NO_ACTIVITY(6), /** The foreground activity is not a FragmentActivity. */ - ERROR_NOT_FRAGMENT_ACTIVITY(4), - /** The authentication system was not available. */ - ERROR_NOT_AVAILABLE(5), + NOT_FRAGMENT_ACTIVITY(7), + /** The device does not have any credentials available. */ + NO_CREDENTIALS(8), + /** No biometric hardware is present. */ + NO_HARDWARE(9), + /** The biometric is temporarily unavailable. */ + HARDWARE_UNAVAILABLE(10), /** No biometrics are enrolled. */ - ERROR_NOT_ENROLLED(6), + NOT_ENROLLED(11), /** The user is locked out temporarily due to too many failed attempts. */ - ERROR_LOCKED_OUT_TEMPORARILY(7), + LOCKED_OUT_TEMPORARILY(12), /** The user is locked out until they log in another way due to too many failed attempts. */ - ERROR_LOCKED_OUT_PERMANENTLY(8); + LOCKED_OUT_PERMANENTLY(13), + /** The device does not have enough storage to complete authentication. */ + NO_SPACE(14), + /** The hardware is unavailable until a security update is performed. */ + SECURITY_UPDATE_REQUIRED(15), + /** Some unrecognized error case was encountered */ + UNKNOWN_ERROR(16); final int index; - AuthResult(final int index) { + AuthResultCode(final int index) { this.index = index; } } @@ -126,43 +146,17 @@ public void setReason(@NonNull String setterArg) { this.reason = setterArg; } - private @NonNull String biometricHint; - - public @NonNull String getBiometricHint() { - return biometricHint; - } - - public void setBiometricHint(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"biometricHint\" is null."); - } - this.biometricHint = setterArg; - } - - private @NonNull String biometricNotRecognized; + private @NonNull String signInHint; - public @NonNull String getBiometricNotRecognized() { - return biometricNotRecognized; + public @NonNull String getSignInHint() { + return signInHint; } - public void setBiometricNotRecognized(@NonNull String setterArg) { + public void setSignInHint(@NonNull String setterArg) { if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"biometricNotRecognized\" is null."); + throw new IllegalStateException("Nonnull field \"signInHint\" is null."); } - this.biometricNotRecognized = setterArg; - } - - private @NonNull String biometricRequiredTitle; - - public @NonNull String getBiometricRequiredTitle() { - return biometricRequiredTitle; - } - - public void setBiometricRequiredTitle(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"biometricRequiredTitle\" is null."); - } - this.biometricRequiredTitle = setterArg; + this.signInHint = setterArg; } private @NonNull String cancelButton; @@ -178,60 +172,6 @@ public void setCancelButton(@NonNull String setterArg) { this.cancelButton = setterArg; } - private @NonNull String deviceCredentialsRequiredTitle; - - public @NonNull String getDeviceCredentialsRequiredTitle() { - return deviceCredentialsRequiredTitle; - } - - public void setDeviceCredentialsRequiredTitle(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException( - "Nonnull field \"deviceCredentialsRequiredTitle\" is null."); - } - this.deviceCredentialsRequiredTitle = setterArg; - } - - private @NonNull String deviceCredentialsSetupDescription; - - public @NonNull String getDeviceCredentialsSetupDescription() { - return deviceCredentialsSetupDescription; - } - - public void setDeviceCredentialsSetupDescription(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException( - "Nonnull field \"deviceCredentialsSetupDescription\" is null."); - } - this.deviceCredentialsSetupDescription = setterArg; - } - - private @NonNull String goToSettingsButton; - - public @NonNull String getGoToSettingsButton() { - return goToSettingsButton; - } - - public void setGoToSettingsButton(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"goToSettingsButton\" is null."); - } - this.goToSettingsButton = setterArg; - } - - private @NonNull String goToSettingsDescription; - - public @NonNull String getGoToSettingsDescription() { - return goToSettingsDescription; - } - - public void setGoToSettingsDescription(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"goToSettingsDescription\" is null."); - } - this.goToSettingsDescription = setterArg; - } - private @NonNull String signInTitle; public @NonNull String getSignInTitle() { @@ -258,30 +198,14 @@ public boolean equals(Object o) { } AuthStrings that = (AuthStrings) o; return reason.equals(that.reason) - && biometricHint.equals(that.biometricHint) - && biometricNotRecognized.equals(that.biometricNotRecognized) - && biometricRequiredTitle.equals(that.biometricRequiredTitle) + && signInHint.equals(that.signInHint) && cancelButton.equals(that.cancelButton) - && deviceCredentialsRequiredTitle.equals(that.deviceCredentialsRequiredTitle) - && deviceCredentialsSetupDescription.equals(that.deviceCredentialsSetupDescription) - && goToSettingsButton.equals(that.goToSettingsButton) - && goToSettingsDescription.equals(that.goToSettingsDescription) && signInTitle.equals(that.signInTitle); } @Override public int hashCode() { - return Objects.hash( - reason, - biometricHint, - biometricNotRecognized, - biometricRequiredTitle, - cancelButton, - deviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription, - goToSettingsButton, - goToSettingsDescription, - signInTitle); + return Objects.hash(reason, signInHint, cancelButton, signInTitle); } public static final class Builder { @@ -294,27 +218,11 @@ public static final class Builder { return this; } - private @Nullable String biometricHint; + private @Nullable String signInHint; @CanIgnoreReturnValue - public @NonNull Builder setBiometricHint(@NonNull String setterArg) { - this.biometricHint = setterArg; - return this; - } - - private @Nullable String biometricNotRecognized; - - @CanIgnoreReturnValue - public @NonNull Builder setBiometricNotRecognized(@NonNull String setterArg) { - this.biometricNotRecognized = setterArg; - return this; - } - - private @Nullable String biometricRequiredTitle; - - @CanIgnoreReturnValue - public @NonNull Builder setBiometricRequiredTitle(@NonNull String setterArg) { - this.biometricRequiredTitle = setterArg; + public @NonNull Builder setSignInHint(@NonNull String setterArg) { + this.signInHint = setterArg; return this; } @@ -326,38 +234,6 @@ public static final class Builder { return this; } - private @Nullable String deviceCredentialsRequiredTitle; - - @CanIgnoreReturnValue - public @NonNull Builder setDeviceCredentialsRequiredTitle(@NonNull String setterArg) { - this.deviceCredentialsRequiredTitle = setterArg; - return this; - } - - private @Nullable String deviceCredentialsSetupDescription; - - @CanIgnoreReturnValue - public @NonNull Builder setDeviceCredentialsSetupDescription(@NonNull String setterArg) { - this.deviceCredentialsSetupDescription = setterArg; - return this; - } - - private @Nullable String goToSettingsButton; - - @CanIgnoreReturnValue - public @NonNull Builder setGoToSettingsButton(@NonNull String setterArg) { - this.goToSettingsButton = setterArg; - return this; - } - - private @Nullable String goToSettingsDescription; - - @CanIgnoreReturnValue - public @NonNull Builder setGoToSettingsDescription(@NonNull String setterArg) { - this.goToSettingsDescription = setterArg; - return this; - } - private @Nullable String signInTitle; @CanIgnoreReturnValue @@ -369,14 +245,8 @@ public static final class Builder { public @NonNull AuthStrings build() { AuthStrings pigeonReturn = new AuthStrings(); pigeonReturn.setReason(reason); - pigeonReturn.setBiometricHint(biometricHint); - pigeonReturn.setBiometricNotRecognized(biometricNotRecognized); - pigeonReturn.setBiometricRequiredTitle(biometricRequiredTitle); + pigeonReturn.setSignInHint(signInHint); pigeonReturn.setCancelButton(cancelButton); - pigeonReturn.setDeviceCredentialsRequiredTitle(deviceCredentialsRequiredTitle); - pigeonReturn.setDeviceCredentialsSetupDescription(deviceCredentialsSetupDescription); - pigeonReturn.setGoToSettingsButton(goToSettingsButton); - pigeonReturn.setGoToSettingsDescription(goToSettingsDescription); pigeonReturn.setSignInTitle(signInTitle); return pigeonReturn; } @@ -384,16 +254,10 @@ public static final class Builder { @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(10); + ArrayList toListResult = new ArrayList<>(4); toListResult.add(reason); - toListResult.add(biometricHint); - toListResult.add(biometricNotRecognized); - toListResult.add(biometricRequiredTitle); + toListResult.add(signInHint); toListResult.add(cancelButton); - toListResult.add(deviceCredentialsRequiredTitle); - toListResult.add(deviceCredentialsSetupDescription); - toListResult.add(goToSettingsButton); - toListResult.add(goToSettingsDescription); toListResult.add(signInTitle); return toListResult; } @@ -402,28 +266,111 @@ ArrayList toList() { AuthStrings pigeonResult = new AuthStrings(); Object reason = pigeonVar_list.get(0); pigeonResult.setReason((String) reason); - Object biometricHint = pigeonVar_list.get(1); - pigeonResult.setBiometricHint((String) biometricHint); - Object biometricNotRecognized = pigeonVar_list.get(2); - pigeonResult.setBiometricNotRecognized((String) biometricNotRecognized); - Object biometricRequiredTitle = pigeonVar_list.get(3); - pigeonResult.setBiometricRequiredTitle((String) biometricRequiredTitle); - Object cancelButton = pigeonVar_list.get(4); + Object signInHint = pigeonVar_list.get(1); + pigeonResult.setSignInHint((String) signInHint); + Object cancelButton = pigeonVar_list.get(2); pigeonResult.setCancelButton((String) cancelButton); - Object deviceCredentialsRequiredTitle = pigeonVar_list.get(5); - pigeonResult.setDeviceCredentialsRequiredTitle((String) deviceCredentialsRequiredTitle); - Object deviceCredentialsSetupDescription = pigeonVar_list.get(6); - pigeonResult.setDeviceCredentialsSetupDescription((String) deviceCredentialsSetupDescription); - Object goToSettingsButton = pigeonVar_list.get(7); - pigeonResult.setGoToSettingsButton((String) goToSettingsButton); - Object goToSettingsDescription = pigeonVar_list.get(8); - pigeonResult.setGoToSettingsDescription((String) goToSettingsDescription); - Object signInTitle = pigeonVar_list.get(9); + Object signInTitle = pigeonVar_list.get(3); pigeonResult.setSignInTitle((String) signInTitle); return pigeonResult; } } + /** + * The results of an authentication request. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class AuthResult { + /** The specific result returned from the SDK. */ + private @NonNull AuthResultCode code; + + public @NonNull AuthResultCode getCode() { + return code; + } + + public void setCode(@NonNull AuthResultCode setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"code\" is null."); + } + this.code = setterArg; + } + + /** The error message associated with the result, if any. */ + private @Nullable String errorMessage; + + public @Nullable String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(@Nullable String setterArg) { + this.errorMessage = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + AuthResult() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AuthResult that = (AuthResult) o; + return code.equals(that.code) && Objects.equals(errorMessage, that.errorMessage); + } + + @Override + public int hashCode() { + return Objects.hash(code, errorMessage); + } + + public static final class Builder { + + private @Nullable AuthResultCode code; + + @CanIgnoreReturnValue + public @NonNull Builder setCode(@NonNull AuthResultCode setterArg) { + this.code = setterArg; + return this; + } + + private @Nullable String errorMessage; + + @CanIgnoreReturnValue + public @NonNull Builder setErrorMessage(@Nullable String setterArg) { + this.errorMessage = setterArg; + return this; + } + + public @NonNull AuthResult build() { + AuthResult pigeonReturn = new AuthResult(); + pigeonReturn.setCode(code); + pigeonReturn.setErrorMessage(errorMessage); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(code); + toListResult.add(errorMessage); + return toListResult; + } + + static @NonNull AuthResult fromList(@NonNull ArrayList pigeonVar_list) { + AuthResult pigeonResult = new AuthResult(); + Object code = pigeonVar_list.get(0); + pigeonResult.setCode((AuthResultCode) code); + Object errorMessage = pigeonVar_list.get(1); + pigeonResult.setErrorMessage((String) errorMessage); + return pigeonResult; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class AuthOptions { private @NonNull Boolean biometricOnly; @@ -465,19 +412,6 @@ public void setSticky(@NonNull Boolean setterArg) { this.sticky = setterArg; } - private @NonNull Boolean useErrorDialgs; - - public @NonNull Boolean getUseErrorDialgs() { - return useErrorDialgs; - } - - public void setUseErrorDialgs(@NonNull Boolean setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"useErrorDialgs\" is null."); - } - this.useErrorDialgs = setterArg; - } - /** Constructor is non-public to enforce null safety; use Builder. */ AuthOptions() {} @@ -492,13 +426,12 @@ public boolean equals(Object o) { AuthOptions that = (AuthOptions) o; return biometricOnly.equals(that.biometricOnly) && sensitiveTransaction.equals(that.sensitiveTransaction) - && sticky.equals(that.sticky) - && useErrorDialgs.equals(that.useErrorDialgs); + && sticky.equals(that.sticky); } @Override public int hashCode() { - return Objects.hash(biometricOnly, sensitiveTransaction, sticky, useErrorDialgs); + return Objects.hash(biometricOnly, sensitiveTransaction, sticky); } public static final class Builder { @@ -527,31 +460,21 @@ public static final class Builder { return this; } - private @Nullable Boolean useErrorDialgs; - - @CanIgnoreReturnValue - public @NonNull Builder setUseErrorDialgs(@NonNull Boolean setterArg) { - this.useErrorDialgs = setterArg; - return this; - } - public @NonNull AuthOptions build() { AuthOptions pigeonReturn = new AuthOptions(); pigeonReturn.setBiometricOnly(biometricOnly); pigeonReturn.setSensitiveTransaction(sensitiveTransaction); pigeonReturn.setSticky(sticky); - pigeonReturn.setUseErrorDialgs(useErrorDialgs); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(4); + ArrayList toListResult = new ArrayList<>(3); toListResult.add(biometricOnly); toListResult.add(sensitiveTransaction); toListResult.add(sticky); - toListResult.add(useErrorDialgs); return toListResult; } @@ -563,8 +486,6 @@ ArrayList toList() { pigeonResult.setSensitiveTransaction((Boolean) sensitiveTransaction); Object sticky = pigeonVar_list.get(2); pigeonResult.setSticky((Boolean) sticky); - Object useErrorDialgs = pigeonVar_list.get(3); - pigeonResult.setUseErrorDialgs((Boolean) useErrorDialgs); return pigeonResult; } } @@ -580,7 +501,7 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 129: { Object value = readValue(buffer); - return value == null ? null : AuthResult.values()[((Long) value).intValue()]; + return value == null ? null : AuthResultCode.values()[((Long) value).intValue()]; } case (byte) 130: { @@ -590,6 +511,8 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 131: return AuthStrings.fromList((ArrayList) readValue(buffer)); case (byte) 132: + return AuthResult.fromList((ArrayList) readValue(buffer)); + case (byte) 133: return AuthOptions.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -598,17 +521,20 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { @Override protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof AuthResult) { + if (value instanceof AuthResultCode) { stream.write(129); - writeValue(stream, value == null ? null : ((AuthResult) value).index); + writeValue(stream, value == null ? null : ((AuthResultCode) value).index); } else if (value instanceof AuthClassification) { stream.write(130); writeValue(stream, value == null ? null : ((AuthClassification) value).index); } else if (value instanceof AuthStrings) { stream.write(131); writeValue(stream, ((AuthStrings) value).toList()); - } else if (value instanceof AuthOptions) { + } else if (value instanceof AuthResult) { stream.write(132); + writeValue(stream, ((AuthResult) value).toList()); + } else if (value instanceof AuthOptions) { + stream.write(133); writeValue(stream, ((AuthOptions) value).toList()); } else { super.writeValue(stream, value); @@ -660,8 +586,11 @@ public interface LocalAuthApi { Boolean stopAuthentication(); /** * Returns the biometric types that are enrolled, and can thus be used without additional setup. + * + *

Returns null if there is no activity, in which case the enrolled biometrics can't be + * determined. */ - @NonNull + @Nullable List getEnrolledBiometrics(); /** * Attempts to authenticate the user with the provided [options], and using [strings] for any diff --git a/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml b/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml deleted file mode 100644 index 902635ef543..00000000000 --- a/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java index bd9a5a27b59..d6020fa396f 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java @@ -15,6 +15,7 @@ import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; import io.flutter.plugins.localauth.Messages.AuthOptions; import io.flutter.plugins.localauth.Messages.AuthResult; +import io.flutter.plugins.localauth.Messages.AuthResultCode; import io.flutter.plugins.localauth.Messages.AuthStrings; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,14 +29,8 @@ public class AuthenticationHelperTest { static final AuthStrings dummyStrings = new AuthStrings.Builder() .setReason("a reason") - .setBiometricHint("a hint") - .setBiometricNotRecognized("biometric not recognized") - .setBiometricRequiredTitle("biometric required") + .setSignInHint("a hint") .setCancelButton("cancel") - .setDeviceCredentialsRequiredTitle("credentials required") - .setDeviceCredentialsSetupDescription("credentials setup description") - .setGoToSettingsButton("go") - .setGoToSettingsDescription("go to settings description") .setSignInTitle("sign in") .build(); @@ -44,11 +39,54 @@ public class AuthenticationHelperTest { .setBiometricOnly(false) .setSensitiveTransaction(false) .setSticky(false) - .setUseErrorDialgs(false) .build(); @Test - public void onAuthenticationError_withoutDialogs_returnsNotAvailableForNoCredential() { + public void onAuthenticationError_returnsUserCanceled() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_USER_CANCELED, ""); + + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.USER_CANCELED) + .setErrorMessage("") + .build()); + } + + @Test + public void onAuthenticationError_returnsNegativeButton() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_NEGATIVE_BUTTON, ""); + + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.NEGATIVE_BUTTON) + .setErrorMessage("") + .build()); + } + + @Test + public void onAuthenticationError_withoutDialogs_returnsNoCredential() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -61,7 +99,12 @@ public void onAuthenticationError_withoutDialogs_returnsNotAvailableForNoCredent helper.onAuthenticationError(BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL, ""); - verify(handler).complete(AuthResult.ERROR_NOT_AVAILABLE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.NO_CREDENTIALS) + .setErrorMessage("") + .build()); } @Test @@ -78,11 +121,16 @@ public void onAuthenticationError_withoutDialogs_returnsNotEnrolledForNoBiometri helper.onAuthenticationError(BiometricPrompt.ERROR_NO_BIOMETRICS, ""); - verify(handler).complete(AuthResult.ERROR_NOT_ENROLLED); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.NOT_ENROLLED) + .setErrorMessage("") + .build()); } @Test - public void onAuthenticationError_returnsNotAvailableForHardwareUnavailable() { + public void onAuthenticationError_returnsHardwareUnavailable() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -95,11 +143,16 @@ public void onAuthenticationError_returnsNotAvailableForHardwareUnavailable() { helper.onAuthenticationError(BiometricPrompt.ERROR_HW_UNAVAILABLE, ""); - verify(handler).complete(AuthResult.ERROR_NOT_AVAILABLE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.HARDWARE_UNAVAILABLE) + .setErrorMessage("") + .build()); } @Test - public void onAuthenticationError_returnsNotAvailableForHardwareNotPresent() { + public void onAuthenticationError_returnsHardwareNotPresent() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -112,7 +165,12 @@ public void onAuthenticationError_returnsNotAvailableForHardwareNotPresent() { helper.onAuthenticationError(BiometricPrompt.ERROR_HW_NOT_PRESENT, ""); - verify(handler).complete(AuthResult.ERROR_NOT_AVAILABLE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.NO_HARDWARE) + .setErrorMessage("") + .build()); } @Test @@ -129,7 +187,12 @@ public void onAuthenticationError_returnsTemporaryLockoutForLockout() { helper.onAuthenticationError(BiometricPrompt.ERROR_LOCKOUT, ""); - verify(handler).complete(AuthResult.ERROR_LOCKED_OUT_TEMPORARILY); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.LOCKED_OUT_TEMPORARILY) + .setErrorMessage("") + .build()); } @Test @@ -146,11 +209,16 @@ public void onAuthenticationError_returnsPermanentLockoutForLockoutPermanent() { helper.onAuthenticationError(BiometricPrompt.ERROR_LOCKOUT_PERMANENT, ""); - verify(handler).complete(AuthResult.ERROR_LOCKED_OUT_PERMANENTLY); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.LOCKED_OUT_PERMANENTLY) + .setErrorMessage("") + .build()); } @Test - public void onAuthenticationError_withoutSticky_returnsFailureForCanceled() { + public void onAuthenticationError_withoutSticky_returnsSystemCanceled() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -163,11 +231,76 @@ public void onAuthenticationError_withoutSticky_returnsFailureForCanceled() { helper.onAuthenticationError(BiometricPrompt.ERROR_CANCELED, ""); - verify(handler).complete(AuthResult.FAILURE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.SYSTEM_CANCELED) + .setErrorMessage("") + .build()); + } + + @Test + public void onAuthenticationError_returnsTimeout() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_TIMEOUT, ""); + + verify(handler) + .complete( + new AuthResult.Builder().setCode(AuthResultCode.TIMEOUT).setErrorMessage("").build()); + } + + @Test + public void onAuthenticationError_returnsNoSpace() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_NO_SPACE, ""); + + verify(handler) + .complete( + new AuthResult.Builder().setCode(AuthResultCode.NO_SPACE).setErrorMessage("").build()); + } + + @Test + public void onAuthenticationError_returnsSecurityUpdateRequired() { + final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); + final AuthenticationHelper helper = + new AuthenticationHelper( + null, + buildMockActivityWithContext(mock(FragmentActivity.class)), + defaultOptions, + dummyStrings, + handler, + true); + + helper.onAuthenticationError(BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED, ""); + + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.SECURITY_UPDATE_REQUIRED) + .setErrorMessage("") + .build()); } @Test - public void onAuthenticationError_withoutSticky_returnsFailureForOtherCases() { + public void onAuthenticationError_returnsUnknownForOtherCases() { final AuthCompletionHandler handler = mock(AuthCompletionHandler.class); final AuthenticationHelper helper = new AuthenticationHelper( @@ -178,9 +311,14 @@ public void onAuthenticationError_withoutSticky_returnsFailureForOtherCases() { handler, true); - helper.onAuthenticationError(BiometricPrompt.ERROR_VENDOR, ""); + helper.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, ""); - verify(handler).complete(AuthResult.FAILURE); + verify(handler) + .complete( + new AuthResult.Builder() + .setCode(AuthResultCode.UNKNOWN_ERROR) + .setErrorMessage("") + .build()); } private FragmentActivity buildMockActivityWithContext(FragmentActivity mockActivity) { diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java index 1c676a67f76..5a58297360f 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -32,6 +32,7 @@ import io.flutter.plugins.localauth.Messages.AuthClassification; import io.flutter.plugins.localauth.Messages.AuthOptions; import io.flutter.plugins.localauth.Messages.AuthResult; +import io.flutter.plugins.localauth.Messages.AuthResultCode; import io.flutter.plugins.localauth.Messages.AuthStrings; import io.flutter.plugins.localauth.Messages.Result; import java.util.List; @@ -46,14 +47,8 @@ public class LocalAuthTest { static final AuthStrings dummyStrings = new AuthStrings.Builder() .setReason("a reason") - .setBiometricHint("a hint") - .setBiometricNotRecognized("biometric not recognized") - .setBiometricRequiredTitle("biometric required") + .setSignInHint("a hint") .setCancelButton("cancel") - .setDeviceCredentialsRequiredTitle("credentials required") - .setDeviceCredentialsSetupDescription("credentials setup description") - .setGoToSettingsButton("go") - .setGoToSettingsDescription("go to settings description") .setSignInTitle("sign in") .build(); @@ -62,7 +57,6 @@ public class LocalAuthTest { .setBiometricOnly(false) .setSensitiveTransaction(false) .setSticky(false) - .setUseErrorDialgs(false) .build(); @Test @@ -74,7 +68,7 @@ public void authenticate_returnsErrorWhenAuthInProgress() { plugin.authenticate(defaultOptions, dummyStrings, mockResult); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); verify(mockResult).success(captor.capture()); - assertEquals(AuthResult.ERROR_ALREADY_IN_PROGRESS, captor.getValue()); + assertEquals(AuthResultCode.ALREADY_IN_PROGRESS, captor.getValue().getCode()); } @Test @@ -86,7 +80,7 @@ public void authenticate_returnsErrorWithNoForegroundActivity() { plugin.authenticate(defaultOptions, dummyStrings, mockResult); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); verify(mockResult).success(captor.capture()); - assertEquals(AuthResult.ERROR_NO_ACTIVITY, captor.getValue()); + assertEquals(AuthResultCode.NO_ACTIVITY, captor.getValue().getCode()); } @Test @@ -98,7 +92,7 @@ public void authenticate_returnsErrorWhenActivityNotFragmentActivity() { plugin.authenticate(defaultOptions, dummyStrings, mockResult); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); verify(mockResult).success(captor.capture()); - assertEquals(AuthResult.ERROR_NOT_FRAGMENT_ACTIVITY, captor.getValue()); + assertEquals(AuthResultCode.NOT_FRAGMENT_ACTIVITY, captor.getValue().getCode()); } @Test @@ -111,7 +105,7 @@ public void authenticate_returnsErrorWhenDeviceNotSupported() { plugin.authenticate(defaultOptions, dummyStrings, mockResult); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); verify(mockResult).success(captor.capture()); - assertEquals(AuthResult.ERROR_NOT_AVAILABLE, captor.getValue()); + assertEquals(AuthResultCode.NO_CREDENTIALS, captor.getValue().getCode()); } @Test @@ -143,7 +137,6 @@ public void authenticate_properlyConfiguresBiometricOnlyAuthenticationRequest() .setBiometricOnly(true) .setSensitiveTransaction(false) .setSticky(false) - .setUseErrorDialgs(false) .build(); plugin.authenticate(options, dummyStrings, mockResult); assertFalse(allowCredentialsCaptor.getValue()); @@ -281,6 +274,14 @@ public void onDetachedFromActivity_ShouldReleaseActivity() { assertNull(plugin.getActivity()); } + @Test + public void getEnrolledBiometrics_shouldReturnNullForNoActivity() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + + final List enrolled = plugin.getEnrolledBiometrics(); + assertNull(enrolled); + } + @Test public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent() { final LocalAuthPlugin plugin = new LocalAuthPlugin(); diff --git a/packages/local_auth/local_auth_android/example/lib/main.dart b/packages/local_auth/local_auth_android/example/lib/main.dart index ff094a526c9..7e1ac81d1b1 100644 --- a/packages/local_auth/local_auth_android/example/lib/main.dart +++ b/packages/local_auth/local_auth_android/example/lib/main.dart @@ -92,11 +92,21 @@ class _MyAppState extends State { setState(() { _isAuthenticating = false; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected Error - ${e.message}'; }); return; } @@ -129,11 +139,21 @@ class _MyAppState extends State { _isAuthenticating = false; _authorized = 'Authenticating'; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected Error - ${e.message}'; }); return; } diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml index c75acd437c4..c273068d84c 100644 --- a/packages/local_auth/local_auth_android/example/pubspec.yaml +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - local_auth_platform_interface: ^1.0.0 + local_auth_platform_interface: ^1.1.0 dev_dependencies: flutter_test: diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart index 5f2367f9e37..af9289be3f2 100644 --- a/packages/local_auth/local_auth_android/lib/local_auth_android.dart +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; import 'src/auth_messages_android.dart'; @@ -39,62 +38,77 @@ class LocalAuthAndroid extends LocalAuthPlatform { biometricOnly: options.biometricOnly, sensitiveTransaction: options.sensitiveTransaction, 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/blob/master/docs/ecosystem/contributing/README.md#platform-exception-handling - // The PlatformExceptions thrown here are for compatibiilty with the - // previous Java implementation. - switch (result) { - case AuthResult.success: + switch (result.code) { + case AuthResultCode.success: return true; - case AuthResult.failure: - return false; - case AuthResult.errorAlreadyInProgress: - throw PlatformException( - code: 'auth_in_progress', - message: 'Authentication in progress', + case AuthResultCode.negativeButton: + case AuthResultCode.userCanceled: + // Variants of user cancelation format are not currently distinguished, + // but could be if there's a use case for it in the future. + throw const LocalAuthException( + code: LocalAuthExceptionCode.userCanceled, ); - case AuthResult.errorNoActivity: - throw PlatformException( - code: 'no_activity', - message: 'local_auth plugin requires a foreground activity', + case AuthResultCode.systemCanceled: + throw const LocalAuthException( + code: LocalAuthExceptionCode.systemCanceled, ); - case AuthResult.errorNotFragmentActivity: - throw PlatformException( - code: 'no_fragment_activity', - message: - 'local_auth plugin requires activity to be a FragmentActivity.', + case AuthResultCode.timeout: + throw const LocalAuthException(code: LocalAuthExceptionCode.timeout); + case AuthResultCode.alreadyInProgress: + throw const LocalAuthException( + code: LocalAuthExceptionCode.authInProgress, ); - case AuthResult.errorNotAvailable: - throw PlatformException( - code: 'NotAvailable', - message: 'Security credentials not available.', + case AuthResultCode.noActivity: + throw const LocalAuthException( + code: LocalAuthExceptionCode.uiUnavailable, + description: 'No Activity available.', ); - case AuthResult.errorNotEnrolled: - throw PlatformException( - code: 'NotEnrolled', - message: 'No Biometrics enrolled on this device.', + case AuthResultCode.notFragmentActivity: + throw const LocalAuthException( + code: LocalAuthExceptionCode.uiUnavailable, + description: 'The current Activity must be a FragmentActivity.', ); - case AuthResult.errorLockedOutTemporarily: - throw PlatformException( - code: 'LockedOut', - message: - 'The operation was canceled because the API is locked out ' - 'due to too many attempts. This occurs after 5 failed ' - 'attempts, and lasts for 30 seconds.', + case AuthResultCode.noCredentials: + throw const LocalAuthException( + code: LocalAuthExceptionCode.noCredentialsSet, ); - case AuthResult.errorLockedOutPermanently: - throw PlatformException( - code: 'PermanentlyLockedOut', - message: - 'The operation was canceled because ERROR_LOCKOUT ' - 'occurred too many times. Biometric authentication is disabled ' - 'until the user unlocks with strong authentication ' - '(PIN/Pattern/Password)', + case AuthResultCode.noHardware: + throw const LocalAuthException( + code: LocalAuthExceptionCode.noBiometricHardware, + ); + case AuthResultCode.hardwareUnavailable: + throw const LocalAuthException( + code: LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable, + ); + case AuthResultCode.notEnrolled: + throw const LocalAuthException( + code: LocalAuthExceptionCode.noBiometricsEnrolled, + ); + case AuthResultCode.lockedOutTemporarily: + throw const LocalAuthException( + code: LocalAuthExceptionCode.temporaryLockout, + ); + case AuthResultCode.lockedOutPermanently: + throw const LocalAuthException( + code: LocalAuthExceptionCode.biometricLockout, + ); + case AuthResultCode.noSpace: + throw LocalAuthException( + code: LocalAuthExceptionCode.deviceError, + description: 'Not enough space available: ${result.errorMessage}', + ); + case AuthResultCode.securityUpdateRequired: + throw LocalAuthException( + code: LocalAuthExceptionCode.deviceError, + description: 'Security update required: ${result.errorMessage}', + ); + case AuthResultCode.unknownError: + throw LocalAuthException( + code: LocalAuthExceptionCode.unknownError, + description: result.errorMessage, ); } } @@ -106,7 +120,13 @@ class LocalAuthAndroid extends LocalAuthPlatform { @override Future> getEnrolledBiometrics() async { - final List result = await _api.getEnrolledBiometrics(); + final List? result = await _api.getEnrolledBiometrics(); + if (result == null) { + throw const LocalAuthException( + code: LocalAuthExceptionCode.uiUnavailable, + description: 'No Activity available.', + ); + } return result.map((AuthClassification value) { switch (value) { case AuthClassification.weak: @@ -135,21 +155,8 @@ class LocalAuthAndroid extends LocalAuthPlatform { } return AuthStrings( reason: localizedReason, - biometricHint: messages?.biometricHint ?? androidBiometricHint, - biometricNotRecognized: - messages?.biometricNotRecognized ?? androidBiometricNotRecognized, - biometricRequiredTitle: - messages?.biometricRequiredTitle ?? androidBiometricRequiredTitle, + signInHint: messages?.signInHint ?? androidSignInHint, cancelButton: messages?.cancelButton ?? androidCancelButton, - deviceCredentialsRequiredTitle: - messages?.deviceCredentialsRequiredTitle ?? - androidDeviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription: - messages?.deviceCredentialsSetupDescription ?? - androidDeviceCredentialsSetupDescription, - goToSettingsButton: messages?.goToSettingsButton ?? goToSettings, - goToSettingsDescription: - messages?.goToSettingsDescription ?? androidGoToSettingsDescription, signInTitle: messages?.signInTitle ?? androidSignInTitle, ); } diff --git a/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart b/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart index a19f3a83078..b499f4963f9 100644 --- a/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart +++ b/packages/local_auth/local_auth_android/lib/src/auth_messages_android.dart @@ -13,58 +13,20 @@ import 'package:local_auth_platform_interface/types/auth_messages.dart'; class AndroidAuthMessages extends AuthMessages { /// Constructs a new instance. const AndroidAuthMessages({ - this.biometricHint, - this.biometricNotRecognized, - this.biometricRequiredTitle, - this.biometricSuccess, + this.signInHint, this.cancelButton, - this.deviceCredentialsRequiredTitle, - this.deviceCredentialsSetupDescription, - this.goToSettingsButton, - this.goToSettingsDescription, this.signInTitle, }); - /// Hint message advising the user how to authenticate with biometrics. + /// Hint message advising the user how to authenticate. /// Maximum 60 characters. - final String? biometricHint; - - /// Message to let the user know that authentication was failed. - /// Maximum 60 characters. - final String? biometricNotRecognized; - - /// Message shown as a title in a dialog which indicates the user - /// has not set up biometric authentication on their device. - /// Maximum 60 characters. - final String? biometricRequiredTitle; - - /// Message to let the user know that authentication was successful. - /// Maximum 60 characters - final String? biometricSuccess; + final String? signInHint; /// Message shown on a button that the user can click to leave the /// current dialog. /// Maximum 30 characters. final String? cancelButton; - /// Message shown as a title in a dialog which indicates the user - /// has not set up credentials authentication on their device. - /// Maximum 60 characters. - final String? deviceCredentialsRequiredTitle; - - /// Message advising the user to go to the settings and configure - /// device credentials on their device. - final String? deviceCredentialsSetupDescription; - - /// Message shown on a button that the user can click to go to settings pages - /// from the current dialog. - /// Maximum 30 characters. - final String? goToSettingsButton; - - /// Message advising the user to go to the settings and configure - /// biometric on their device. - final String? goToSettingsDescription; - /// Message shown as a title in a dialog which indicates the user /// that they need to scan biometric to continue. /// Maximum 60 characters. @@ -73,22 +35,9 @@ class AndroidAuthMessages extends AuthMessages { @override Map get args { return { - 'biometricHint': biometricHint ?? androidBiometricHint, - 'biometricNotRecognized': - biometricNotRecognized ?? androidBiometricNotRecognized, - 'biometricSuccess': biometricSuccess ?? androidBiometricSuccess, - 'biometricRequired': - biometricRequiredTitle ?? androidBiometricRequiredTitle, + // This legacy key is kept for backwards compatibility. + 'biometricHint': signInHint ?? androidSignInHint, 'cancelButton': cancelButton ?? androidCancelButton, - 'deviceCredentialsRequired': - deviceCredentialsRequiredTitle ?? - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - deviceCredentialsSetupDescription ?? - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettingsButton ?? goToSettings, - 'goToSettingDescription': - goToSettingsDescription ?? androidGoToSettingsDescription, 'signInTitle': signInTitle ?? androidSignInTitle, }; } @@ -98,68 +47,23 @@ class AndroidAuthMessages extends AuthMessages { identical(this, other) || other is AndroidAuthMessages && runtimeType == other.runtimeType && - biometricHint == other.biometricHint && - biometricNotRecognized == other.biometricNotRecognized && - biometricRequiredTitle == other.biometricRequiredTitle && - biometricSuccess == other.biometricSuccess && + signInHint == other.signInHint && cancelButton == other.cancelButton && - deviceCredentialsRequiredTitle == - other.deviceCredentialsRequiredTitle && - deviceCredentialsSetupDescription == - other.deviceCredentialsSetupDescription && - goToSettingsButton == other.goToSettingsButton && - goToSettingsDescription == other.goToSettingsDescription && signInTitle == other.signInTitle; @override - int get hashCode => Object.hash( - super.hashCode, - biometricHint, - biometricNotRecognized, - biometricRequiredTitle, - biometricSuccess, - cancelButton, - deviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription, - goToSettingsButton, - goToSettingsDescription, - signInTitle, - ); + int get hashCode => + Object.hash(super.hashCode, signInHint, cancelButton, signInTitle); } // Default strings for AndroidAuthMessages. Currently supports English. // Intl.message must be string literals. -/// Message shown on a button that the user can click to go to settings pages -/// from the current dialog. -String get goToSettings => Intl.message( - 'Go to settings', - desc: - 'Message shown on a button that the user can click to go to ' - 'settings pages from the current dialog. Maximum 30 characters.', -); - -/// Hint message advising the user how to authenticate with biometrics. -String get androidBiometricHint => Intl.message( +/// Hint message advising the user how to authenticate . +String get androidSignInHint => Intl.message( 'Verify identity', desc: - 'Hint message advising the user how to authenticate with biometrics. ' - 'Maximum 60 characters.', -); - -/// Message to let the user know that authentication was failed. -String get androidBiometricNotRecognized => Intl.message( - 'Not recognized. Try again.', - desc: - 'Message to let the user know that authentication was failed. ' - 'Maximum 60 characters.', -); - -/// Message to let the user know that authentication was successful. It -String get androidBiometricSuccess => Intl.message( - 'Success', - desc: - 'Message to let the user know that authentication was successful. ' + 'Hint message advising the user how to authenticate. ' 'Maximum 60 characters.', ); @@ -180,42 +84,3 @@ String get androidSignInTitle => Intl.message( 'Message shown as a title in a dialog which indicates the user ' 'that they need to scan biometric to continue. Maximum 60 characters.', ); - -/// Message shown as a title in a dialog which indicates the user -/// has not set up biometric authentication on their device. -String get androidBiometricRequiredTitle => Intl.message( - 'Biometric required', - desc: - 'Message shown as a title in a dialog which indicates the user ' - 'has not set up biometric authentication on their device. ' - 'Maximum 60 characters.', -); - -/// Message shown as a title in a dialog which indicates the user -/// has not set up credentials authentication on their device. -String get androidDeviceCredentialsRequiredTitle => Intl.message( - 'Device credentials required', - desc: - 'Message shown as a title in a dialog which indicates the user ' - 'has not set up credentials authentication on their device. ' - 'Maximum 60 characters.', -); - -/// Message advising the user to go to the settings and configure -/// device credentials on their device. -String get androidDeviceCredentialsSetupDescription => Intl.message( - 'Device credentials required', - desc: - 'Message advising the user to go to the settings and configure ' - 'device credentials on their device.', -); - -/// Message advising the user to go to the settings and configure -/// biometric on their device. -String get androidGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Go to ' - "'Settings > Security' to add biometric authentication.", - desc: - 'Message advising the user to go to the settings and configure ' - 'biometric on their device.', -); diff --git a/packages/local_auth/local_auth_android/lib/src/messages.g.dart b/packages/local_auth/local_auth_android/lib/src/messages.g.dart index f28aaf7a4c7..e8b3b64699c 100644 --- a/packages/local_auth/local_auth_android/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -19,34 +19,61 @@ PlatformException _createConnectionError(String channelName) { } /// Possible outcomes of an authentication attempt. -enum AuthResult { +enum AuthResultCode { /// The user authenticated successfully. success, - /// The user failed to successfully authenticate. - failure, + /// The user pressed the negative button, which corresponds to + /// [AuthStrings.cancelButton]. + negativeButton, + + /// The user canceled authentication without pressing the negative button. + /// + /// This may be triggered by a swipe or a back button, for example. + userCanceled, + + /// Authentication was caneceled by the system. + systemCanceled, + + /// Authentication timed out. + timeout, /// An authentication was already in progress. - errorAlreadyInProgress, + alreadyInProgress, /// There is no foreground activity. - errorNoActivity, + noActivity, /// The foreground activity is not a FragmentActivity. - errorNotFragmentActivity, + notFragmentActivity, + + /// The device does not have any credentials available. + noCredentials, - /// The authentication system was not available. - errorNotAvailable, + /// No biometric hardware is present. + noHardware, + + /// The biometric is temporarily unavailable. + hardwareUnavailable, /// No biometrics are enrolled. - errorNotEnrolled, + notEnrolled, /// The user is locked out temporarily due to too many failed attempts. - errorLockedOutTemporarily, + lockedOutTemporarily, /// The user is locked out until they log in another way due to too many /// failed attempts. - errorLockedOutPermanently, + lockedOutPermanently, + + /// The device does not have enough storage to complete authentication. + noSpace, + + /// The hardware is unavailable until a security update is performed. + securityUpdateRequired, + + /// Some unrecognized error case was encountered + unknownError, } /// Pigeon equivalent of the subset of BiometricType used by Android. @@ -58,65 +85,53 @@ enum AuthClassification { weak, strong } class AuthStrings { AuthStrings({ required this.reason, - required this.biometricHint, - required this.biometricNotRecognized, - required this.biometricRequiredTitle, + required this.signInHint, required this.cancelButton, - required this.deviceCredentialsRequiredTitle, - required this.deviceCredentialsSetupDescription, - required this.goToSettingsButton, - required this.goToSettingsDescription, required this.signInTitle, }); String reason; - String biometricHint; - - String biometricNotRecognized; - - String biometricRequiredTitle; + String signInHint; String cancelButton; - String deviceCredentialsRequiredTitle; - - String deviceCredentialsSetupDescription; - - String goToSettingsButton; - - String goToSettingsDescription; - String signInTitle; Object encode() { - return [ - reason, - biometricHint, - biometricNotRecognized, - biometricRequiredTitle, - cancelButton, - deviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription, - goToSettingsButton, - goToSettingsDescription, - signInTitle, - ]; + return [reason, signInHint, cancelButton, signInTitle]; } static AuthStrings decode(Object result) { result as List; return AuthStrings( reason: result[0]! as String, - biometricHint: result[1]! as String, - biometricNotRecognized: result[2]! as String, - biometricRequiredTitle: result[3]! as String, - cancelButton: result[4]! as String, - deviceCredentialsRequiredTitle: result[5]! as String, - deviceCredentialsSetupDescription: result[6]! as String, - goToSettingsButton: result[7]! as String, - goToSettingsDescription: result[8]! as String, - signInTitle: result[9]! as String, + signInHint: result[1]! as String, + cancelButton: result[2]! as String, + signInTitle: result[3]! as String, + ); + } +} + +/// The results of an authentication request. +class AuthResult { + AuthResult({required this.code, this.errorMessage}); + + /// The specific result returned from the SDK. + AuthResultCode code; + + /// The error message associated with the result, if any. + String? errorMessage; + + Object encode() { + return [code, errorMessage]; + } + + static AuthResult decode(Object result) { + result as List; + return AuthResult( + code: result[0]! as AuthResultCode, + errorMessage: result[1] as String?, ); } } @@ -126,7 +141,6 @@ class AuthOptions { required this.biometricOnly, required this.sensitiveTransaction, required this.sticky, - required this.useErrorDialgs, }); bool biometricOnly; @@ -135,15 +149,8 @@ class AuthOptions { bool sticky; - bool useErrorDialgs; - Object encode() { - return [ - biometricOnly, - sensitiveTransaction, - sticky, - useErrorDialgs, - ]; + return [biometricOnly, sensitiveTransaction, sticky]; } static AuthOptions decode(Object result) { @@ -152,7 +159,6 @@ class AuthOptions { biometricOnly: result[0]! as bool, sensitiveTransaction: result[1]! as bool, sticky: result[2]! as bool, - useErrorDialgs: result[3]! as bool, ); } } @@ -164,7 +170,7 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is AuthResult) { + } else if (value is AuthResultCode) { buffer.putUint8(129); writeValue(buffer, value.index); } else if (value is AuthClassification) { @@ -173,9 +179,12 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is AuthStrings) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is AuthOptions) { + } else if (value is AuthResult) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is AuthOptions) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -186,13 +195,15 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: final int? value = readValue(buffer) as int?; - return value == null ? null : AuthResult.values[value]; + return value == null ? null : AuthResultCode.values[value]; case 130: final int? value = readValue(buffer) as int?; return value == null ? null : AuthClassification.values[value]; case 131: return AuthStrings.decode(readValue(buffer)!); case 132: + return AuthResult.decode(readValue(buffer)!); + case 133: return AuthOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -313,7 +324,10 @@ class LocalAuthApi { /// Returns the biometric types that are enrolled, and can thus be used /// without additional setup. - Future> getEnrolledBiometrics() async { + /// + /// Returns null if there is no activity, in which case the enrolled + /// biometrics can't be determined. + Future?> getEnrolledBiometrics() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_android.LocalAuthApi.getEnrolledBiometrics$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -332,14 +346,9 @@ class LocalAuthApi { message: pigeonVar_replyList[1] as String?, details: pigeonVar_replyList[2], ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); } else { - return (pigeonVar_replyList[0] as List?)! - .cast(); + return (pigeonVar_replyList[0] as List?) + ?.cast(); } } diff --git a/packages/local_auth/local_auth_android/pigeons/messages.dart b/packages/local_auth/local_auth_android/pigeons/messages.dart index cf4238c366b..698f012a3dc 100644 --- a/packages/local_auth/local_auth_android/pigeons/messages.dart +++ b/packages/local_auth/local_auth_android/pigeons/messages.dart @@ -19,58 +19,84 @@ class AuthStrings { /// Constructs a new instance. const AuthStrings({ required this.reason, - required this.biometricHint, - required this.biometricNotRecognized, - required this.biometricRequiredTitle, + required this.signInHint, required this.cancelButton, - required this.deviceCredentialsRequiredTitle, - required this.deviceCredentialsSetupDescription, - required this.goToSettingsButton, - required this.goToSettingsDescription, required this.signInTitle, }); final String reason; - final String biometricHint; - final String biometricNotRecognized; - final String biometricRequiredTitle; + final String signInHint; final String cancelButton; - final String deviceCredentialsRequiredTitle; - final String deviceCredentialsSetupDescription; - final String goToSettingsButton; - final String goToSettingsDescription; final String signInTitle; } /// Possible outcomes of an authentication attempt. -enum AuthResult { +enum AuthResultCode { /// The user authenticated successfully. success, - /// The user failed to successfully authenticate. - failure, + /// The user pressed the negative button, which corresponds to + /// [AuthStrings.cancelButton]. + negativeButton, + + /// The user canceled authentication without pressing the negative button. + /// + /// This may be triggered by a swipe or a back button, for example. + userCanceled, + + /// Authentication was caneceled by the system. + systemCanceled, + + /// Authentication timed out. + timeout, /// An authentication was already in progress. - errorAlreadyInProgress, + alreadyInProgress, /// There is no foreground activity. - errorNoActivity, + noActivity, /// The foreground activity is not a FragmentActivity. - errorNotFragmentActivity, + notFragmentActivity, - /// The authentication system was not available. - errorNotAvailable, + /// The device does not have any credentials available. + noCredentials, + + /// No biometric hardware is present. + noHardware, + + /// The biometric is temporarily unavailable. + hardwareUnavailable, /// No biometrics are enrolled. - errorNotEnrolled, + notEnrolled, /// The user is locked out temporarily due to too many failed attempts. - errorLockedOutTemporarily, + lockedOutTemporarily, /// The user is locked out until they log in another way due to too many /// failed attempts. - errorLockedOutPermanently, + lockedOutPermanently, + + /// The device does not have enough storage to complete authentication. + noSpace, + + /// The hardware is unavailable until a security update is performed. + securityUpdateRequired, + + /// Some unrecognized error case was encountered + unknownError, +} + +/// The results of an authentication request. +class AuthResult { + const AuthResult({required this.code, this.errorMessage}); + + /// The specific result returned from the SDK. + final AuthResultCode code; + + /// The error message associated with the result, if any. + final String? errorMessage; } class AuthOptions { @@ -78,12 +104,10 @@ class AuthOptions { required this.biometricOnly, required this.sensitiveTransaction, required this.sticky, - required this.useErrorDialgs, }); final bool biometricOnly; final bool sensitiveTransaction; final bool sticky; - final bool useErrorDialgs; } /// Pigeon equivalent of the subset of BiometricType used by Android. @@ -106,7 +130,10 @@ abstract class LocalAuthApi { /// Returns the biometric types that are enrolled, and can thus be used /// without additional setup. - List getEnrolledBiometrics(); + /// + /// Returns null if there is no activity, in which case the enrolled + /// biometrics can't be determined. + List? getEnrolledBiometrics(); /// Attempts to authenticate the user with the provided [options], and using /// [strings] for any UI. diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index cccf3f166c2..d3fd59b04f0 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_android description: Android implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.53 +version: 2.0.0 environment: sdk: ^3.9.0 @@ -22,7 +22,7 @@ dependencies: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 intl: ">=0.17.0 <0.21.0" - local_auth_platform_interface: ^1.0.1 + local_auth_platform_interface: ^1.1.0 dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart index 11f40c500da..8b2a109f96a 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:local_auth_android/local_auth_android.dart'; import 'package:local_auth_android/src/messages.g.dart'; @@ -86,6 +85,21 @@ void main() { expect(result, []); }); + + test('throws no UI for null', () async { + when(api.getEnrolledBiometrics()).thenAnswer((_) async => null); + + expect( + () async => plugin.getEnrolledBiometrics(), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ), + ), + ); + }); }); group('authenticate', () { @@ -93,7 +107,7 @@ void main() { test('passes default values when nothing is provided', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); const String reason = 'test reason'; await plugin.authenticate( @@ -108,20 +122,8 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_android.dart - expect(strings.biometricHint, androidBiometricHint); - expect(strings.biometricNotRecognized, androidBiometricNotRecognized); - expect(strings.biometricRequiredTitle, androidBiometricRequiredTitle); + expect(strings.signInHint, androidSignInHint); expect(strings.cancelButton, androidCancelButton); - expect( - strings.deviceCredentialsRequiredTitle, - androidDeviceCredentialsRequiredTitle, - ); - expect( - strings.deviceCredentialsSetupDescription, - androidDeviceCredentialsSetupDescription, - ); - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, androidGoToSettingsDescription); expect(strings.signInTitle, androidSignInTitle); }); @@ -130,7 +132,7 @@ void main() { () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); const String reason = 'test reason'; await plugin.authenticate( @@ -145,23 +147,8 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_android.dart - expect(strings.biometricHint, androidBiometricHint); - expect(strings.biometricNotRecognized, androidBiometricNotRecognized); - expect(strings.biometricRequiredTitle, androidBiometricRequiredTitle); + expect(strings.signInHint, androidSignInHint); expect(strings.cancelButton, androidCancelButton); - expect( - strings.deviceCredentialsRequiredTitle, - androidDeviceCredentialsRequiredTitle, - ); - expect( - strings.deviceCredentialsSetupDescription, - androidDeviceCredentialsSetupDescription, - ); - expect(strings.goToSettingsButton, goToSettings); - expect( - strings.goToSettingsDescription, - androidGoToSettingsDescription, - ); expect(strings.signInTitle, androidSignInTitle); }, ); @@ -169,33 +156,21 @@ void main() { test('passes all non-default values correctly', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); // These are arbitrary values; all that matters is that: // - they are different from the defaults, and // - they are different from each other. const String reason = 'A'; const String hint = 'B'; - const String bioNotRecognized = 'C'; - const String bioRequired = 'D'; - const String cancel = 'E'; - const String credentialsRequired = 'F'; - const String credentialsSetup = 'G'; - const String goButton = 'H'; - const String goDescription = 'I'; - const String signInTitle = 'J'; + const String cancel = 'C'; + const String signInTitle = 'D'; await plugin.authenticate( localizedReason: reason, authMessages: [ const AndroidAuthMessages( - biometricHint: hint, - biometricNotRecognized: bioNotRecognized, - biometricRequiredTitle: bioRequired, + signInHint: hint, cancelButton: cancel, - deviceCredentialsRequiredTitle: credentialsRequired, - deviceCredentialsSetupDescription: credentialsSetup, - goToSettingsButton: goButton, - goToSettingsDescription: goDescription, signInTitle: signInTitle, ), AnotherPlatformAuthMessages(), @@ -207,39 +182,26 @@ void main() { ); final AuthStrings strings = result.captured[0] as AuthStrings; expect(strings.reason, reason); - expect(strings.biometricHint, hint); - expect(strings.biometricNotRecognized, bioNotRecognized); - expect(strings.biometricRequiredTitle, bioRequired); + expect(strings.signInHint, hint); expect(strings.cancelButton, cancel); - expect(strings.deviceCredentialsRequiredTitle, credentialsRequired); - expect(strings.deviceCredentialsSetupDescription, credentialsSetup); - expect(strings.goToSettingsButton, goButton); - expect(strings.goToSettingsDescription, goDescription); expect(strings.signInTitle, signInTitle); }); test('passes provided messages with default fallbacks', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); // These are arbitrary values; all that matters is that: // - they are different from the defaults, and // - they are different from each other. const String reason = 'A'; const String hint = 'B'; - const String bioNotRecognized = 'C'; - const String bioRequired = 'D'; - const String cancel = 'E'; + const String cancel = 'C'; await plugin.authenticate( localizedReason: reason, authMessages: [ - const AndroidAuthMessages( - biometricHint: hint, - biometricNotRecognized: bioNotRecognized, - biometricRequiredTitle: bioRequired, - cancelButton: cancel, - ), + const AndroidAuthMessages(signInHint: hint, cancelButton: cancel), ], ); @@ -249,22 +211,10 @@ void main() { final AuthStrings strings = result.captured[0] as AuthStrings; expect(strings.reason, reason); // These should all be the provided values. - expect(strings.biometricHint, hint); - expect(strings.biometricNotRecognized, bioNotRecognized); - expect(strings.biometricRequiredTitle, bioRequired); + expect(strings.signInHint, hint); expect(strings.cancelButton, cancel); // These were non set, so should all be the default values from // auth_messages_android.dart - expect( - strings.deviceCredentialsRequiredTitle, - androidDeviceCredentialsRequiredTitle, - ); - expect( - strings.deviceCredentialsSetupDescription, - androidDeviceCredentialsSetupDescription, - ); - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, androidGoToSettingsDescription); expect(strings.signInTitle, androidSignInTitle); }); }); @@ -273,7 +223,7 @@ void main() { test('passes default values', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); await plugin.authenticate( localizedReason: 'reason', @@ -287,13 +237,12 @@ void main() { expect(options.biometricOnly, false); expect(options.sensitiveTransaction, true); expect(options.sticky, false); - expect(options.useErrorDialgs, true); }); test('passes provided non-default values', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); await plugin.authenticate( localizedReason: 'reason', @@ -302,7 +251,6 @@ void main() { biometricOnly: true, sensitiveTransaction: false, stickyAuth: true, - useErrorDialogs: false, ), ); @@ -313,7 +261,6 @@ void main() { expect(options.biometricOnly, true); expect(options.sensitiveTransaction, false); expect(options.sticky, true); - expect(options.useErrorDialgs, false); }); }); @@ -321,7 +268,7 @@ void main() { test('handles success', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.success); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); final bool result = await plugin.authenticate( localizedReason: 'reason', @@ -331,25 +278,58 @@ void main() { expect(result, true); }); - test('handles failure', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.failure); + test( + 'converts negativeButton to userCanceled LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.negativeButton), + ); - final bool result = await plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ); + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.userCanceled, + ), + ), + ); + }, + ); - expect(result, false); - }); + test( + 'converts userCanceled to userCanceled LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.userCanceled), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.userCanceled, + ), + ), + ); + }, + ); test( - 'converts errorAlreadyInProgress to legacy PlatformException', + 'converts systemCanceled to systemCanceled LocalAuthException', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorAlreadyInProgress); + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.systemCanceled), + ); expect( () async => plugin.authenticate( @@ -357,26 +337,20 @@ void main() { authMessages: [], ), throwsA( - isA() - .having( - (PlatformException e) => e.code, - 'code', - 'auth_in_progress', - ) - .having( - (PlatformException e) => e.message, - 'message', - 'Authentication in progress', - ), + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.systemCanceled, + ), ), ); }, ); - test('converts errorNoActivity to legacy PlatformException', () async { + test('converts timeout to timeout LocalAuthException', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorNoActivity); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.timeout)); expect( () async => plugin.authenticate( @@ -384,23 +358,21 @@ void main() { authMessages: [], ), throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'no_activity') - .having( - (PlatformException e) => e.message, - 'message', - 'local_auth plugin requires a foreground activity', - ), + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.timeout, + ), ), ); }); test( - 'converts errorNotFragmentActivity to legacy PlatformException', + 'converts alreadyInProgress to authInProgress LocalAuthException', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorNotFragmentActivity); + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.alreadyInProgress), + ); expect( () async => plugin.authenticate( @@ -408,26 +380,20 @@ void main() { authMessages: [], ), throwsA( - isA() - .having( - (PlatformException e) => e.code, - 'code', - 'no_fragment_activity', - ) - .having( - (PlatformException e) => e.message, - 'message', - 'local_auth plugin requires activity to be a FragmentActivity.', - ), + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.authInProgress, + ), ), ); }, ); - test('converts errorNotAvailable to legacy PlatformException', () async { + test('converts noActivity to uiUnavailable LocalAuthException', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorNotAvailable); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.noActivity)); expect( () async => plugin.authenticate( @@ -435,21 +401,180 @@ void main() { authMessages: [], ), throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'NotAvailable') - .having( - (PlatformException e) => e.message, - 'message', - 'Security credentials not available.', - ), + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ), ), ); }); - test('converts errorNotEnrolled to legacy PlatformException', () async { + test( + 'converts notFragmentActivity to uiUnavailable LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.notFragmentActivity), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ), + ), + ); + }, + ); + + test( + 'converts noCredentials to noCredentialsSet LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.noCredentials), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noCredentialsSet, + ), + ), + ); + }, + ); + + test( + 'converts noHardware to noBiometricHardware LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.noHardware), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricHardware, + ), + ), + ); + }, + ); + + test( + 'converts hardwareUnavailable to biometricHardwareTemporarilyUnavailable LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.hardwareUnavailable), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable, + ), + ), + ); + }, + ); + + test( + 'converts notEnrolled to noBiometricsEnrolled LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.notEnrolled), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricsEnrolled, + ), + ), + ); + }, + ); + + test( + 'converts lockedOutTemporarily to temporaryLockout LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.lockedOutTemporarily), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.temporaryLockout, + ), + ), + ); + }, + ); + + test( + 'converts lockedOutPermanently to biometricLockout LocalAuthException', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult(code: AuthResultCode.lockedOutPermanently), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.biometricLockout, + ), + ), + ); + }, + ); + + test('converts noSpace to deviceError LocalAuthException', () async { when( api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorNotEnrolled); + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.noSpace)); expect( () async => plugin.authenticate( @@ -457,23 +582,28 @@ void main() { authMessages: [], ), throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'NotEnrolled') + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.deviceError, + ) .having( - (PlatformException e) => e.message, - 'message', - 'No Biometrics enrolled on this device.', + (LocalAuthException e) => e.description, + 'description', + startsWith('Not enough space available:'), ), ), ); }); test( - 'converts errorLockedOutTemporarily to legacy PlatformException', + 'converts securityUpdateRequired to deviceError LocalAuthException', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorLockedOutTemporarily); + when(api.authenticate(any, any)).thenAnswer( + (_) async => + AuthResult(code: AuthResultCode.securityUpdateRequired), + ); expect( () async => plugin.authenticate( @@ -481,14 +611,16 @@ void main() { authMessages: [], ), throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'LockedOut') + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.deviceError, + ) .having( - (PlatformException e) => e.message, - 'message', - 'The operation was canceled because the API is locked out ' - 'due to too many attempts. This occurs after 5 failed ' - 'attempts, and lasts for 30 seconds.', + (LocalAuthException e) => e.description, + 'description', + startsWith('Security update required:'), ), ), ); @@ -496,11 +628,15 @@ void main() { ); test( - 'converts errorLockedOutPermanently to legacy PlatformException', + 'converts unknownError to unknownError LocalAuthException, passing error message', () async { - when( - api.authenticate(any, any), - ).thenAnswer((_) async => AuthResult.errorLockedOutPermanently); + const String errorMessage = 'Some error message'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResult( + code: AuthResultCode.unknownError, + errorMessage: errorMessage, + ), + ); expect( () async => plugin.authenticate( @@ -508,19 +644,16 @@ void main() { authMessages: [], ), throwsA( - isA() + isA() .having( - (PlatformException e) => e.code, + (LocalAuthException e) => e.code, 'code', - 'PermanentlyLockedOut', + LocalAuthExceptionCode.unknownError, ) .having( - (PlatformException e) => e.message, - 'message', - 'The operation was canceled because ERROR_LOCKOUT occurred ' - 'too many times. Biometric authentication is disabled ' - 'until the user unlocks with strong ' - 'authentication (PIN/Pattern/Password)', + (LocalAuthException e) => e.description, + 'description', + errorMessage, ), ), ); diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart b/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart index 549cb242bcd..64d3e903231 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in local_auth_android/test/local_auth_test.dart. // Do not manually edit this file. @@ -17,11 +17,17 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeAuthResult_0 extends _i1.SmartFake implements _i2.AuthResult { + _FakeAuthResult_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [LocalAuthApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -66,14 +72,12 @@ class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi { as _i4.Future); @override - _i4.Future> getEnrolledBiometrics() => + _i4.Future?> getEnrolledBiometrics() => (super.noSuchMethod( Invocation.method(#getEnrolledBiometrics, []), - returnValue: _i4.Future>.value( - <_i2.AuthClassification>[], - ), + returnValue: _i4.Future?>.value(), ) - as _i4.Future>); + as _i4.Future?>); @override _i4.Future<_i2.AuthResult> authenticate( @@ -83,7 +87,10 @@ class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi { (super.noSuchMethod( Invocation.method(#authenticate, [options, strings]), returnValue: _i4.Future<_i2.AuthResult>.value( - _i2.AuthResult.success, + _FakeAuthResult_0( + this, + Invocation.method(#authenticate, [options, strings]), + ), ), ) as _i4.Future<_i2.AuthResult>); diff --git a/packages/local_auth/local_auth_darwin/CHANGELOG.md b/packages/local_auth/local_auth_darwin/CHANGELOG.md index 48f6713772b..b0f65ac76a1 100644 --- a/packages/local_auth/local_auth_darwin/CHANGELOG.md +++ b/packages/local_auth/local_auth_darwin/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.0.0 + +* **BREAKING CHANGES:** + * Switches to `LocalAuthException` for error reporting. + * Removes support for `useErrorDialogs`. + ## 1.6.1 * Removes code for versions of iOS older than 13.0. diff --git a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift index 4682929caa9..71983979733 100644 --- a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift +++ b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift @@ -30,97 +30,6 @@ final class StubAuthContextFactory: AuthContextFactory { } } -final class StubViewProvider: ViewProvider { - #if os(macOS) - var view: NSView? - var window: NSWindow - init() { - self.window = NSWindow() - self.view = NSView() - self.window.contentView = self.view - } - #endif -} - -#if os(macOS) - final class TestAlert: AuthAlert { - var messageText: String = "" - var buttons: [String] = [] - var presentingWindow: NSWindow? - - func addButton(withTitle title: String) -> NSButton { - buttons.append(title) - return NSButton() // The return value is not used by the plugin. - } - - func beginSheetModal( - for sheetWindow: NSWindow, - completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil - ) { - presentingWindow = sheetWindow - handler?(NSApplication.ModalResponse.OK) - } - - func runModal() -> NSApplication.ModalResponse { - return NSApplication.ModalResponse.OK - } - } -#else - final class TestAlertController: AuthAlertController { - var actions: [UIAlertAction] = [] - var presented = false - var presentingViewController: UIViewController? - // The handler to trigger when present is called, to simulate an action selection. - var onPresentActionHandler: ((UIAlertAction) -> Void)? - - func addAction(_ action: UIAlertAction) { - actions.append(action) - } - - func present( - on presentingViewController: UIViewController, animated: Bool, - completion: (() -> Void)? = nil - ) { - presented = true - self.presentingViewController = presentingViewController - // The plugin does not use the passed action, so just send a dummy value. If that ever - // changes, the test will need to track the action along with the handler. - onPresentActionHandler?(UIAlertAction()) - } - } - -#endif - -final class StubAlertFactory: AuthAlertFactory { - #if os(macOS) - var alert: TestAlert = TestAlert() - #else - var alertController: TestAlertController = TestAlertController() - #endif - - #if os(macOS) - func createAlert() -> AuthAlert { - return self.alert - } - #else - func createAlertController( - title: String?, message: String?, preferredStyle: UIAlertController.Style - ) -> AuthAlertController { - return self.alertController - } - - func createAlertAction( - title: String?, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)? - ) -> UIAlertAction { - // Configure the fake controller to trigger this button when presented. This is currently an - // arbitrary button, just to ensure that the completion handler is triggered so that the - // test can wait for the full cycle of async calls to complete. - alertController.onPresentActionHandler = handler - return UIAlertAction(title: title, style: style, handler: handler) - } - #endif -} - final class StubAuthContext: NSObject, AuthContext { /// Whether calls to this stub are expected to be for biometric authentication. /// @@ -171,23 +80,17 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testSuccessfullAuthWithBiometrics() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider - ) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.expectBiometrics = true stubAuthContext.evaluateResponse = true let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: true, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: true, sticky: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): XCTAssertEqual(successDetails.result, .success) @@ -202,23 +105,17 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testSuccessfullAuthWithoutBiometrics() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() - let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateResponse = true let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): XCTAssertEqual(successDetails.result, .success) @@ -233,12 +130,8 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testFailedAuthWithBiometrics() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.expectBiometrics = true @@ -247,17 +140,64 @@ class LocalAuthPluginTests: XCTestCase { let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: true, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: true, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .authenticationFailed) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorAppCancel() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError( + domain: "LocalAuthentication", code: LAError.appCancel.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .appCancel) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithErrorSystemCancel() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError( + domain: "LocalAuthentication", code: LAError.systemCancel.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) - // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration - // behavior, so is preserved as part of the migration, but a failed - // authentication should return failure, not an error that results in a - // PlatformException. switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .errorNotAvailable) + XCTAssertEqual(successDetails.result, .systemCancel) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -267,28 +207,23 @@ class LocalAuthPluginTests: XCTestCase { } @MainActor - func testFailedAuthWithErrorUserCancelled() { + func testFailedAuthWithErrorUserCancel() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( domain: "LocalAuthentication", code: LAError.userCancel.rawValue) - let expectation = expectation(description: "Result is called for user cancel") + let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .errorUserCancelled) + XCTAssertEqual(successDetails.result, .userCancel) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -300,26 +235,130 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testFailedAuthWithErrorUserFallback() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.evaluateError = NSError( domain: "LocalAuthentication", code: LAError.userFallback.rawValue) - let expectation = expectation(description: "Result is called for user fallback") + let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .errorUserFallback) + XCTAssertEqual(successDetails.result, .userFallback) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + #if os(macOS) + @available(macOS 11.2, *) + @MainActor + func testFailedAuthWithErrorBiometricDisconnected() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryDisconnected.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .biometryDisconnected) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @available(macOS 11.2, *) + @MainActor + func testFailedAuthWithErrorBiometricNotPaired() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryNotPaired.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .biometryNotPaired) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @available(macOS 12.0, *) + @MainActor + func testFailedAuthWithErrorBiometricInvalidDimensions() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.invalidDimensions.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .invalidDimensions) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + #endif + + @MainActor + func testFailedAuthWithErrorBiometricLockout() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryLockout.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .biometryLockout) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -331,26 +370,21 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testFailedAuthWithErrorBiometricNotAvailable() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.canEvaluateError = NSError( domain: "LocalAuthentication", code: LAError.biometryNotAvailable.rawValue) - let expectation = expectation(description: "Result is called for biometric not available") + let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .errorBiometricNotAvailable) + XCTAssertEqual(successDetails.result, .biometryNotAvailable) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -360,27 +394,23 @@ class LocalAuthPluginTests: XCTestCase { } @MainActor - func testFailedWithUnknownErrorCode() { + func testFailedAuthWithErrorBiometricNotEnrolled() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() - stubAuthContext.evaluateError = NSError(domain: "error", code: 99) + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.biometryNotEnrolled.rawValue) let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .errorNotAvailable) + XCTAssertEqual(successDetails.result, .biometryNotEnrolled) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -390,27 +420,23 @@ class LocalAuthPluginTests: XCTestCase { } @MainActor - func testSystemCancelledWithoutStickyAuth() { + func testFailedAuthWithErrorBiometricInvalidContext() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() - stubAuthContext.evaluateError = NSError(domain: "error", code: LAError.systemCancel.rawValue) + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.invalidContext.rawValue) let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .failure) + XCTAssertEqual(successDetails.result, .invalidContext) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -420,32 +446,23 @@ class LocalAuthPluginTests: XCTestCase { } @MainActor - func testFailedAuthWithoutBiometrics() { + func testFailedAuthWithErrorBiometricNotInteractive() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() - stubAuthContext.evaluateError = NSError( - domain: "error", code: LAError.authenticationFailed.rawValue) + stubAuthContext.canEvaluateError = NSError( + domain: "LocalAuthentication", code: LAError.notInteractive.rawValue) let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in - XCTAssertTrue(Thread.isMainThread) - // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration - // behavior, so is preserved as part of the migration, but a failed - // authentication should return failure, not an error that results in a - // PlatformException. switch resultDetails { case .success(let successDetails): - XCTAssertEqual(successDetails.result, .errorNotAvailable) + XCTAssertEqual(successDetails.result, .notInteractive) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -455,52 +472,119 @@ class LocalAuthPluginTests: XCTestCase { } @MainActor - func testFailedAuthShowsAlert() { + func testFailedAuthWithErrorBiometricPasscodeNotSet() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings() stubAuthContext.canEvaluateError = NSError( - domain: "error", code: LAError.biometryNotEnrolled.rawValue) + domain: "LocalAuthentication", code: LAError.passcodeNotSet.rawValue) let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: true), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .passcodeNotSet) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } expectation.fulfill() } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedWithUnknownErrorCode() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError(domain: "error", code: 99) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .unknownError) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testSystemCancelledWithoutStickyAuth() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError(domain: "error", code: LAError.systemCancel.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .systemCancel) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + @MainActor + func testFailedAuthWithoutBiometrics() { + let stubAuthContext = StubAuthContext() + let plugin = LocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError( + domain: "error", code: LAError.authenticationFailed.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + options: AuthOptions(biometricOnly: false, sticky: false), + strings: strings + ) { resultDetails in + switch resultDetails { + case .success(let successDetails): + XCTAssertEqual(successDetails.result, .authenticationFailed) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } self.waitForExpectations(timeout: timeout) - #if os(macOS) - XCTAssertEqual(alertFactory.alert.presentingWindow, viewProvider.view?.window) - #else - XCTAssertTrue(alertFactory.alertController.presented) - XCTAssertEqual(alertFactory.alertController.actions.count, 2) - #endif } @MainActor func testLocalizedFallbackTitle() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings(localizedFallbackTitle: "a title") stubAuthContext.evaluateResponse = true let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in XCTAssertEqual(stubAuthContext.localizedFallbackTitle, strings.localizedFallbackTitle) @@ -512,19 +596,15 @@ class LocalAuthPluginTests: XCTestCase { @MainActor func testSkippedLocalizedFallbackTitle() { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let strings = createAuthStrings(localizedFallbackTitle: nil) stubAuthContext.evaluateResponse = true let expectation = expectation(description: "Result is called") plugin.authenticate( - options: AuthOptions(biometricOnly: false, sticky: false, useErrorDialogs: false), + options: AuthOptions(biometricOnly: false, sticky: false), strings: strings ) { resultDetails in XCTAssertNil(stubAuthContext.localizedFallbackTitle) @@ -535,12 +615,8 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withEnrolledHardware() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true @@ -550,12 +626,8 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withNonEnrolledHardware() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.canEvaluateError = NSError( @@ -567,12 +639,8 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withBiometryNotAvailable() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.canEvaluateError = NSError( @@ -584,12 +652,8 @@ class LocalAuthPluginTests: XCTestCase { func testDeviceSupportsBiometrics_withBiometryNotAvailableWhenPermissionsDenied() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.biometryType = LABiometryType.touchID @@ -602,12 +666,8 @@ class LocalAuthPluginTests: XCTestCase { func testGetEnrolledBiometricsWithFaceID() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.biometryType = .faceID @@ -619,12 +679,8 @@ class LocalAuthPluginTests: XCTestCase { func testGetEnrolledBiometricsWithTouchID() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.biometryType = .touchID @@ -636,12 +692,8 @@ class LocalAuthPluginTests: XCTestCase { func testGetEnrolledBiometricsWithoutEnrolledHardware() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) stubAuthContext.expectBiometrics = true stubAuthContext.canEvaluateError = NSError( @@ -653,12 +705,8 @@ class LocalAuthPluginTests: XCTestCase { func testIsDeviceSupportedHandlesSupported() throws { let stubAuthContext = StubAuthContext() - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let result = try plugin.isDeviceSupported() XCTAssertTrue(result) @@ -668,12 +716,8 @@ class LocalAuthPluginTests: XCTestCase { let stubAuthContext = StubAuthContext() // An arbitrary error to cause canEvaluatePolicy to return false. stubAuthContext.canEvaluateError = NSError(domain: "error", code: 1) - let alertFactory = StubAlertFactory() - let viewProvider = StubViewProvider() let plugin = LocalAuthPlugin( - contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]), - alertFactory: alertFactory, - viewProvider: viewProvider) + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) let result = try plugin.isDeviceSupported() XCTAssertFalse(result) @@ -683,9 +727,6 @@ class LocalAuthPluginTests: XCTestCase { func createAuthStrings(localizedFallbackTitle: String? = nil) -> AuthStrings { return AuthStrings( reason: "a reason", - lockOut: "locked out", - goToSettingsButton: "Go To Settings", - goToSettingsDescription: "Settings", cancelButton: "Cancel", localizedFallbackTitle: localizedFallbackTitle) } diff --git a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/LocalAuthPlugin.swift b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/LocalAuthPlugin.swift index 143599b86c7..3222b795eb5 100644 --- a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/LocalAuthPlugin.swift +++ b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/LocalAuthPlugin.swift @@ -21,80 +21,6 @@ final class DefaultAuthContextFactory: AuthContextFactory { // MARK: - -#if os(iOS) - /// A default alert controller that wraps UIAlertController. - final class DefaultAlertController: AuthAlertController { - /// The wrapped alert controller. - private let controller: UIAlertController - - /// Returns a wrapper for the given UIAlertController. - init(wrapping controller: UIAlertController) { - self.controller = controller - } - - @MainActor - func addAction(_ action: UIAlertAction) { - controller.addAction(action) - } - - @MainActor - func present( - on presentingViewController: UIViewController, - animated: Bool, - completion: (() -> Void)? = nil - ) { - presentingViewController.present(controller, animated: animated, completion: completion) - } - } -#endif // os(iOS) - -/// A default alert factory that wraps standard UIAlertController and NSAlert allocation for iOS and -/// macOS respectfully. -final class DefaultAlertFactory: AuthAlertFactory { - #if os(macOS) - func createAlert() -> AuthAlert { - return NSAlert() - } - #elseif os(iOS) - func createAlertController( - title: String?, - message: String?, - preferredStyle: UIAlertController.Style - ) -> AuthAlertController { - return DefaultAlertController( - wrapping: - UIAlertController(title: title, message: message, preferredStyle: preferredStyle)) - } - - func createAlertAction( - title: String?, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)? = nil - ) -> UIAlertAction { - return UIAlertAction(title: title, style: style, handler: handler) - } - #endif -} - -// MARK: - - -/// A default view provider that wraps the FlutterPluginRegistrar. -final class DefaultViewProvider: ViewProvider { - /// The wrapped registrar. - let registrar: FlutterPluginRegistrar - - /// Returns a wrapper for the given FlutterPluginRegistrar. - init(registrar: FlutterPluginRegistrar) { - self.registrar = registrar - } - - #if os(macOS) - var view: NSView? { - return registrar.view - } - #endif // os(macOS) -} - -// MARK: - - /// A data container for sticky auth state. struct StickyAuthState { let options: AuthOptions @@ -111,18 +37,12 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch /// The factory to create LAContexts. private let authContextFactory: AuthContextFactory - /// The factory to create alerts. - private let alertFactory: AuthAlertFactory - /// The Flutter view provider. - private let viewProvider: ViewProvider /// Manages the last call state for sticky auth. private var lastCallState: StickyAuthState? public static func register(with registrar: FlutterPluginRegistrar) { let instance = LocalAuthPlugin( - contextFactory: DefaultAuthContextFactory(), - alertFactory: DefaultAlertFactory(), - viewProvider: DefaultViewProvider(registrar: registrar)) + contextFactory: DefaultAuthContextFactory()) registrar.addApplicationDelegate(instance) // Workaround for https://github.com/flutter/flutter/issues/118103. #if os(iOS) @@ -135,13 +55,9 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch /// Returns an instance that uses the given factory to create LAContexts. init( - contextFactory: AuthContextFactory, - alertFactory: AuthAlertFactory, - viewProvider: ViewProvider + contextFactory: AuthContextFactory ) { self.authContextFactory = contextFactory - self.alertFactory = alertFactory - self.viewProvider = viewProvider } // MARK: LocalAuthApi @@ -183,8 +99,8 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch completion( .success( AuthResultDetails( - result: .failure, - errorMessage: "evaluatePolicy failed without an error" + result: .unknownError, + errorMessage: "canEvaluatePolicy failed without an error" ))) } } @@ -239,62 +155,6 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch // MARK: Private Methods - @MainActor - private func showAlert( - message: String, - dismissButtonTitle: String, - openSettingsButtonTitle: String?, - completion: @escaping (Result) -> Void - ) { - #if os(macOS) - var alert = alertFactory.createAlert() - alert.messageText = message - alert.addButton(withTitle: dismissButtonTitle) - if let window = viewProvider.view?.window { - alert.beginSheetModal(for: window) { [weak self] code in - self?.handleResult(succeeded: false, completion: completion) - } - } else { - alert.runModal() - self.handleResult(succeeded: false, completion: completion) - } - #elseif os(iOS) - // TODO(stuartmorgan): Get the view controller from the view provider once it's possible. - // See https://github.com/flutter/flutter/issues/104117. - guard let controller = UIApplication.shared.delegate?.window??.rootViewController else { - // TODO(stuartmorgan): Create a new error code for failure to show UI, and return it here. - self.handleResult(succeeded: false, completion: completion) - return - } - let alert = alertFactory.createAlertController( - title: "", - message: message, - preferredStyle: .alert) - - let defaultAction = alertFactory.createAlertAction( - title: dismissButtonTitle, - style: .default - ) { [weak self] action in - self?.handleResult(succeeded: false, completion: completion) - } - - alert.addAction(defaultAction) - if let openSettingsButtonTitle = openSettingsButtonTitle, - let url = URL(string: UIApplication.openSettingsURLString) - { - let additionalAction = UIAlertAction( - title: openSettingsButtonTitle, - style: .default - ) { [weak self] action in - UIApplication.shared.open(url, options: [:], completionHandler: nil) - self?.handleResult(succeeded: false, completion: completion) - } - alert.addAction(additionalAction) - } - alert.present(on: controller, animated: true, completion: nil) - #endif - } - private func handleAuthReply( success: Bool, error: Error?, @@ -303,54 +163,38 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch completion: @escaping (Result) -> Void ) { if success { - handleResult(succeeded: true, completion: completion) + handleResult(result: .success, completion: completion) return } if let error = error as? NSError { - switch LAError.Code(rawValue: error.code) { - case .biometryNotAvailable, - .biometryNotEnrolled, - .biometryLockout, - .userFallback, - .passcodeNotSet, - .authenticationFailed: - handleError(error, options: options, strings: strings, completion: completion) - case .systemCancel: - if options.sticky { - lastCallState = StickyAuthState( - options: options, - strings: strings, - resultHandler: completion) - } else { - handleResult(succeeded: false, completion: completion) - } - default: + if error.code == LAError.Code.systemCancel.rawValue && options.sticky { + lastCallState = StickyAuthState( + options: options, + strings: strings, + resultHandler: completion) + } else { handleError(error, options: options, strings: strings, completion: completion) } } else { - // The Obj-C declaration of evaluatePolicy defines the callback type as NSError*, but the - // Swift version is (any Error)?, so provide a fallback in case somehow the type is not - // NSError. - // TODO(stuartmorgan): Add an "unknown error" enum option and return that here instead of - // failure. + // This should not happen according to docs, but if it ever does the plugin should still + // fire the completion. completion( .success( AuthResultDetails( - result: .failure, - errorMessage: "Unknown error from evaluatePolicy", - errorDetails: error?.localizedDescription) - )) + result: .unknownError, + errorMessage: "evaluatePolicy failed without an error" + ))) } } private func handleResult( - succeeded: Bool, completion: @escaping (Result) -> Void + result: AuthResult, completion: @escaping (Result) -> Void ) { completion( .success( AuthResultDetails( - result: succeeded ? .success : .failure, + result: result, errorMessage: nil, errorDetails: nil) )) @@ -365,45 +209,43 @@ public final class LocalAuthPlugin: NSObject, FlutterPlugin, LocalAuthApi, @unch let result: AuthResult let errorCode = LAError.Code(rawValue: authError.code) switch errorCode { - case .passcodeNotSet, - .biometryNotEnrolled: - if options.useErrorDialogs { - DispatchQueue.main.async { [weak self] in - self?.showAlert( - message: strings.goToSettingsDescription, - dismissButtonTitle: strings.cancelButton, - openSettingsButtonTitle: strings.goToSettingsButton, - completion: completion) - } - return - } - result = errorCode == .passcodeNotSet ? .errorPasscodeNotSet : .errorNotEnrolled + case .appCancel: + result = .appCancel + case .systemCancel: + result = .systemCancel case .userCancel: - result = .errorUserCancelled - case .userFallback: - result = .errorUserFallback - case .biometryNotAvailable: - result = .errorBiometricNotAvailable + result = .userCancel + case .biometryDisconnected: + result = .biometryDisconnected case .biometryLockout: - DispatchQueue.main.async { [weak self] in - self?.showAlert( - message: strings.lockOut, - dismissButtonTitle: strings.cancelButton, - openSettingsButtonTitle: nil, - completion: completion) - } - return + result = .biometryLockout + case .biometryNotAvailable: + result = .biometryNotAvailable + case .biometryNotEnrolled: + result = .biometryNotEnrolled + case .biometryNotPaired: + result = .biometryNotPaired + case .authenticationFailed: + result = .authenticationFailed + case .invalidContext: + result = .invalidContext + case .invalidDimensions: + result = .invalidDimensions + case .notInteractive: + result = .notInteractive + case .passcodeNotSet: + result = .passcodeNotSet + case .userFallback: + result = .userFallback default: - // TODO(stuartmorgan): Improve the error mapping as part of a cross-platform overhaul of - // error handling. See https://github.com/flutter/flutter/issues/113687 - result = .errorNotAvailable + result = .unknownError } completion( .success( AuthResultDetails( result: result, errorMessage: authError.localizedDescription, - errorDetails: authError.domain) + errorDetails: "\(authError.domain): \(authError.code)") )) } diff --git a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/SystemWrappers.swift b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/SystemWrappers.swift index 2b8b33c08ce..f9bec200ba6 100644 --- a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/SystemWrappers.swift +++ b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/SystemWrappers.swift @@ -46,93 +46,3 @@ protocol AuthContextFactory { /// In production code, this should return an LAContext. func createAuthContext() -> AuthContext } - -// MARK: - - -#if os(macOS) - /// Protocol for interacting with NSAlert instances, abstracted to allow using mock/fake instances - /// in unit tests. - protocol AuthAlert { - /// Direct passthrough to NSAlert's messageText. - @MainActor - var messageText: String { get set } - - /// Direct passthrough to NSAlert's addButton. - @MainActor - @discardableResult func addButton(withTitle title: String) -> NSButton - - /// Direct passthrough to NSAlert's beginSheetModal. - @MainActor - func beginSheetModal( - for sheetWindow: NSWindow, - completionHandler handler: ((NSApplication.ModalResponse) -> Void)? - ) - - /// Direct passthrough to NSAlert's runModal. - @MainActor - @discardableResult func runModal() -> NSApplication.ModalResponse - } - - /// AuthAlert is intentionally a direct passthroguh to NSAlert. - extension NSAlert: AuthAlert {} -#endif // macOS - -#if os(iOS) - /// Protocol for interacting with UIAlertController instances, abstracted to allow using mock/fake - /// instances in unit tests. - protocol AuthAlertController { - /// Direct passthrough to UIAlertController's addAction. - @MainActor - func addAction(_ action: UIAlertAction) - - /// Reversed wrapper of presentViewController:... since the protocol can't be passed to the real - /// method. - @MainActor - func present( - on presentingViewController: UIViewController, - animated: Bool, - completion: (() -> Void)? - ) - } -#endif // iOS - -/// Protocol for a factory that wraps standard UIAlertController and NSAlert creation for -/// iOS and macOS. Used to allow context injection in unit tests. -protocol AuthAlertFactory { - #if os(macOS) - /// Creates a new instance of an implementation of the AuthAlert abstraction. - /// - /// In production code, this should return an NSAlert. - func createAlert() -> AuthAlert - #elseif os(iOS) - /// Creates a new instance of an implementation of the AuthAlertController abstraction. - /// - /// In production code, this should return something as close as possible to a direct passthrough - /// to UIAlertController. - func createAlertController( - title: String?, - message: String?, - preferredStyle: UIAlertController.Style - ) -> AuthAlertController - - /// Creates a new instance of a UIAlertAction. - /// - /// Abstracted to allow unit tests to capture the handler, since UIAlertAction does not provide - /// a getter for the handler. - func createAlertAction( - title: String?, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)? - ) -> UIAlertAction - #endif -} - -/// Protocol for a provider of the view containing the Flutter content, abstracted to allow using -/// mock/fake instances in unit tests. -protocol ViewProvider { - #if os(macOS) - /// Returns the view displaying the Flutter content, if any. - var view: NSView? { get } - #elseif os(iOS) - // TODO(stuartmorgan): Add a view accessor once https://github.com/flutter/flutter/issues/104117 - // is resolved, and use that in 'showAlertWithMessage:...'. - #endif -} diff --git a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/messages.g.swift b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/messages.g.swift index 184bab81314..c14a148a206 100644 --- a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/messages.g.swift +++ b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/messages.g.swift @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v25.5.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -133,20 +133,24 @@ func deepHashmessages(value: Any?, hasher: inout Hasher) { enum AuthResult: Int { /// The user authenticated successfully. case success = 0 - /// The user failed to successfully authenticate. - case failure = 1 - /// The authentication system was not available. - case errorNotAvailable = 2 - /// No biometrics are enrolled. - case errorNotEnrolled = 3 - /// No passcode is set. - case errorPasscodeNotSet = 4 - /// The user cancelled the authentication. - case errorUserCancelled = 5 - /// The user tapped the "Enter Password" fallback. - case errorUserFallback = 6 - /// The user biometrics is disabled. - case errorBiometricNotAvailable = 7 + /// Native UI needed to be displayed, but couldn't be. + case uiUnavailable = 1 + case appCancel = 2 + case systemCancel = 3 + case userCancel = 4 + case biometryDisconnected = 5 + case biometryLockout = 6 + case biometryNotAvailable = 7 + case biometryNotEnrolled = 8 + case biometryNotPaired = 9 + case authenticationFailed = 10 + case invalidContext = 11 + case invalidDimensions = 12 + case notInteractive = 13 + case passcodeNotSet = 14 + case userFallback = 15 + /// An error other than the expected types occurred. + case unknownError = 16 } /// Pigeon equivalent of the subset of BiometricType used by iOS. @@ -162,26 +166,17 @@ enum AuthBiometric: Int { /// Generated class from Pigeon that represents data sent in messages. struct AuthStrings: Hashable { var reason: String - var lockOut: String - var goToSettingsButton: String? = nil - var goToSettingsDescription: String var cancelButton: String var localizedFallbackTitle: String? = nil // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> AuthStrings? { let reason = pigeonVar_list[0] as! String - let lockOut = pigeonVar_list[1] as! String - let goToSettingsButton: String? = nilOrValue(pigeonVar_list[2]) - let goToSettingsDescription = pigeonVar_list[3] as! String - let cancelButton = pigeonVar_list[4] as! String - let localizedFallbackTitle: String? = nilOrValue(pigeonVar_list[5]) + let cancelButton = pigeonVar_list[1] as! String + let localizedFallbackTitle: String? = nilOrValue(pigeonVar_list[2]) return AuthStrings( reason: reason, - lockOut: lockOut, - goToSettingsButton: goToSettingsButton, - goToSettingsDescription: goToSettingsDescription, cancelButton: cancelButton, localizedFallbackTitle: localizedFallbackTitle ) @@ -189,9 +184,6 @@ struct AuthStrings: Hashable { func toList() -> [Any?] { return [ reason, - lockOut, - goToSettingsButton, - goToSettingsDescription, cancelButton, localizedFallbackTitle, ] @@ -208,25 +200,21 @@ struct AuthStrings: Hashable { struct AuthOptions: Hashable { var biometricOnly: Bool var sticky: Bool - var useErrorDialogs: Bool // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> AuthOptions? { let biometricOnly = pigeonVar_list[0] as! Bool let sticky = pigeonVar_list[1] as! Bool - let useErrorDialogs = pigeonVar_list[2] as! Bool return AuthOptions( biometricOnly: biometricOnly, - sticky: sticky, - useErrorDialogs: useErrorDialogs + sticky: sticky ) } func toList() -> [Any?] { return [ biometricOnly, sticky, - useErrorDialogs, ] } static func == (lhs: AuthOptions, rhs: AuthOptions) -> Bool { diff --git a/packages/local_auth/local_auth_darwin/example/lib/main.dart b/packages/local_auth/local_auth_darwin/example/lib/main.dart index 424df7eb252..0c9fabec413 100644 --- a/packages/local_auth/local_auth_darwin/example/lib/main.dart +++ b/packages/local_auth/local_auth_darwin/example/lib/main.dart @@ -92,11 +92,21 @@ class _MyAppState extends State { setState(() { _isAuthenticating = false; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected Error - ${e.message}'; }); return; } @@ -129,11 +139,21 @@ class _MyAppState extends State { _isAuthenticating = false; _authorized = 'Authenticating'; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + if (e.code != LocalAuthExceptionCode.userCanceled && + e.code != LocalAuthExceptionCode.systemCanceled) { + _authorized = 'Error - ${e.code.name}: ${e.description}'; + } + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected Error - ${e.message}'; }); return; } diff --git a/packages/local_auth/local_auth_darwin/example/pubspec.yaml b/packages/local_auth/local_auth_darwin/example/pubspec.yaml index 2cbb79b33bb..99959c4cb2f 100644 --- a/packages/local_auth/local_auth_darwin/example/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - local_auth_platform_interface: ^1.0.0 + local_auth_platform_interface: ^1.1.0 dev_dependencies: flutter_test: diff --git a/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart b/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart index 90545be12d3..82ee3554e39 100644 --- a/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart +++ b/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; import 'src/messages.g.dart'; @@ -45,59 +44,54 @@ class LocalAuthDarwin extends LocalAuthPlatform { AuthOptions( biometricOnly: options.biometricOnly, sticky: options.stickyAuth, - useErrorDialogs: options.useErrorDialogs, ), _useMacOSAuthMessages ? _pigeonStringsFromMacOSAuthMessages(localizedReason, authMessages) : _pigeonStringsFromiOSAuthMessages(localizedReason, authMessages), ); - // TODO(stuartmorgan): Replace this with structured errors, coordinated - // across all platform implementations, per - // https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#platform-exception-handling - // The PlatformExceptions thrown here are for compatibiilty with the - // previous Objective-C implementation. + LocalAuthExceptionCode code; switch (resultDetails.result) { case AuthResult.success: return true; - case AuthResult.failure: + case AuthResult.authenticationFailed: return false; - case AuthResult.errorNotAvailable: - throw PlatformException( - code: 'NotAvailable', - message: resultDetails.errorMessage, - details: resultDetails.errorDetails, - ); - case AuthResult.errorNotEnrolled: - throw PlatformException( - code: 'NotEnrolled', - message: resultDetails.errorMessage, - details: resultDetails.errorDetails, - ); - case AuthResult.errorPasscodeNotSet: - throw PlatformException( - code: 'PasscodeNotSet', - message: resultDetails.errorMessage, - details: resultDetails.errorDetails, - ); - case AuthResult.errorUserCancelled: - throw PlatformException( - code: 'UserCancelled', - message: resultDetails.errorMessage, - details: resultDetails.errorDetails, - ); - case AuthResult.errorBiometricNotAvailable: - throw PlatformException( - code: 'BiometricNotAvailable', - message: resultDetails.errorMessage, - details: resultDetails.errorDetails, - ); - case AuthResult.errorUserFallback: - throw PlatformException( - code: 'UserFallback', - message: resultDetails.errorMessage, - details: resultDetails.errorDetails, - ); + case AuthResult.appCancel: + // If the plugin client intentionally canceled authentication, no need + // to return a specific error. + return false; + case AuthResult.uiUnavailable: + code = LocalAuthExceptionCode.uiUnavailable; + case AuthResult.systemCancel: + code = LocalAuthExceptionCode.systemCanceled; + case AuthResult.userCancel: + code = LocalAuthExceptionCode.userCanceled; + case AuthResult.biometryDisconnected: + code = LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable; + case AuthResult.biometryLockout: + code = LocalAuthExceptionCode.biometricLockout; + case AuthResult.biometryNotAvailable: + // Treated as no hardware since docs suggest that this means that there is + // no known device; paired but not connected is biometryDisconnected. + case AuthResult.biometryNotPaired: + code = LocalAuthExceptionCode.noBiometricHardware; + case AuthResult.biometryNotEnrolled: + code = LocalAuthExceptionCode.noBiometricsEnrolled; + case AuthResult.invalidContext: + case AuthResult.invalidDimensions: + case AuthResult.notInteractive: + code = LocalAuthExceptionCode.uiUnavailable; + case AuthResult.passcodeNotSet: + code = LocalAuthExceptionCode.noCredentialsSet; + case AuthResult.userFallback: + code = LocalAuthExceptionCode.userRequestedFallback; + case AuthResult.unknownError: + code = LocalAuthExceptionCode.unknownError; } + throw LocalAuthException( + code: code, + description: resultDetails.errorMessage, + details: resultDetails.errorDetails, + ); } @override @@ -138,13 +132,7 @@ class LocalAuthDarwin extends LocalAuthPlatform { } return AuthStrings( reason: localizedReason, - lockOut: messages?.lockOut ?? iOSLockOut, - goToSettingsButton: messages?.goToSettingsButton ?? goToSettings, - goToSettingsDescription: - messages?.goToSettingsDescription ?? iOSGoToSettingsDescription, - // TODO(stuartmorgan): The default's name is confusing here for legacy - // reasons; this should be fixed as part of some future breaking change. - cancelButton: messages?.cancelButton ?? iOSOkButton, + cancelButton: messages?.cancelButton ?? iOSCancelButton, localizedFallbackTitle: messages?.localizedFallbackTitle, ); } @@ -162,9 +150,6 @@ class LocalAuthDarwin extends LocalAuthPlatform { } return AuthStrings( reason: localizedReason, - lockOut: messages?.lockOut ?? macOSLockOut, - goToSettingsDescription: - messages?.goToSettingsDescription ?? macOSGoToSettingsDescription, cancelButton: messages?.cancelButton ?? macOSCancelButton, localizedFallbackTitle: messages?.localizedFallbackTitle, ); diff --git a/packages/local_auth/local_auth_darwin/lib/src/messages.g.dart b/packages/local_auth/local_auth_darwin/lib/src/messages.g.dart index 21131267d85..d895007d346 100644 --- a/packages/local_auth/local_auth_darwin/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_darwin/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v25.5.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -41,26 +41,25 @@ enum AuthResult { /// The user authenticated successfully. success, - /// The user failed to successfully authenticate. - failure, - - /// The authentication system was not available. - errorNotAvailable, - - /// No biometrics are enrolled. - errorNotEnrolled, - - /// No passcode is set. - errorPasscodeNotSet, - - /// The user cancelled the authentication. - errorUserCancelled, - - /// The user tapped the "Enter Password" fallback. - errorUserFallback, - - /// The user biometrics is disabled. - errorBiometricNotAvailable, + /// Native UI needed to be displayed, but couldn't be. + uiUnavailable, + appCancel, + systemCancel, + userCancel, + biometryDisconnected, + biometryLockout, + biometryNotAvailable, + biometryNotEnrolled, + biometryNotPaired, + authenticationFailed, + invalidContext, + invalidDimensions, + notInteractive, + passcodeNotSet, + userFallback, + + /// An error other than the expected types occurred. + unknownError, } /// Pigeon equivalent of the subset of BiometricType used by iOS. @@ -72,34 +71,18 @@ enum AuthBiometric { face, fingerprint } class AuthStrings { AuthStrings({ required this.reason, - required this.lockOut, - this.goToSettingsButton, - required this.goToSettingsDescription, required this.cancelButton, this.localizedFallbackTitle, }); String reason; - String lockOut; - - String? goToSettingsButton; - - String goToSettingsDescription; - String cancelButton; String? localizedFallbackTitle; List _toList() { - return [ - reason, - lockOut, - goToSettingsButton, - goToSettingsDescription, - cancelButton, - localizedFallbackTitle, - ]; + return [reason, cancelButton, localizedFallbackTitle]; } Object encode() { @@ -110,11 +93,8 @@ class AuthStrings { result as List; return AuthStrings( reason: result[0]! as String, - lockOut: result[1]! as String, - goToSettingsButton: result[2] as String?, - goToSettingsDescription: result[3]! as String, - cancelButton: result[4]! as String, - localizedFallbackTitle: result[5] as String?, + cancelButton: result[1]! as String, + localizedFallbackTitle: result[2] as String?, ); } @@ -136,20 +116,14 @@ class AuthStrings { } class AuthOptions { - AuthOptions({ - required this.biometricOnly, - required this.sticky, - required this.useErrorDialogs, - }); + AuthOptions({required this.biometricOnly, required this.sticky}); bool biometricOnly; bool sticky; - bool useErrorDialogs; - List _toList() { - return [biometricOnly, sticky, useErrorDialogs]; + return [biometricOnly, sticky]; } Object encode() { @@ -161,7 +135,6 @@ class AuthOptions { return AuthOptions( biometricOnly: result[0]! as bool, sticky: result[1]! as bool, - useErrorDialogs: result[2]! as bool, ); } diff --git a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart index 0e850ffdf27..49865d76e21 100644 --- a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart +++ b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_ios.dart @@ -11,25 +11,7 @@ import 'package:local_auth_platform_interface/types/auth_messages.dart'; @immutable class IOSAuthMessages extends AuthMessages { /// Constructs a new instance. - const IOSAuthMessages({ - this.lockOut, - this.goToSettingsButton, - this.goToSettingsDescription, - this.cancelButton, - this.localizedFallbackTitle, - }); - - /// Message advising the user to re-enable biometrics on their device. - final String? lockOut; - - /// Message shown on a button that the user can click to go to settings pages - /// from the current dialog. - /// Maximum 30 characters. - final String? goToSettingsButton; - - /// Message advising the user to go to the settings and configure Biometrics - /// for their device. - final String? goToSettingsDescription; + const IOSAuthMessages({this.cancelButton, this.localizedFallbackTitle}); /// Message shown on a button that the user can click to leave the current /// dialog. @@ -38,16 +20,14 @@ class IOSAuthMessages extends AuthMessages { /// The localized title for the fallback button in the dialog presented to /// the user during authentication. + /// + /// Set this to an empty string to hide the fallback button. final String? localizedFallbackTitle; @override Map get args { return { - 'lockOut': lockOut ?? iOSLockOut, - 'goToSetting': goToSettingsButton ?? goToSettings, - 'goToSettingDescriptionIOS': - goToSettingsDescription ?? iOSGoToSettingsDescription, - 'okButton': cancelButton ?? iOSOkButton, + 'okButton': cancelButton ?? iOSCancelButton, if (localizedFallbackTitle != null) 'localizedFallbackTitle': localizedFallbackTitle!, }; @@ -58,56 +38,20 @@ class IOSAuthMessages extends AuthMessages { identical(this, other) || other is IOSAuthMessages && runtimeType == other.runtimeType && - lockOut == other.lockOut && - goToSettingsButton == other.goToSettingsButton && - goToSettingsDescription == other.goToSettingsDescription && cancelButton == other.cancelButton && localizedFallbackTitle == other.localizedFallbackTitle; @override - int get hashCode => Object.hash( - super.hashCode, - lockOut, - goToSettingsButton, - goToSettingsDescription, - cancelButton, - localizedFallbackTitle, - ); + int get hashCode => + Object.hash(super.hashCode, cancelButton, localizedFallbackTitle); } // Default Strings for IOSAuthMessages plugin. Currently supports English. // Intl.message must be string literals. -/// Message shown on a button that the user can click to go to settings pages -/// from the current dialog. -String get goToSettings => Intl.message( - 'Go to settings', - desc: - 'Message shown on a button that the user can click to go to ' - 'settings pages from the current dialog. Maximum 30 characters.', -); - -/// Message advising the user to re-enable biometrics on their device. -/// It shows in a dialog on iOS. -String get iOSLockOut => Intl.message( - 'Biometric authentication is disabled. Please lock and unlock your screen to ' - 'enable it.', - desc: 'Message advising the user to re-enable biometrics on their device.', -); - -/// Message advising the user to go to the settings and configure Biometrics -/// for their device. -String get iOSGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Please either enable ' - 'Touch ID or Face ID on your phone.', - desc: - 'Message advising the user to go to the settings and configure Biometrics ' - 'for their device.', -); - /// Message shown on a button that the user can click to leave the current /// dialog. -String get iOSOkButton => Intl.message( +String get iOSCancelButton => Intl.message( 'OK', desc: 'Message showed on a button that the user can click to leave the ' diff --git a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart index cdfdbcf3779..292bf3ba986 100644 --- a/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart +++ b/packages/local_auth/local_auth_darwin/lib/types/auth_messages_macos.dart @@ -11,19 +11,7 @@ import 'package:local_auth_platform_interface/types/auth_messages.dart'; @immutable class MacOSAuthMessages extends AuthMessages { /// Constructs a new instance. - const MacOSAuthMessages({ - this.lockOut, - this.goToSettingsDescription, - this.cancelButton, - this.localizedFallbackTitle, - }); - - /// Message advising the user to re-enable biometrics on their device. - final String? lockOut; - - /// Message advising the user to go to the settings and configure Biometrics - /// for their device. - final String? goToSettingsDescription; + const MacOSAuthMessages({this.cancelButton, this.localizedFallbackTitle}); /// Message shown on a button that the user can click to leave the current /// dialog. @@ -32,12 +20,13 @@ class MacOSAuthMessages extends AuthMessages { /// The localized title for the fallback button in the dialog presented to /// the user during authentication. + /// + /// Set this to an empty string to hide the fallback button. final String? localizedFallbackTitle; @override Map get args { return { - 'lockOut': lockOut ?? macOSLockOut, 'okButton': cancelButton ?? macOSCancelButton, if (localizedFallbackTitle != null) 'localizedFallbackTitle': localizedFallbackTitle!, @@ -49,39 +38,17 @@ class MacOSAuthMessages extends AuthMessages { identical(this, other) || other is MacOSAuthMessages && runtimeType == other.runtimeType && - lockOut == other.lockOut && cancelButton == other.cancelButton && localizedFallbackTitle == other.localizedFallbackTitle; @override - int get hashCode => Object.hash( - super.hashCode, - lockOut, - cancelButton, - localizedFallbackTitle, - ); + int get hashCode => + Object.hash(super.hashCode, cancelButton, localizedFallbackTitle); } // Default Strings for MacOSAuthMessages plugin. Currently supports English. // Intl.message must be string literals. -/// Message advising the user to re-enable biometrics on their device. -/// It shows in a dialog on macOS. -String get macOSLockOut => Intl.message( - 'Biometric authentication is disabled. Please restart your computer and try again.', - desc: 'Message advising the user to re-enable biometrics on their device.', -); - -/// Message advising the user to go to the settings and configure Biometrics -/// for their device. -String get macOSGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Please enable ' - 'Touch ID on your computer in the Settings app.', - desc: - 'Message advising the user to go to the settings and configure Biometrics ' - 'for their device.', -); - /// Message shown on a button that the user can click to leave the current /// dialog. String get macOSCancelButton => Intl.message( diff --git a/packages/local_auth/local_auth_darwin/pigeons/messages.dart b/packages/local_auth/local_auth_darwin/pigeons/messages.dart index 6e91e7b229e..74a767977db 100644 --- a/packages/local_auth/local_auth_darwin/pigeons/messages.dart +++ b/packages/local_auth/local_auth_darwin/pigeons/messages.dart @@ -19,17 +19,11 @@ class AuthStrings { /// Constructs a new instance. const AuthStrings({ required this.reason, - required this.lockOut, - this.goToSettingsButton, - required this.goToSettingsDescription, required this.cancelButton, required this.localizedFallbackTitle, }); final String reason; - final String lockOut; - final String? goToSettingsButton; - final String goToSettingsDescription; final String cancelButton; final String? localizedFallbackTitle; } @@ -39,37 +33,34 @@ enum AuthResult { /// The user authenticated successfully. success, - /// The user failed to successfully authenticate. - failure, - - /// The authentication system was not available. - errorNotAvailable, - - /// No biometrics are enrolled. - errorNotEnrolled, - - /// No passcode is set. - errorPasscodeNotSet, - - /// The user cancelled the authentication. - errorUserCancelled, - - /// The user tapped the "Enter Password" fallback. - errorUserFallback, - - /// The user biometrics is disabled. - errorBiometricNotAvailable, + /// Native UI needed to be displayed, but couldn't be. + uiUnavailable, + + // LAError codes; see + // https://developer.apple.com/documentation/localauthentication/laerror-swift.struct/code + appCancel, + systemCancel, + userCancel, + biometryDisconnected, + biometryLockout, + biometryNotAvailable, + biometryNotEnrolled, + biometryNotPaired, + authenticationFailed, + invalidContext, + invalidDimensions, + notInteractive, + passcodeNotSet, + userFallback, + + /// An error other than the expected types occurred. + unknownError, } class AuthOptions { - AuthOptions({ - required this.biometricOnly, - required this.sticky, - required this.useErrorDialogs, - }); + AuthOptions({required this.biometricOnly, required this.sticky}); final bool biometricOnly; final bool sticky; - final bool useErrorDialogs; } class AuthResultDetails { @@ -86,10 +77,6 @@ class AuthResultDetails { final String? errorMessage; /// System-provided error details, if any. - // TODO(stuartmorgan): Remove this when standardizing errors plugin-wide in - // a breaking change. This is here only to preserve the existing error format - // exactly for compatibility, in case clients were checking PlatformException - // details. final String? errorDetails; } diff --git a/packages/local_auth/local_auth_darwin/pubspec.yaml b/packages/local_auth/local_auth_darwin/pubspec.yaml index 52b95b1b676..1a8fad8a31b 100644 --- a/packages/local_auth/local_auth_darwin/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_darwin description: iOS implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_darwin issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.6.1 +version: 2.0.0 environment: sdk: ^3.9.0 @@ -25,7 +25,7 @@ dependencies: flutter: sdk: flutter intl: ">=0.17.0 <0.21.0" - local_auth_platform_interface: ^1.0.1 + local_auth_platform_interface: ^1.1.0 dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.dart b/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.dart index a557d64c67c..4f6e37515e8 100644 --- a/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.dart +++ b/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:local_auth_darwin/local_auth_darwin.dart'; import 'package:local_auth_darwin/src/messages.g.dart'; @@ -108,10 +107,7 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_ios.dart - expect(strings.lockOut, iOSLockOut); - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, iOSGoToSettingsDescription); - expect(strings.cancelButton, iOSOkButton); + expect(strings.cancelButton, iOSCancelButton); expect(strings.localizedFallbackTitle, null); }); @@ -140,10 +136,7 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_ios.dart - expect(strings.lockOut, iOSLockOut); - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, iOSGoToSettingsDescription); - expect(strings.cancelButton, iOSOkButton); + expect(strings.cancelButton, iOSCancelButton); expect(strings.localizedFallbackTitle, null); }, ); @@ -173,8 +166,6 @@ void main() { expect(strings.reason, reason); // These should all be the default values from // auth_messages_ios.dart - expect(strings.lockOut, macOSLockOut); - expect(strings.goToSettingsDescription, macOSGoToSettingsDescription); expect(strings.cancelButton, macOSCancelButton); expect(strings.localizedFallbackTitle, null); }, @@ -196,19 +187,13 @@ void main() { // - they are different from the defaults, and // - they are different from each other. const String reason = 'A'; - const String lockOut = 'B'; - const String goToSettingsButton = 'C'; - const String gotToSettingsDescription = 'D'; - const String cancel = 'E'; - const String localizedFallbackTitle = 'F'; + const String cancel = 'B'; + const String localizedFallbackTitle = 'C'; await plugin.authenticate( localizedReason: reason, authMessages: [ const IOSAuthMessages( - lockOut: lockOut, - goToSettingsButton: goToSettingsButton, - goToSettingsDescription: gotToSettingsDescription, cancelButton: cancel, localizedFallbackTitle: localizedFallbackTitle, ), @@ -221,9 +206,6 @@ void main() { ); final AuthStrings strings = result.captured[0] as AuthStrings; expect(strings.reason, reason); - expect(strings.lockOut, lockOut); - expect(strings.goToSettingsButton, goToSettingsButton); - expect(strings.goToSettingsDescription, gotToSettingsDescription); expect(strings.cancelButton, cancel); expect(strings.localizedFallbackTitle, localizedFallbackTitle); }, @@ -244,14 +226,12 @@ void main() { // - they are different from the defaults, and // - they are different from each other. const String reason = 'A'; - const String lockOut = 'B'; - const String cancel = 'E'; - const String localizedFallbackTitle = 'F'; + const String cancel = 'B'; + const String localizedFallbackTitle = 'C'; await plugin.authenticate( localizedReason: reason, authMessages: [ const MacOSAuthMessages( - lockOut: lockOut, cancelButton: cancel, localizedFallbackTitle: localizedFallbackTitle, ), @@ -264,7 +244,6 @@ void main() { ); final AuthStrings strings = result.captured[0] as AuthStrings; expect(strings.reason, reason); - expect(strings.lockOut, lockOut); expect(strings.cancelButton, cancel); expect(strings.localizedFallbackTitle, localizedFallbackTitle); }, @@ -281,16 +260,12 @@ void main() { // - they are different from the defaults, and // - they are different from each other. const String reason = 'A'; - const String lockOut = 'B'; - const String localizedFallbackTitle = 'C'; - const String cancel = 'D'; + const String localizedFallbackTitle = 'B'; await plugin.authenticate( localizedReason: reason, authMessages: [ const IOSAuthMessages( - lockOut: lockOut, localizedFallbackTitle: localizedFallbackTitle, - cancelButton: cancel, ), ], ); @@ -299,15 +274,12 @@ void main() { api.authenticate(any, captureAny), ); final AuthStrings strings = result.captured[0] as AuthStrings; - expect(strings.reason, reason); // These should all be the provided values. - expect(strings.lockOut, lockOut); + expect(strings.reason, reason); expect(strings.localizedFallbackTitle, localizedFallbackTitle); - expect(strings.cancelButton, cancel); // These were not set, so should all be the default values from // auth_messages_ios.dart - expect(strings.goToSettingsButton, goToSettings); - expect(strings.goToSettingsDescription, iOSGoToSettingsDescription); + expect(strings.cancelButton, iOSCancelButton); }); }); @@ -330,7 +302,6 @@ void main() { final AuthOptions options = result.captured[0] as AuthOptions; expect(options.biometricOnly, false); expect(options.sticky, false); - expect(options.useErrorDialogs, true); }); test('passes provided non-default values', () async { @@ -344,7 +315,6 @@ void main() { options: const AuthenticationOptions( biometricOnly: true, stickyAuth: true, - useErrorDialogs: false, ), ); @@ -354,7 +324,6 @@ void main() { final AuthOptions options = result.captured[0] as AuthOptions; expect(options.biometricOnly, true); expect(options.sticky, true); - expect(options.useErrorDialogs, false); }); }); @@ -374,7 +343,8 @@ void main() { test('handles failure', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails(result: AuthResult.failure), + (_) async => + AuthResultDetails(result: AuthResult.authenticationFailed), ); final bool result = await plugin.authenticate( @@ -385,189 +355,548 @@ void main() { expect(result, false); }); - test('converts errorNotAvailable to legacy PlatformException', () async { - const String errorMessage = 'a message'; - const String errorDetails = 'some details'; + test('handles appCancel as failure', () async { when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails( - result: AuthResult.errorNotAvailable, - errorMessage: errorMessage, - errorDetails: errorDetails, - ), + (_) async => AuthResultDetails(result: AuthResult.appCancel), ); - expect( - () async => plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ), - throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'NotAvailable') - .having( - (PlatformException e) => e.message, - 'message', - errorMessage, - ) - .having( - (PlatformException e) => e.details, - 'details', - errorDetails, - ), - ), + final bool result = await plugin.authenticate( + localizedReason: 'reason', + authMessages: [], ); + + expect(result, false); }); - test('converts errorNotEnrolled to legacy PlatformException', () async { - const String errorMessage = 'a message'; - const String errorDetails = 'some details'; - when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails( - result: AuthResult.errorNotEnrolled, - errorMessage: errorMessage, - errorDetails: errorDetails, - ), - ); + test( + 'converts uiUnavailable to LocalAuthExceptionCode.uiUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.uiUnavailable, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); - expect( - () async => plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ), - throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'NotEnrolled') - .having( - (PlatformException e) => e.message, - 'message', - errorMessage, - ) - .having( - (PlatformException e) => e.details, - 'details', - errorDetails, - ), - ), - ); - }); + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); - test('converts errorUserCancelled to PlatformException', () async { - const String errorMessage = 'The user cancelled authentication.'; - const String errorDetails = 'com.apple.LocalAuthentication'; - when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails( - result: AuthResult.errorUserCancelled, - errorMessage: errorMessage, - errorDetails: errorDetails, - ), - ); + test( + 'converts systemCancel to LocalAuthExceptionCode.systemCanceled', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.systemCancel, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); - expect( - () async => plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ), - throwsA( - isA() - .having( - (PlatformException e) => e.code, - 'code', - 'UserCancelled', - ) - .having( - (PlatformException e) => e.message, - 'message', - errorMessage, - ) - .having( - (PlatformException e) => e.details, - 'details', - errorDetails, - ), - ), - ); - }); + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.systemCanceled, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); - test('converts errorUserFallback to PlatformException', () async { - const String errorMessage = 'The user chose to use the fallback.'; - const String errorDetails = 'com.apple.LocalAuthentication'; - when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails( - result: AuthResult.errorUserFallback, - errorMessage: errorMessage, - errorDetails: errorDetails, - ), - ); + test( + 'converts userCancel to LocalAuthExceptionCode.userCanceled', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.userCancel, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); - expect( - () async => plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ), - throwsA( - isA() - .having((PlatformException e) => e.code, 'code', 'UserFallback') - .having( - (PlatformException e) => e.message, - 'message', - errorMessage, - ) - .having( - (PlatformException e) => e.details, - 'details', - errorDetails, - ), - ), - ); - }); + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.userCanceled, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); - test('converts errorBiometricNotAvailable to PlatformException', () async { - const String errorMessage = - 'Biometrics are not available on this device.'; - const String errorDetails = 'com.apple.LocalAuthentication'; - when(api.authenticate(any, any)).thenAnswer( - (_) async => AuthResultDetails( - result: AuthResult.errorBiometricNotAvailable, - errorMessage: errorMessage, - errorDetails: errorDetails, - ), - ); + test( + 'converts biometryDisconnected to LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryDisconnected, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); - expect( - () async => plugin.authenticate( - localizedReason: 'reason', - authMessages: [], - ), - throwsA( - isA() - // The code here should match what you defined in your Dart switch statement. - .having( - (PlatformException e) => e.code, - 'code', - 'BiometricNotAvailable', - ) - .having( - (PlatformException e) => e.message, - 'message', - errorMessage, - ) - .having( - (PlatformException e) => e.details, - 'details', - errorDetails, - ), - ), - ); - }); + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode + .biometricHardwareTemporarilyUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts biometryLockout to LocalAuthExceptionCode.biometricLockout', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryLockout, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.biometricLockout, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts biometryNotAvailable to LocalAuthExceptionCode.noBiometricHardware', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryNotAvailable, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricHardware, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts biometryNotPaired to LocalAuthExceptionCode.noBiometricHardware', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryNotPaired, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricHardware, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts biometryNotEnrolled to LocalAuthExceptionCode.noBiometricsEnrolled', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.biometryNotEnrolled, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricsEnrolled, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts invalidContext to LocalAuthExceptionCode.uiUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.invalidContext, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts invalidDimensions to LocalAuthExceptionCode.uiUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.invalidDimensions, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts notInteractive to LocalAuthExceptionCode.uiUnavailable', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.notInteractive, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.uiUnavailable, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts passcodeNotSet to LocalAuthExceptionCode.noCredentialsSet', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.passcodeNotSet, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noCredentialsSet, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); + + test( + 'converts userFallback to LocalAuthExceptionCode.userRequestedFallback', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails( + result: AuthResult.userFallback, + errorMessage: errorMessage, + errorDetails: errorDetails, + ), + ); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.userRequestedFallback, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + errorMessage, + ) + .having( + (LocalAuthException e) => e.details, + 'details', + errorDetails, + ), + ), + ); + }, + ); test( - 'converts errorPasscodeNotSet to legacy PlatformException', + 'converts unknownError to LocalAuthExceptionCode.unknownError', () async { const String errorMessage = 'a message'; const String errorDetails = 'some details'; when(api.authenticate(any, any)).thenAnswer( (_) async => AuthResultDetails( - result: AuthResult.errorPasscodeNotSet, + result: AuthResult.unknownError, errorMessage: errorMessage, errorDetails: errorDetails, ), @@ -579,19 +908,19 @@ void main() { authMessages: [], ), throwsA( - isA() + isA() .having( - (PlatformException e) => e.code, + (LocalAuthException e) => e.code, 'code', - 'PasscodeNotSet', + LocalAuthExceptionCode.unknownError, ) .having( - (PlatformException e) => e.message, - 'message', + (LocalAuthException e) => e.description, + 'description', errorMessage, ) .having( - (PlatformException e) => e.details, + (LocalAuthException e) => e.details, 'details', errorDetails, ), diff --git a/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.mocks.dart b/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.mocks.dart index ffa5c074a44..1c93c29927e 100644 --- a/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.mocks.dart +++ b/packages/local_auth/local_auth_darwin/test/local_auth_darwin_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in local_auth_darwin/test/local_auth_darwin_test.dart. // Do not manually edit this file. @@ -17,6 +17,7 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types diff --git a/packages/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md index cab0a630bc7..fff29ae14f2 100644 --- a/packages/local_auth/local_auth_windows/CHANGELOG.md +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.0 +* Switches to `LocalAuthException` for error reporting. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 1.0.11 diff --git a/packages/local_auth/local_auth_windows/example/lib/main.dart b/packages/local_auth/local_auth_windows/example/lib/main.dart index 95d4316e11b..c64205461ef 100644 --- a/packages/local_auth/local_auth_windows/example/lib/main.dart +++ b/packages/local_auth/local_auth_windows/example/lib/main.dart @@ -94,11 +94,18 @@ class _MyAppState extends State { setState(() { _isAuthenticating = false; }); + } on LocalAuthException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - $e'; + }); + return; } on PlatformException catch (e) { print(e); setState(() { _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; + _authorized = 'Unexpected error - ${e.message}'; }); return; } @@ -111,11 +118,6 @@ class _MyAppState extends State { ); } - Future _cancelAuthentication() async { - await LocalAuthPlatform.instance.stopAuthentication(); - setState(() => _isAuthenticating = false); - } - @override Widget build(BuildContext context) { return MaterialApp( @@ -149,18 +151,7 @@ class _MyAppState extends State { ), const Divider(height: 100), Text('Current State: $_authorized\n'), - if (_isAuthenticating) - ElevatedButton( - onPressed: _cancelAuthentication, - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Cancel Authentication'), - Icon(Icons.cancel), - ], - ), - ) - else + if (!_isAuthenticating) Column( children: [ ElevatedButton( diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml index bef084fb595..1cc184db6a3 100644 --- a/packages/local_auth/local_auth_windows/example/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - local_auth_platform_interface: ^1.0.0 + local_auth_platform_interface: ^1.1.0 local_auth_windows: # When depending on this package from a real application you should use: # local_auth_windows: ^x.y.z diff --git a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart index ebeff69ac59..127e00a7822 100644 --- a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart +++ b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart @@ -39,7 +39,34 @@ class LocalAuthWindows extends LocalAuthPlatform { ); } - return _api.authenticate(localizedReason); + return switch (await _api.authenticate(localizedReason)) { + AuthResult.success => true, + AuthResult.failure => false, + AuthResult.noHardware => + throw const LocalAuthException( + code: LocalAuthExceptionCode.noBiometricHardware, + ), + AuthResult.notEnrolled => + throw const LocalAuthException( + code: LocalAuthExceptionCode.noBiometricsEnrolled, + ), + AuthResult.deviceBusy => + throw const LocalAuthException( + code: LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable, + ), + AuthResult.disabledByPolicy => + // This error is niche enough that it doesn't warrant a specific + // mapping, so just use unknownError with a description. + throw const LocalAuthException( + code: LocalAuthExceptionCode.unknownError, + description: 'Group policy has disabled the authentication device.', + ), + AuthResult.unavailable => + throw const LocalAuthException( + code: LocalAuthExceptionCode.unknownError, + description: 'Authentication failed with an unsupported result code.', + ), + }; } @override diff --git a/packages/local_auth/local_auth_windows/lib/src/messages.g.dart b/packages/local_auth/local_auth_windows/lib/src/messages.g.dart index d0ed46f0fcc..3d31a065e46 100644 --- a/packages/local_auth/local_auth_windows/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_windows/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v21.0.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -18,8 +18,55 @@ PlatformException _createConnectionError(String channelName) { ); } +/// Possible outcomes of an authentication attempt. +enum AuthResult { + /// The user authenticated successfully. + success, + + /// The user failed to successfully authenticate. + failure, + + /// No biometric hardware is available. + noHardware, + + /// No biometrics are enrolled. + notEnrolled, + + /// The biometric hardware is currently in use. + deviceBusy, + + /// Device policy does not allow using the authentication system. + disabledByPolicy, + + /// Authentication is unavailable for an unknown reason. + unavailable, +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is AuthResult) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : AuthResult.values[value]; + default: + return super.readValueOfType(type, buffer); + } + } } class LocalAuthApi { @@ -29,77 +76,77 @@ class LocalAuthApi { LocalAuthApi({ BinaryMessenger? binaryMessenger, String messageChannelSuffix = '', - }) : __pigeon_binaryMessenger = binaryMessenger, - __pigeon_messageChannelSuffix = + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? __pigeon_binaryMessenger; + final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - final String __pigeon_messageChannelSuffix; + final String pigeonVar_messageChannelSuffix; /// Returns true if this device supports authentication. Future isDeviceSupported() async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.local_auth_windows.LocalAuthApi.isDeviceSupported$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = + final String pigeonVar_channelName = + 'dev.flutter.pigeon.local_auth_windows.LocalAuthApi.isDeviceSupported$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send(null) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as bool?)!; } } /// Attempts to authenticate the user with the provided [localizedReason] as /// the user-facing explanation for the authorization request. - /// - /// Returns true if authorization succeeds, false if it is attempted but is - /// not successful, and an error if authorization could not be attempted. - Future authenticate(String localizedReason) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.local_auth_windows.LocalAuthApi.authenticate$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = + Future authenticate(String localizedReason) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.local_auth_windows.LocalAuthApi.authenticate$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send([localizedReason]) - as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [localizedReason], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as AuthResult?)!; } } } diff --git a/packages/local_auth/local_auth_windows/pigeons/messages.dart b/packages/local_auth/local_auth_windows/pigeons/messages.dart index bb95ba80a71..07ed4139edc 100644 --- a/packages/local_auth/local_auth_windows/pigeons/messages.dart +++ b/packages/local_auth/local_auth_windows/pigeons/messages.dart @@ -13,6 +13,30 @@ import 'package:pigeon/pigeon.dart'; copyrightHeader: 'pigeons/copyright.txt', ), ) +/// Possible outcomes of an authentication attempt. +enum AuthResult { + /// The user authenticated successfully. + success, + + /// The user failed to successfully authenticate. + failure, + + /// No biometric hardware is available. + noHardware, + + /// No biometrics are enrolled. + notEnrolled, + + /// The biometric hardware is currently in use. + deviceBusy, + + /// Device policy does not allow using the authentication system. + disabledByPolicy, + + /// Authentication is unavailable for an unknown reason. + unavailable, +} + @HostApi() abstract class LocalAuthApi { /// Returns true if this device supports authentication. @@ -21,9 +45,6 @@ abstract class LocalAuthApi { /// Attempts to authenticate the user with the provided [localizedReason] as /// the user-facing explanation for the authorization request. - /// - /// Returns true if authorization succeeds, false if it is attempted but is - /// not successful, and an error if authorization could not be attempted. @async - bool authenticate(String localizedReason); + AuthResult authenticate(String localizedReason); } diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index 6401846b5b0..7dedea02fe2 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_windows description: Windows implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.11 +version: 2.0.0 environment: sdk: ^3.7.0 @@ -19,12 +19,12 @@ flutter: dependencies: flutter: sdk: flutter - local_auth_platform_interface: ^1.0.1 + local_auth_platform_interface: ^1.1.0 dev_dependencies: flutter_test: sdk: flutter - pigeon: ^21.0.0 + pigeon: ^26.0.1 topics: - authentication diff --git a/packages/local_auth/local_auth_windows/test/local_auth_test.dart b/packages/local_auth/local_auth_windows/test/local_auth_test.dart index 82369886234..106310591ab 100644 --- a/packages/local_auth/local_auth_windows/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_windows/test/local_auth_test.dart @@ -2,7 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/src/services/binary_messenger.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; import 'package:local_auth_windows/local_auth_windows.dart'; import 'package:local_auth_windows/src/messages.g.dart'; @@ -17,7 +19,7 @@ void main() { }); test('authenticate handles success', () async { - api.returnValue = true; + api.authReturnValue = AuthResult.success; final bool result = await plugin.authenticate( authMessages: [const WindowsAuthMessages()], @@ -29,7 +31,7 @@ void main() { }); test('authenticate handles failure', () async { - api.returnValue = false; + api.authReturnValue = AuthResult.failure; final bool result = await plugin.authenticate( authMessages: [const WindowsAuthMessages()], @@ -40,6 +42,104 @@ void main() { expect(api.passedReason, 'My localized reason'); }); + test('authenticate handles no hardware', () async { + api.authReturnValue = AuthResult.noHardware; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricHardware, + ), + ), + ); + }); + + test('authenticate handles not enrolled', () async { + api.authReturnValue = AuthResult.notEnrolled; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.noBiometricsEnrolled, + ), + ), + ); + }); + + test('authenticate handles busy', () async { + api.authReturnValue = AuthResult.deviceBusy; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.biometricHardwareTemporarilyUnavailable, + ), + ), + ); + }); + + test('authenticate handles disabled by policy', () async { + api.authReturnValue = AuthResult.disabledByPolicy; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA() + .having( + (LocalAuthException e) => e.code, + 'code', + // Currently there is no specific error code for this case; it can + // be added if there is user demand for it. + LocalAuthExceptionCode.unknownError, + ) + .having( + (LocalAuthException e) => e.description, + 'description', + contains('Group policy has disabled the authentication device'), + ), + ), + ); + }); + + test('authenticate handles generic error', () async { + api.authReturnValue = AuthResult.unavailable; + + await expectLater( + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + ), + throwsA( + isA().having( + (LocalAuthException e) => e.code, + 'code', + LocalAuthExceptionCode.unknownError, + ), + ), + ); + }); + test('authenticate throws for biometricOnly', () async { expect( plugin.authenticate( @@ -52,7 +152,7 @@ void main() { }); test('isDeviceSupported handles supported', () async { - api.returnValue = true; + api.supportedReturnValue = true; final bool result = await plugin.isDeviceSupported(); @@ -60,7 +160,7 @@ void main() { }); test('isDeviceSupported handles unsupported', () async { - api.returnValue = false; + api.supportedReturnValue = false; final bool result = await plugin.isDeviceSupported(); @@ -68,7 +168,7 @@ void main() { }); test('deviceSupportsBiometrics handles supported', () async { - api.returnValue = true; + api.supportedReturnValue = true; final bool result = await plugin.deviceSupportsBiometrics(); @@ -76,7 +176,7 @@ void main() { }); test('deviceSupportsBiometrics handles unsupported', () async { - api.returnValue = false; + api.supportedReturnValue = false; final bool result = await plugin.deviceSupportsBiometrics(); @@ -86,7 +186,7 @@ void main() { test( 'getEnrolledBiometrics returns expected values when supported', () async { - api.returnValue = true; + api.supportedReturnValue = true; final List result = await plugin.getEnrolledBiometrics(); @@ -98,7 +198,7 @@ void main() { ); test('getEnrolledBiometrics returns nothing when unsupported', () async { - api.returnValue = false; + api.supportedReturnValue = false; final List result = await plugin.getEnrolledBiometrics(); @@ -114,20 +214,31 @@ void main() { } class _FakeLocalAuthApi implements LocalAuthApi { - /// The return value for [isDeviceSupported] and [authenticate]. - bool returnValue = false; + /// The return value for [authenticate]. + AuthResult authReturnValue = AuthResult.success; + + /// The return value for [isDeviceSupported]. + bool supportedReturnValue = false; /// The argument that was passed to [authenticate]. String? passedReason; @override - Future authenticate(String localizedReason) async { + Future authenticate(String localizedReason) async { passedReason = localizedReason; - return returnValue; + return authReturnValue; } @override Future isDeviceSupported() async { - return returnValue; + return supportedReturnValue; } + + @override + // ignore: non_constant_identifier_names + BinaryMessenger? get pigeonVar_binaryMessenger => null; + + @override + // ignore: non_constant_identifier_names + String get pigeonVar_messageChannelSuffix => ''; } diff --git a/packages/local_auth/local_auth_windows/windows/local_auth.h b/packages/local_auth/local_auth_windows/windows/local_auth.h index b0b52372fe8..6e7fc73097e 100644 --- a/packages/local_auth/local_auth_windows/windows/local_auth.h +++ b/packages/local_auth/local_auth_windows/windows/local_auth.h @@ -67,8 +67,9 @@ class LocalAuthPlugin : public flutter::Plugin, public LocalAuthApi { // LocalAuthApi: void IsDeviceSupported( std::function reply)> result) override; - void Authenticate(const std::string& localized_reason, - std::function reply)> result) override; + void Authenticate( + const std::string& localized_reason, + std::function reply)> result) override; private: std::unique_ptr user_consent_verifier_; @@ -76,7 +77,7 @@ class LocalAuthPlugin : public flutter::Plugin, public LocalAuthApi { // Starts authentication process. winrt::fire_and_forget AuthenticateCoroutine( const std::string& localized_reason, - std::function reply)> result); + std::function reply)> result); // Returns whether the system supports Windows Hello. winrt::fire_and_forget IsDeviceSupportedCoroutine( diff --git a/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp b/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp index 7f5939987d0..bc9362deeba 100644 --- a/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp +++ b/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp @@ -120,14 +120,14 @@ void LocalAuthPlugin::IsDeviceSupported( void LocalAuthPlugin::Authenticate( const std::string& localized_reason, - std::function reply)> result) { + std::function reply)> result) { AuthenticateCoroutine(localized_reason, std::move(result)); } // Starts authentication process. winrt::fire_and_forget LocalAuthPlugin::AuthenticateCoroutine( const std::string& localized_reason, - std::function reply)> result) { + std::function reply)> result) { std::wstring reason = Utf16FromUtf8(localized_reason); winrt::Windows::Security::Credentials::UI::UserConsentVerifierAvailability @@ -137,19 +137,27 @@ winrt::fire_and_forget LocalAuthPlugin::AuthenticateCoroutine( if (ucv_availability == winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::DeviceNotPresent) { - result(FlutterError("NoHardware", "No biometric hardware found")); + result(AuthResult::kNoHardware); co_return; } else if (ucv_availability == winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::NotConfiguredForUser) { - result( - FlutterError("NotEnrolled", "No biometrics enrolled on this device.")); + result(AuthResult::kNotEnrolled); + co_return; + } else if (ucv_availability == + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DeviceBusy) { + result(AuthResult::kDeviceBusy); + co_return; + } else if (ucv_availability == + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DisabledByPolicy) { + result(AuthResult::kDisabledByPolicy); co_return; } else if (ucv_availability != winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::Available) { - result( - FlutterError("NotAvailable", "Required security features not enabled")); + result(AuthResult::kUnavailable); co_return; } @@ -160,9 +168,11 @@ winrt::fire_and_forget LocalAuthPlugin::AuthenticateCoroutine( reason); result(consent_result == winrt::Windows::Security::Credentials::UI:: - UserConsentVerificationResult::Verified); + UserConsentVerificationResult::Verified + ? AuthResult::kSuccess + : AuthResult::kFailure); } catch (...) { - result(false); + result(AuthResult::kFailure); } } diff --git a/packages/local_auth/local_auth_windows/windows/messages.g.cpp b/packages/local_auth/local_auth_windows/windows/messages.g.cpp index 972668446e3..f8a367c6d54 100644 --- a/packages/local_auth/local_auth_windows/windows/messages.g.cpp +++ b/packages/local_auth/local_auth_windows/windows/messages.g.cpp @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v21.0.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #undef _HAS_EXCEPTIONS @@ -31,22 +31,44 @@ FlutterError CreateConnectionError(const std::string channel_name) { EncodableValue("")); } -PigeonCodecSerializer::PigeonCodecSerializer() {} +PigeonInternalCodecSerializer::PigeonInternalCodecSerializer() {} -EncodableValue PigeonCodecSerializer::ReadValueOfType( +EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( uint8_t type, flutter::ByteStreamReader* stream) const { - return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + switch (type) { + case 129: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = + encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() + ? EncodableValue() + : CustomEncodableValue( + static_cast(enum_arg_value)); + } + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } } -void PigeonCodecSerializer::WriteValue( +void PigeonInternalCodecSerializer::WriteValue( const EncodableValue& value, flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = + std::get_if(&value)) { + if (custom_value->type() == typeid(AuthResult)) { + stream->WriteByte(129); + WriteValue(EncodableValue(static_cast( + std::any_cast(*custom_value))), + stream); + return; + } + } flutter::StandardCodecSerializer::WriteValue(value, stream); } /// The codec used by LocalAuthApi. const flutter::StandardMessageCodec& LocalAuthApi::GetCodec() { return flutter::StandardMessageCodec::GetInstance( - &PigeonCodecSerializer::GetInstance()); + &PigeonInternalCodecSerializer::GetInstance()); } // Sets up an instance of `LocalAuthApi` to handle messages through the @@ -112,14 +134,14 @@ void LocalAuthApi::SetUp(flutter::BinaryMessenger* binary_messenger, const auto& localized_reason_arg = std::get(encodable_localized_reason_arg); api->Authenticate( - localized_reason_arg, [reply](ErrorOr&& output) { + localized_reason_arg, [reply](ErrorOr&& output) { if (output.has_error()) { reply(WrapError(output.error())); return; } EncodableList wrapped; wrapped.push_back( - EncodableValue(std::move(output).TakeValue())); + CustomEncodableValue(std::move(output).TakeValue())); reply(EncodableValue(std::move(wrapped))); }); } catch (const std::exception& exception) { diff --git a/packages/local_auth/local_auth_windows/windows/messages.g.h b/packages/local_auth/local_auth_windows/windows/messages.g.h index cb8a090b13a..304c2ea44f0 100644 --- a/packages/local_auth/local_auth_windows/windows/messages.g.h +++ b/packages/local_auth/local_auth_windows/windows/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v21.0.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #ifndef PIGEON_MESSAGES_G_H_ @@ -58,11 +58,29 @@ class ErrorOr { std::variant v_; }; -class PigeonCodecSerializer : public flutter::StandardCodecSerializer { +// Possible outcomes of an authentication attempt. +enum class AuthResult { + // The user authenticated successfully. + kSuccess = 0, + // The user failed to successfully authenticate. + kFailure = 1, + // No biometric hardware is available. + kNoHardware = 2, + // No biometrics are enrolled. + kNotEnrolled = 3, + // The biometric hardware is currently in use. + kDeviceBusy = 4, + // Device policy does not allow using the authentication system. + kDisabledByPolicy = 5, + // Authentication is unavailable for an unknown reason. + kUnavailable = 6 +}; + +class PigeonInternalCodecSerializer : public flutter::StandardCodecSerializer { public: - PigeonCodecSerializer(); - inline static PigeonCodecSerializer& GetInstance() { - static PigeonCodecSerializer sInstance; + PigeonInternalCodecSerializer(); + inline static PigeonInternalCodecSerializer& GetInstance() { + static PigeonInternalCodecSerializer sInstance; return sInstance; } @@ -86,12 +104,9 @@ class LocalAuthApi { std::function reply)> result) = 0; // Attempts to authenticate the user with the provided [localizedReason] as // the user-facing explanation for the authorization request. - // - // Returns true if authorization succeeds, false if it is attempted but is - // not successful, and an error if authorization could not be attempted. virtual void Authenticate( const std::string& localized_reason, - std::function reply)> result) = 0; + std::function reply)> result) = 0; // The codec used by LocalAuthApi. static const flutter::StandardMessageCodec& GetCodec(); diff --git a/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp b/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp index a6f21818699..b66573c559a 100644 --- a/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp +++ b/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp @@ -94,12 +94,12 @@ TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenAuthorized) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - ErrorOr result(false); + ErrorOr result(AuthResult::kUnavailable); plugin.Authenticate("My Reason", - [&result](ErrorOr reply) { result = reply; }); + [&result](ErrorOr reply) { result = reply; }); EXPECT_FALSE(result.has_error()); - EXPECT_TRUE(result.value()); + EXPECT_EQ(result.value(), AuthResult::kSuccess); } TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenNotAuthorized) { @@ -127,12 +127,78 @@ TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenNotAuthorized) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - ErrorOr result(true); + ErrorOr result(AuthResult::kUnavailable); plugin.Authenticate("My Reason", - [&result](ErrorOr reply) { result = reply; }); + [&result](ErrorOr reply) { result = reply; }); EXPECT_FALSE(result.has_error()); - EXPECT_FALSE(result.value()); + EXPECT_EQ(result.value(), AuthResult::kFailure); +} + +TEST(LocalAuthPlugin, AuthenticateHandlerReportsNoHardware) { + std::unique_ptr mockConsentVerifier = + std::make_unique(); + + EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) + .Times(1) + .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability> { + co_return winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DeviceNotPresent; + }); + + LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(AuthResult::kUnavailable); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); + + EXPECT_FALSE(result.has_error()); + EXPECT_EQ(result.value(), AuthResult::kNoHardware); +} + +TEST(LocalAuthPlugin, AuthenticateHandlerReportsBusy) { + std::unique_ptr mockConsentVerifier = + std::make_unique(); + + EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) + .Times(1) + .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability> { + co_return winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DeviceBusy; + }); + + LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(AuthResult::kUnavailable); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); + + EXPECT_FALSE(result.has_error()); + EXPECT_EQ(result.value(), AuthResult::kDeviceBusy); +} + +TEST(LocalAuthPlugin, AuthenticateHandlerReportsDisabledByPolicy) { + std::unique_ptr mockConsentVerifier = + std::make_unique(); + + EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) + .Times(1) + .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability> { + co_return winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DisabledByPolicy; + }); + + LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(AuthResult::kUnavailable); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); + + EXPECT_FALSE(result.has_error()); + EXPECT_EQ(result.value(), AuthResult::kDisabledByPolicy); } } // namespace test