diff --git a/packages/integration_test/android/gradle.properties b/packages/integration_test/android/gradle.properties
index 2bd6f4fda009..8bd86f680510 100644
--- a/packages/integration_test/android/gradle.properties
+++ b/packages/integration_test/android/gradle.properties
@@ -1,2 +1 @@
org.gradle.jvmargs=-Xmx1536M
-
diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md
index b27ec83d41a7..8bb043f52d8f 100644
--- a/packages/local_auth/CHANGELOG.md
+++ b/packages/local_auth/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 1.1.0-nullsafety
+
+* Allow pin, passcode, and pattern authentication with `authenticate` method
+* **Breaking change**. Parameter names refactored to use the generic `biometric` prefix in place of `fingerprint` in the `AndroidAuthMessages` class
+ * `fingerprintHint` is now `biometricHint`
+ * `fingerprintNotRecognized`is now `biometricNotRecognized`
+ * `fingerprintSuccess`is now `biometricSuccess`
+ * `fingerprintRequiredTitle` is now `biometricRequiredTitle`
+
## 1.0.0-nullsafety.3
* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets.
diff --git a/packages/local_auth/README.md b/packages/local_auth/README.md
index 516561be4230..80820d759fec 100644
--- a/packages/local_auth/README.md
+++ b/packages/local_auth/README.md
@@ -23,8 +23,8 @@ bool canCheckBiometrics =
Currently the following biometric types are implemented:
-* BiometricType.face
-* BiometricType.fingerprint
+- BiometricType.face
+- BiometricType.fingerprint
To get a list of enrolled biometrics, call getAvailableBiometrics:
@@ -44,10 +44,10 @@ if (Platform.isIOS) {
We have default dialogs with an 'OK' button to show authentication error
messages for the following 2 cases:
-1. Passcode/PIN/Pattern Not Set. The user has not yet configured a passcode on
- iOS or PIN/pattern on Android.
-2. Touch ID/Fingerprint Not Enrolled. The user has not enrolled any
- fingerprints on the device.
+1. Passcode/PIN/Pattern Not Set. The user has not yet configured a passcode on
+ iOS or PIN/pattern on Android.
+2. Touch ID/Fingerprint Not Enrolled. The user has not enrolled any
+ fingerprints on the device.
Which means, if there's no fingerprint on the user's device, a dialog with
instructions will pop up to let the user set up fingerprint. If the user clicks
@@ -55,20 +55,33 @@ instructions will pop up to let the user set up fingerprint. If the user clicks
Use the exported APIs to trigger local authentication with default dialogs:
+The `authenticate()` method uses biometric authentication, but also allows
+users to use pin, pattern, or passcode.
+
```dart
var localAuth = LocalAuthentication();
bool didAuthenticate =
- await localAuth.authenticateWithBiometrics(
+ await localAuth.authenticate(
localizedReason: 'Please authenticate to show account balance');
```
+To authenticate using biometric authentication only, set `biometricOnly` to `true`.
+
+```dart
+var localAuth = LocalAuthentication();
+bool didAuthenticate =
+ await localAuth.authenticate(
+ localizedReason: 'Please authenticate to show account balance',
+ biometricOnly: true);
+```
+
If you don't want to use the default dialogs, call this API with
'useErrorDialogs = false'. In this case, it will throw the error message back
and you need to handle them in your dart code:
```dart
bool didAuthenticate =
- await localAuth.authenticateWithBiometrics(
+ await localAuth.authenticate(
localizedReason: 'Please authenticate to show account balance',
useErrorDialogs: false);
```
@@ -84,7 +97,7 @@ const iosStrings = const IOSAuthMessages(
goToSettingsButton: 'settings',
goToSettingsDescription: 'Please set up your Touch ID.',
lockOut: 'Please reenable your Touch ID');
-await localAuth.authenticateWithBiometrics(
+await localAuth.authenticate(
localizedReason: 'Please authenticate to show account balance',
useErrorDialogs: false,
iOSAuthStrings: iosStrings);
@@ -112,7 +125,7 @@ import 'package:flutter/services.dart';
import 'package:local_auth/error_codes.dart' as auth_error;
try {
- bool didAuthenticate = await local_auth.authenticateWithBiometrics(
+ bool didAuthenticate = await local_auth.authenticate(
localizedReason: 'Please authenticate to show account balance');
} on PlatformException catch (e) {
if (e.code == auth_error.notAvailable) {
@@ -134,7 +147,6 @@ you need to also add:
to your Info.plist file. Failure to do so results in a dialog that tells the user your
app has not been updated to use TouchID.
-
## Android Integration
Note that local_auth plugin requires the use of a FragmentActivity as
@@ -191,7 +203,7 @@ Update your project's `AndroidManifest.xml` file to include the
On Android, you can check only for existence of fingerprint hardware prior
to API 29 (Android Q). Therefore, if you would like to support other biometrics
types (such as face scanning) and you want to support SDKs lower than Q,
-*do not* call `getAvailableBiometrics`. Simply call `authenticateWithBiometrics`.
+_do not_ call `getAvailableBiometrics`. Simply call `authenticate` with `biometricOnly: true`.
This will return an error if there was no hardware available.
## Sticky Auth
diff --git a/packages/local_auth/android/build.gradle b/packages/local_auth/android/build.gradle
index 0fc603c36867..ae8e6f2828a2 100644
--- a/packages/local_auth/android/build.gradle
+++ b/packages/local_auth/android/build.gradle
@@ -8,7 +8,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.3.0'
+ classpath 'com.android.tools.build:gradle:4.1.1'
}
}
diff --git a/packages/local_auth/android/src/main/AndroidManifest.xml b/packages/local_auth/android/src/main/AndroidManifest.xml
index b7da0caab6da..cb6cb985a986 100644
--- a/packages/local_auth/android/src/main/AndroidManifest.xml
+++ b/packages/local_auth/android/src/main/AndroidManifest.xml
@@ -1,3 +1,5 @@
+
+
diff --git a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java
index e907cc43f2b4..096c7efd6d3d 100644
--- a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java
+++ b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java
@@ -29,7 +29,7 @@
import java.util.concurrent.Executor;
/**
- * Authenticates the user with fingerprint and sends corresponding response back to Flutter.
+ * Authenticates the user with biometrics and sends corresponding response back to Flutter.
*
*
One instance per call is generated to ensure readable separation of executable paths across
* method calls.
@@ -37,10 +37,8 @@
@SuppressWarnings("deprecation")
class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback
implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver {
-
/** The callback that handles the result of this authentication process. */
interface AuthCompletionHandler {
-
/** Called when authentication was successful. */
void onSuccess();
@@ -75,24 +73,32 @@ interface AuthCompletionHandler {
Lifecycle lifecycle,
FragmentActivity activity,
MethodCall call,
- AuthCompletionHandler completionHandler) {
+ AuthCompletionHandler completionHandler,
+ boolean allowCredentials) {
this.lifecycle = lifecycle;
this.activity = activity;
this.completionHandler = completionHandler;
this.call = call;
this.isAuthSticky = call.argument("stickyAuth");
this.uiThreadExecutor = new UiThreadExecutor();
- this.promptInfo =
+
+ BiometricPrompt.PromptInfo.Builder promptBuilder =
new BiometricPrompt.PromptInfo.Builder()
.setDescription((String) call.argument("localizedReason"))
.setTitle((String) call.argument("signInTitle"))
- .setSubtitle((String) call.argument("fingerprintHint"))
- .setNegativeButtonText((String) call.argument("cancelButton"))
+ .setSubtitle((String) call.argument("biometricHint"))
.setConfirmationRequired((Boolean) call.argument("sensitiveTransaction"))
- .build();
+ .setConfirmationRequired((Boolean) call.argument("sensitiveTransaction"));
+
+ if (allowCredentials) {
+ promptBuilder.setDeviceCredentialAllowed(true);
+ } else {
+ promptBuilder.setNegativeButtonText((String) call.argument("cancelButton"));
+ }
+ this.promptInfo = promptBuilder.build();
}
- /** Start the fingerprint listener. */
+ /** Start the biometric listener. */
void authenticate() {
if (lifecycle != null) {
lifecycle.addObserver(this);
@@ -103,7 +109,7 @@ void authenticate() {
biometricPrompt.authenticate(promptInfo);
}
- /** Cancels the fingerprint authentication. */
+ /** Cancels the biometric authentication. */
void stopAuthentication() {
if (biometricPrompt != null) {
biometricPrompt.cancelAuthentication();
@@ -111,7 +117,7 @@ void stopAuthentication() {
}
}
- /** Stops the fingerprint listener. */
+ /** Stops the biometric listener. */
private void stop() {
if (lifecycle != null) {
lifecycle.removeObserver(this);
@@ -125,21 +131,27 @@ private void stop() {
public void onAuthenticationError(int errorCode, CharSequence errString) {
switch (errorCode) {
case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL:
- completionHandler.onError(
- "PasscodeNotSet",
- "Phone not secured by PIN, pattern or password, or SIM is currently locked.");
- break;
+ if (call.argument("useErrorDialogs")) {
+ showGoToSettingsDialog(
+ (String) call.argument("deviceCredentialsRequired"),
+ (String) call.argument("deviceCredentialsSetupDescription"));
+ return;
+ }
+ completionHandler.onError("NotAvailable", "Security credentials not available.");
case BiometricPrompt.ERROR_NO_SPACE:
case BiometricPrompt.ERROR_NO_BIOMETRICS:
+ if (promptInfo.isDeviceCredentialAllowed()) return;
if (call.argument("useErrorDialogs")) {
- showGoToSettingsDialog();
+ showGoToSettingsDialog(
+ (String) call.argument("biometricRequired"),
+ (String) call.argument("goToSettingDescription"));
return;
}
completionHandler.onError("NotEnrolled", "No Biometrics enrolled on this device.");
break;
case BiometricPrompt.ERROR_HW_UNAVAILABLE:
case BiometricPrompt.ERROR_HW_NOT_PRESENT:
- completionHandler.onError("NotAvailable", "Biometrics is not available on this device.");
+ completionHandler.onError("NotAvailable", "Security credentials not available.");
break;
case BiometricPrompt.ERROR_LOCKOUT:
completionHandler.onError(
@@ -176,7 +188,7 @@ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult resul
public void onAuthenticationFailed() {}
/**
- * If the activity is paused, we keep track because fingerprint dialog simply returns "User
+ * If the activity is paused, we keep track because biometric dialog simply returns "User
* cancelled" when the activity is paused.
*/
@Override
@@ -215,12 +227,12 @@ public void onResume(@NonNull LifecycleOwner owner) {
// Suppress inflateParams lint because dialogs do not need to attach to a parent view.
@SuppressLint("InflateParams")
- private void showGoToSettingsDialog() {
+ private void showGoToSettingsDialog(String title, String descriptionText) {
View view = LayoutInflater.from(activity).inflate(R.layout.go_to_setting, null, false);
TextView message = (TextView) view.findViewById(R.id.fingerprint_required);
TextView description = (TextView) view.findViewById(R.id.go_to_setting_description);
- message.setText((String) call.argument("fingerprintRequired"));
- description.setText((String) call.argument("goToSettingDescription"));
+ message.setText(title);
+ description.setText(descriptionText);
Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom);
OnClickListener goToSettingHandler =
new OnClickListener() {
diff --git a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java
index 0f93f40e947a..f4c6c168f54d 100644
--- a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java
+++ b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java
@@ -4,9 +4,18 @@
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.content.pm.PackageManager;
+import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.biometric.BiometricManager;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.Lifecycle;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
@@ -17,6 +26,8 @@
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
+import io.flutter.plugin.common.PluginRegistry;
+import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -29,14 +40,33 @@
@SuppressWarnings("deprecation")
public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware {
private static final String CHANNEL_NAME = "plugins.flutter.io/local_auth";
-
+ private static final int LOCK_REQUEST_CODE = 221;
private Activity activity;
private final AtomicBoolean authInProgress = new AtomicBoolean(false);
- private AuthenticationHelper authenticationHelper;
+ private AuthenticationHelper authHelper;
// These are null when not using v2 embedding.
private MethodChannel channel;
private Lifecycle lifecycle;
+ private BiometricManager biometricManager;
+ private FingerprintManager fingerprintManager;
+ private KeyguardManager keyguardManager;
+ private 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) {
+ authenticateSuccess(lockRequestResult);
+ } else {
+ authenticateFail(lockRequestResult);
+ }
+ lockRequestResult = null;
+ }
+ return false;
+ }
+ };
/**
* Registers a plugin with the v1 embedding api {@code io.flutter.plugin.common}.
@@ -49,13 +79,12 @@ public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, Activi
* io.flutter.plugin.common.BinaryMessenger}.
*/
@SuppressWarnings("deprecation")
- public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
+ public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
- channel.setMethodCallHandler(new LocalAuthPlugin(registrar.activity()));
- }
-
- private LocalAuthPlugin(Activity activity) {
- this.activity = activity;
+ final LocalAuthPlugin plugin = new LocalAuthPlugin();
+ plugin.activity = registrar.activity();
+ channel.setMethodCallHandler(plugin);
+ registrar.addActivityResultListener(plugin.resultListener);
}
/**
@@ -66,118 +95,241 @@ private LocalAuthPlugin(Activity activity) {
public LocalAuthPlugin() {}
@Override
- public void onMethodCall(MethodCall call, final Result result) {
- if (call.method.equals("authenticateWithBiometrics")) {
- if (authInProgress.get()) {
- // Apps should not invoke another authentication request while one is in progress,
- // so we classify this as an error condition. If we ever find a legitimate use case for
- // this, we can try to cancel the ongoing auth and start a new one but for now, not worth
- // the complexity.
- result.error("auth_in_progress", "Authentication in progress", null);
- return;
- }
+ public void onMethodCall(MethodCall call, @NonNull final Result result) {
+ switch (call.method) {
+ case "authenticate":
+ authenticate(call, result);
+ break;
+ case "getAvailableBiometrics":
+ getAvailableBiometrics(result);
+ break;
+ case "isDeviceSupported":
+ isDeviceSupported(result);
+ break;
+ case "stopAuthentication":
+ stopAuthentication(result);
+ break;
+ default:
+ result.notImplemented();
+ break;
+ }
+ }
- if (activity == null || activity.isFinishing()) {
- result.error("no_activity", "local_auth plugin requires a foreground activity", null);
- return;
- }
+ /*
+ * Starts authentication process
+ */
+ private void authenticate(MethodCall call, final Result result) {
+ if (authInProgress.get()) {
+ result.error("auth_in_progress", "Authentication in progress", null);
+ return;
+ }
- if (!(activity instanceof FragmentActivity)) {
- result.error(
- "no_fragment_activity",
- "local_auth plugin requires activity to be a FragmentActivity.",
- null);
- return;
- }
- authInProgress.set(true);
- authenticationHelper =
- new AuthenticationHelper(
- lifecycle,
- (FragmentActivity) activity,
- call,
- new AuthCompletionHandler() {
- @Override
- public void onSuccess() {
- if (authInProgress.compareAndSet(true, false)) {
- result.success(true);
- }
- }
-
- @Override
- public void onFailure() {
- if (authInProgress.compareAndSet(true, false)) {
- result.success(false);
- }
- }
-
- @Override
- public void onError(String code, String error) {
- if (authInProgress.compareAndSet(true, false)) {
- result.error(code, error, null);
- }
- }
- });
- authenticationHelper.authenticate();
- } else if (call.method.equals("getAvailableBiometrics")) {
- try {
- if (activity == null || activity.isFinishing()) {
- result.error("no_activity", "local_auth plugin requires a foreground activity", null);
- return;
- }
- ArrayList biometrics = new ArrayList();
- PackageManager packageManager = activity.getPackageManager();
- if (Build.VERSION.SDK_INT >= 23) {
- if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
- biometrics.add("fingerprint");
+ if (activity == null || activity.isFinishing()) {
+ result.error("no_activity", "local_auth plugin requires a foreground activity", null);
+ return;
+ }
+
+ if (!(activity instanceof FragmentActivity)) {
+ result.error(
+ "no_fragment_activity",
+ "local_auth plugin requires activity to be a FragmentActivity.",
+ null);
+ return;
+ }
+
+ if (!isDeviceSupported()) {
+ authInProgress.set(false);
+ result.error("NotAvailable", "Required security features not enabled", null);
+ return;
+ }
+
+ authInProgress.set(true);
+ AuthCompletionHandler completionHandler =
+ new AuthCompletionHandler() {
+ @Override
+ public void onSuccess() {
+ authenticateSuccess(result);
}
- }
- if (Build.VERSION.SDK_INT >= 29) {
- if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
- biometrics.add("face");
+
+ @Override
+ public void onFailure() {
+ authenticateFail(result);
}
- if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
- biometrics.add("iris");
+
+ @Override
+ public void onError(String code, String error) {
+ if (authInProgress.compareAndSet(true, false)) {
+ result.error(code, error, null);
+ }
}
+ };
+
+ // if is biometricOnly try biometric prompt - might not work
+ boolean isBiometricOnly = call.argument("biometricOnly");
+ if (isBiometricOnly) {
+ if (!canAuthenticateWithBiometrics()) {
+ if (!hasBiometricHardware()) {
+ completionHandler.onError("NoHardware", "No biometric hardware found");
}
- result.success(biometrics);
- } catch (Exception e) {
- result.error("no_biometrics_available", e.getMessage(), null);
+ completionHandler.onError("NotEnrolled", "No biometrics enrolled on this device.");
+ return;
}
- } else if (call.method.equals(("stopAuthentication"))) {
- stopAuthentication(result);
- } else {
- result.notImplemented();
+ authHelper =
+ new AuthenticationHelper(
+ lifecycle, (FragmentActivity) activity, call, completionHandler, false);
+ authHelper.authenticate();
+ return;
+ }
+
+ // API 29 and above
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ authHelper =
+ new AuthenticationHelper(
+ lifecycle, (FragmentActivity) activity, call, completionHandler, true);
+ authHelper.authenticate();
+ return;
+ }
+
+ // API 23 - 28 with fingerprint
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && fingerprintManager != null) {
+ if (fingerprintManager.hasEnrolledFingerprints()) {
+ authHelper =
+ new AuthenticationHelper(
+ lifecycle, (FragmentActivity) activity, call, completionHandler, false);
+ authHelper.authenticate();
+ return;
+ }
+ }
+
+ // API 23 or higher with device credentials
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && keyguardManager != null
+ && keyguardManager.isDeviceSecure()) {
+ String title = call.argument("signInTitle");
+ String reason = call.argument("localizedReason");
+ Intent authIntent = keyguardManager.createConfirmDeviceCredentialIntent(title, reason);
+
+ // save result for async response
+ lockRequestResult = result;
+ activity.startActivityForResult(authIntent, LOCK_REQUEST_CODE);
+ return;
+ }
+
+ // Unable to authenticate
+ result.error("NotSupported", "This device does not support required security features", null);
+ }
+
+ private void authenticateSuccess(Result result) {
+ if (authInProgress.compareAndSet(true, false)) {
+ result.success(true);
+ }
+ }
+
+ private void authenticateFail(Result result) {
+ if (authInProgress.compareAndSet(true, false)) {
+ result.success(false);
}
}
/*
- Stops the authentication if in progress.
- */
+ * Stops the authentication if in progress.
+ */
private void stopAuthentication(Result result) {
try {
- if (authenticationHelper != null && authInProgress.get()) {
- authenticationHelper.stopAuthentication();
- authenticationHelper = null;
- result.success(true);
- return;
+ if (authHelper != null && authInProgress.get()) {
+ authHelper.stopAuthentication();
+ authHelper = null;
}
- result.success(false);
+ authInProgress.set(false);
+ result.success(true);
} catch (Exception e) {
result.success(false);
}
}
+ /*
+ * Returns biometric types available on device
+ */
+ private void getAvailableBiometrics(final Result result) {
+ try {
+ if (activity == null || activity.isFinishing()) {
+ result.error("no_activity", "local_auth plugin requires a foreground activity", null);
+ return;
+ }
+ ArrayList biometrics = getAvailableBiometrics();
+ result.success(biometrics);
+ } catch (Exception e) {
+ result.error("no_biometrics_available", e.getMessage(), null);
+ }
+ }
+
+ private ArrayList getAvailableBiometrics() {
+ ArrayList biometrics = new ArrayList<>();
+ if (activity == null || activity.isFinishing()) {
+ return biometrics;
+ }
+ PackageManager packageManager = activity.getPackageManager();
+ if (Build.VERSION.SDK_INT >= 23) {
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
+ biometrics.add("fingerprint");
+ }
+ }
+ if (Build.VERSION.SDK_INT >= 29) {
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
+ biometrics.add("face");
+ }
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
+ biometrics.add("iris");
+ }
+ }
+
+ return biometrics;
+ }
+
+ private boolean isDeviceSupported() {
+ if (keyguardManager == null) return false;
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && keyguardManager.isDeviceSecure());
+ }
+
+ private boolean canAuthenticateWithBiometrics() {
+ if (biometricManager == null) return false;
+ return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS;
+ }
+
+ private boolean hasBiometricHardware() {
+ if (biometricManager == null) return false;
+ return biometricManager.canAuthenticate() != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE;
+ }
+
+ private void isDeviceSupported(Result result) {
+ result.success(isDeviceSupported());
+ }
+
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
- channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME);
+ channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), CHANNEL_NAME);
+ channel.setMethodCallHandler(this);
}
@Override
- public void onDetachedFromEngine(FlutterPluginBinding binding) {}
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
+
+ private void setServicesFromActivity(Activity activity) {
+ if (activity == null) return;
+ this.activity = activity;
+ Context context = activity.getBaseContext();
+ biometricManager = BiometricManager.from(activity);
+ keyguardManager = (KeyguardManager) context.getSystemService(KEYGUARD_SERVICE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ fingerprintManager =
+ (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
+ }
+ }
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
- activity = binding.getActivity();
+ binding.addActivityResultListener(resultListener);
+ setServicesFromActivity(binding.getActivity());
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
channel.setMethodCallHandler(this);
}
@@ -185,18 +337,17 @@ public void onAttachedToActivity(ActivityPluginBinding binding) {
@Override
public void onDetachedFromActivityForConfigChanges() {
lifecycle = null;
- activity = null;
}
@Override
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
- activity = binding.getActivity();
+ binding.addActivityResultListener(resultListener);
+ setServicesFromActivity(binding.getActivity());
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
}
@Override
public void onDetachedFromActivity() {
- activity = null;
lifecycle = null;
channel.setMethodCallHandler(null);
}
diff --git a/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties
index 9a4163a4f5ee..186b71557c50 100644
--- a/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties
+++ b/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/packages/local_auth/example/android/build.gradle b/packages/local_auth/example/android/build.gradle
index 541636cc492a..ea78cdf2c29c 100644
--- a/packages/local_auth/example/android/build.gradle
+++ b/packages/local_auth/example/android/build.gradle
@@ -5,7 +5,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.3.0'
+ classpath 'com.android.tools.build:gradle:4.1.1'
}
}
diff --git a/packages/local_auth/example/android/gradle.properties b/packages/local_auth/example/android/gradle.properties
index a6738207fd15..7fe61a74cee0 100644
--- a/packages/local_auth/example/android/gradle.properties
+++ b/packages/local_auth/example/android/gradle.properties
@@ -1,4 +1,4 @@
-org.gradle.jvmargs=-Xmx1536M
+org.gradle.jvmargs=-Xmx1024m
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true
diff --git a/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties
index 562393332f6c..cd9fe1c68282 100644
--- a/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Thu May 30 07:21:52 NPT 2019
+#Sun Jan 03 14:07:08 CST 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
diff --git a/packages/local_auth/example/android/settings_aar.gradle b/packages/local_auth/example/android/settings_aar.gradle
new file mode 100644
index 000000000000..e7b4def49cb5
--- /dev/null
+++ b/packages/local_auth/example/android/settings_aar.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/packages/local_auth/example/lib/main.dart b/packages/local_auth/example/lib/main.dart
index 0a07e2c4437d..8cdad5b7eed3 100644
--- a/packages/local_auth/example/lib/main.dart
+++ b/packages/local_auth/example/lib/main.dart
@@ -21,11 +21,22 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State {
final LocalAuthentication auth = LocalAuthentication();
- late bool _canCheckBiometrics;
- late List _availableBiometrics;
+ _SupportState _supportState = _SupportState.unknown;
+ bool? _canCheckBiometrics;
+ List? _availableBiometrics;
String _authorized = 'Not Authorized';
bool _isAuthenticating = false;
+ @override
+ void initState() {
+ super.initState();
+ auth.isDeviceSupported().then(
+ (isSupported) => setState(() => _supportState = isSupported
+ ? _SupportState.supported
+ : _SupportState.unsupported),
+ );
+ }
+
Future _checkBiometrics() async {
late bool canCheckBiometrics;
try {
@@ -63,8 +74,8 @@ class _MyAppState extends State {
_isAuthenticating = true;
_authorized = 'Authenticating';
});
- authenticated = await auth.authenticateWithBiometrics(
- localizedReason: 'Scan your fingerprint to authenticate',
+ authenticated = await auth.authenticate(
+ localizedReason: 'Let OS determine authentication method',
useErrorDialogs: true,
stickyAuth: true);
setState(() {
@@ -73,6 +84,42 @@ class _MyAppState extends State {
});
} on PlatformException catch (e) {
print(e);
+ setState(() {
+ _isAuthenticating = false;
+ _authorized = "Error - ${e.message}";
+ });
+ return;
+ }
+ if (!mounted) return;
+
+ setState(
+ () => _authorized = authenticated ? 'Authorized' : 'Not Authorized');
+ }
+
+ Future _authenticateWithBiometrics() async {
+ bool authenticated = false;
+ try {
+ setState(() {
+ _isAuthenticating = true;
+ _authorized = 'Authenticating';
+ });
+ authenticated = await auth.authenticate(
+ localizedReason:
+ 'Scan your fingerprint (or face or whatever) to authenticate',
+ useErrorDialogs: true,
+ stickyAuth: true,
+ biometricOnly: true);
+ setState(() {
+ _isAuthenticating = false;
+ _authorized = 'Authenticating';
+ });
+ } on PlatformException catch (e) {
+ print(e);
+ setState(() {
+ _isAuthenticating = false;
+ _authorized = "Error - ${e.message}";
+ });
+ return;
}
if (!mounted) return;
@@ -82,39 +129,92 @@ class _MyAppState extends State {
});
}
- void _cancelAuthentication() {
- auth.stopAuthentication();
+ void _cancelAuthentication() async {
+ await auth.stopAuthentication();
+ setState(() => _isAuthenticating = false);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
- home: Scaffold(
- appBar: AppBar(
- title: const Text('Plugin example app'),
- ),
- body: ConstrainedBox(
- constraints: const BoxConstraints.expand(),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: [
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Plugin example app'),
+ ),
+ body: ListView(
+ padding: const EdgeInsets.only(top: 30),
+ children: [
+ Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (_supportState == _SupportState.unknown)
+ CircularProgressIndicator()
+ else if (_supportState == _SupportState.supported)
+ Text("This device is supported")
+ else
+ Text("This device is not supported"),
+ Divider(height: 100),
Text('Can check biometrics: $_canCheckBiometrics\n'),
ElevatedButton(
child: const Text('Check biometrics'),
onPressed: _checkBiometrics,
),
+ Divider(height: 100),
Text('Available biometrics: $_availableBiometrics\n'),
ElevatedButton(
child: const Text('Get available biometrics'),
onPressed: _getAvailableBiometrics,
),
+ Divider(height: 100),
Text('Current State: $_authorized\n'),
- ElevatedButton(
- child: Text(_isAuthenticating ? 'Cancel' : 'Authenticate'),
- onPressed:
- _isAuthenticating ? _cancelAuthentication : _authenticate,
- )
- ])),
- ));
+ (_isAuthenticating)
+ ? ElevatedButton(
+ onPressed: _cancelAuthentication,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text("Cancel Authentication"),
+ Icon(Icons.cancel),
+ ],
+ ),
+ )
+ : Column(
+ children: [
+ ElevatedButton(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text('Authenticate'),
+ Icon(Icons.perm_device_information),
+ ],
+ ),
+ onPressed: _authenticate,
+ ),
+ ElevatedButton(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(_isAuthenticating
+ ? 'Cancel'
+ : 'Authenticate: biometrics only'),
+ Icon(Icons.fingerprint),
+ ],
+ ),
+ onPressed: _authenticateWithBiometrics,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
}
}
+
+enum _SupportState {
+ unknown,
+ supported,
+ unsupported,
+}
diff --git a/packages/local_auth/integration_test/local_auth_test.dart b/packages/local_auth/integration_test/local_auth_test.dart
index c09810032461..d6527c7601e4 100644
--- a/packages/local_auth/integration_test/local_auth_test.dart
+++ b/packages/local_auth/integration_test/local_auth_test.dart
@@ -13,6 +13,9 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('canCheckBiometrics', (WidgetTester tester) async {
- expect(LocalAuthentication().getAvailableBiometrics(), completion(isList));
+ expect(
+ LocalAuthentication().getAvailableBiometrics(),
+ completion(isList),
+ );
});
}
diff --git a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m
index aa0c217ef543..cda49a7d68c3 100644
--- a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m
+++ b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m
@@ -22,10 +22,17 @@ + (void)registerWithRegistrar:(NSObject *)registrar {
}
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
- if ([@"authenticateWithBiometrics" isEqualToString:call.method]) {
- [self authenticateWithBiometrics:call.arguments withFlutterResult:result];
+ if ([@"authenticate" isEqualToString:call.method]) {
+ bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue];
+ if (isBiometricOnly) {
+ [self authenticateWithBiometrics:call.arguments withFlutterResult:result];
+ } else {
+ [self authenticate:call.arguments withFlutterResult:result];
+ }
} else if ([@"getAvailableBiometrics" isEqualToString:call.method]) {
[self getAvailableBiometrics:result];
+ } else if ([@"isDeviceSupported" isEqualToString:call.method]) {
+ result(@YES);
} else {
result(FlutterMethodNotImplemented);
}
@@ -89,7 +96,6 @@ - (void)getAvailableBiometrics:(FlutterResult)result {
}
result(biometrics);
}
-
- (void)authenticateWithBiometrics:(NSDictionary *)arguments
withFlutterResult:(FlutterResult)result {
LAContext *context = [[LAContext alloc] init];
@@ -130,6 +136,48 @@ - (void)authenticateWithBiometrics:(NSDictionary *)arguments
}
}
+- (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result {
+ LAContext *context = [[LAContext alloc] init];
+ NSError *authError = nil;
+ _lastCallArgs = nil;
+ _lastResult = nil;
+ context.localizedFallbackTitle = @"";
+
+ if (@available(iOS 9.0, *)) {
+ if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) {
+ [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication
+ localizedReason:arguments[@"localizedReason"]
+ reply:^(BOOL success, NSError *error) {
+ if (success) {
+ result(@YES);
+ } else {
+ switch (error.code) {
+ case LAErrorPasscodeNotSet:
+ case LAErrorTouchIDNotAvailable:
+ case LAErrorTouchIDNotEnrolled:
+ case LAErrorTouchIDLockout:
+ [self handleErrors:error
+ flutterArguments:arguments
+ withFlutterResult:result];
+ return;
+ case LAErrorSystemCancel:
+ if ([arguments[@"stickyAuth"] boolValue]) {
+ self->_lastCallArgs = arguments;
+ self->_lastResult = result;
+ return;
+ }
+ }
+ result(@NO);
+ }
+ }];
+ } else {
+ [self handleErrors:authError flutterArguments:arguments withFlutterResult:result];
+ }
+ } else {
+ // Fallback on earlier versions
+ }
+}
+
- (void)handleErrors:(NSError *)authError
flutterArguments:(NSDictionary *)arguments
withFlutterResult:(FlutterResult)result {
diff --git a/packages/local_auth/lib/auth_strings.dart b/packages/local_auth/lib/auth_strings.dart
index 3afc23827d98..855098b6aba4 100644
--- a/packages/local_auth/lib/auth_strings.dart
+++ b/packages/local_auth/lib/auth_strings.dart
@@ -15,38 +15,46 @@ import 'package:intl/intl.dart';
/// Provides default values for all messages.
class AndroidAuthMessages {
const AndroidAuthMessages({
- this.fingerprintHint,
- this.fingerprintNotRecognized,
- this.fingerprintSuccess,
+ this.biometricHint,
+ this.biometricNotRecognized,
+ this.biometricRequiredTitle,
+ this.biometricSuccess,
this.cancelButton,
- this.signInTitle,
- this.fingerprintRequiredTitle,
+ this.deviceCredentialsRequiredTitle,
+ this.deviceCredentialsSetupDescription,
this.goToSettingsButton,
this.goToSettingsDescription,
+ this.signInTitle,
});
- final String? fingerprintHint;
- final String? fingerprintNotRecognized;
- final String? fingerprintSuccess;
+ final String? biometricHint;
+ final String? biometricNotRecognized;
+ final String? biometricRequiredTitle;
+ final String? biometricSuccess;
final String? cancelButton;
- final String? signInTitle;
- final String? fingerprintRequiredTitle;
+ final String? deviceCredentialsRequiredTitle;
+ final String? deviceCredentialsSetupDescription;
final String? goToSettingsButton;
final String? goToSettingsDescription;
+ final String? signInTitle;
Map get args {
return {
- 'fingerprintHint': fingerprintHint ?? androidFingerprintHint,
- 'fingerprintNotRecognized':
- fingerprintNotRecognized ?? androidFingerprintNotRecognized,
- 'fingerprintSuccess': fingerprintSuccess ?? androidFingerprintSuccess,
+ 'biometricHint': biometricHint ?? androidBiometricHint,
+ 'biometricNotRecognized':
+ biometricNotRecognized ?? androidBiometricNotRecognized,
+ 'biometricSuccess': biometricSuccess ?? androidBiometricSuccess,
+ 'biometricRequired':
+ biometricRequiredTitle ?? androidBiometricRequiredTitle,
'cancelButton': cancelButton ?? androidCancelButton,
- 'signInTitle': signInTitle ?? androidSignInTitle,
- 'fingerprintRequired':
- fingerprintRequiredTitle ?? androidFingerprintRequiredTitle,
+ 'deviceCredentialsRequired': deviceCredentialsRequiredTitle ??
+ androidDeviceCredentialsRequiredTitle,
+ 'deviceCredentialsSetupDescription': deviceCredentialsSetupDescription ??
+ androidDeviceCredentialsSetupDescription,
'goToSetting': goToSettingsButton ?? goToSettings,
'goToSettingDescription':
goToSettingsDescription ?? androidGoToSettingsDescription,
+ 'signInTitle': signInTitle ?? androidSignInTitle,
};
}
}
@@ -80,16 +88,17 @@ class IOSAuthMessages {
// Strings for local_authentication plugin. Currently supports English.
// Intl.message must be string literals.
-String get androidFingerprintHint => Intl.message('Touch sensor',
- desc: 'Hint message advising the user how to scan their fingerprint. It is '
+String get androidBiometricHint => Intl.message('Verify identity',
+ desc:
+ 'Hint message advising the user how to authenticate with biometrics. It is '
'used on Android side. Maximum 60 characters.');
-String get androidFingerprintNotRecognized =>
- Intl.message('Fingerprint not recognized. Try again.',
+String get androidBiometricNotRecognized =>
+ Intl.message('Not recognized. Try again.',
desc: 'Message to let the user know that authentication was failed. It '
'is used on Android side. Maximum 60 characters.');
-String get androidFingerprintSuccess => Intl.message('Fingerprint recognized.',
+String get androidBiometricSuccess => Intl.message('Success',
desc: 'Message to let the user know that authentication was successful. It '
'is used on Android side. Maximum 60 characters.');
@@ -97,17 +106,26 @@ String get androidCancelButton => Intl.message('Cancel',
desc: 'Message showed on a button that the user can click to leave the '
'current dialog. It is used on Android side. Maximum 30 characters.');
-String get androidSignInTitle => Intl.message('Fingerprint Authentication',
+String get androidSignInTitle => Intl.message('Authentication required',
desc: 'Message showed as a title in a dialog which indicates the user '
- 'that they need to scan fingerprint to continue. It is used on '
+ 'that they need to scan biometric to continue. It is used on '
'Android side. Maximum 60 characters.');
-String get androidFingerprintRequiredTitle {
- return Intl.message('Fingerprint required',
- desc: 'Message showed as a title in a dialog which indicates the user '
- 'fingerprint is not set up yet on their device. It is used on Android'
- ' side. Maximum 60 characters.');
-}
+String get androidBiometricRequiredTitle => Intl.message('Biometric required',
+ desc: 'Message showed as a title in a dialog which indicates the user '
+ 'has not set up biometric authentication on their device. It is used on Android'
+ ' side. Maximum 60 characters.');
+
+String get androidDeviceCredentialsRequiredTitle => Intl.message(
+ 'Device credentials required',
+ desc: 'Message showed as a title in a dialog which indicates the user '
+ 'has not set up credentials authentication on their device. It is used on Android'
+ ' side. Maximum 60 characters.');
+
+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. It shows in a dialog on Android side.');
String get goToSettings => Intl.message('Go to settings',
desc: 'Message showed on a button that the user can click to go to '
@@ -115,10 +133,10 @@ String get goToSettings => Intl.message('Go to settings',
'and iOS side. Maximum 30 characters.');
String get androidGoToSettingsDescription => Intl.message(
- 'Fingerprint is not set up on your device. Go to '
- '\'Settings > Security\' to add your fingerprint.',
+ '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 '
- 'fingerprint on their device. It shows in a dialog on Android side.');
+ 'biometric on their device. It shows in a dialog on Android side.');
String get iOSLockOut => Intl.message(
'Biometric authentication is disabled. Please lock and unlock your screen to '
diff --git a/packages/local_auth/lib/error_codes.dart b/packages/local_auth/lib/error_codes.dart
index 3f6f298ba4f3..3b080c13baf7 100644
--- a/packages/local_auth/lib/error_codes.dart
+++ b/packages/local_auth/lib/error_codes.dart
@@ -3,7 +3,7 @@
// found in the LICENSE file.
// Exception codes for `PlatformException` returned by
-// `authenticateWithBiometrics`.
+// `authenticate`.
/// Indicates that the user has not yet configured a passcode (iOS) or
/// PIN/pattern/password (Android) on the device.
diff --git a/packages/local_auth/lib/local_auth.dart b/packages/local_auth/lib/local_auth.dart
index f1dbdd4840a8..f8a7228bcd8d 100644
--- a/packages/local_auth/lib/local_auth.dart
+++ b/packages/local_auth/lib/local_auth.dart
@@ -30,7 +30,29 @@ void setMockPathProviderPlatform(Platform platform) {
/// A Flutter plugin for authenticating the user identity locally.
class LocalAuthentication {
- /// Authenticates the user with biometrics available on the device.
+ /// The `authenticateWithBiometrics` method has been deprecated.
+ /// Use `authenticate` with `biometricOnly: true` instead
+ @Deprecated("Use `authenticate` with `biometricOnly: true` instead")
+ Future authenticateWithBiometrics({
+ required String localizedReason,
+ bool useErrorDialogs = true,
+ bool stickyAuth = false,
+ AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(),
+ IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(),
+ bool sensitiveTransaction = true,
+ }) =>
+ authenticate(
+ localizedReason: localizedReason,
+ useErrorDialogs: useErrorDialogs,
+ stickyAuth: stickyAuth,
+ androidAuthStrings: androidAuthStrings,
+ iOSAuthStrings: iOSAuthStrings,
+ sensitiveTransaction: sensitiveTransaction,
+ biometricOnly: true,
+ );
+
+ /// Authenticates the user with biometrics available on the device while also
+ /// allowing the user to use device authentication - pin, pattern, passcode.
///
/// Returns a [Future] holding true, if the user successfully authenticated,
/// false otherwise.
@@ -62,17 +84,21 @@ class LocalAuthentication {
/// dialog after the face is recognized to make sure the user meant to unlock
/// their phone.
///
+ /// Setting [biometricOnly] to true prevents authenticates from using non-biometric
+ /// local authentication such as pin, passcode, and passcode.
+ ///
/// Throws an [PlatformException] if there were technical problems with local
/// authentication (e.g. lack of relevant hardware). This might throw
/// [PlatformException] with error code [otherOperatingSystem] on the iOS
/// simulator.
- Future authenticateWithBiometrics({
+ Future authenticate({
required String localizedReason,
bool useErrorDialogs = true,
bool stickyAuth = false,
AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(),
IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(),
bool sensitiveTransaction = true,
+ bool biometricOnly = false,
}) async {
assert(localizedReason != null);
final Map args = {
@@ -80,6 +106,7 @@ class LocalAuthentication {
'useErrorDialogs': useErrorDialogs,
'stickyAuth': stickyAuth,
'sensitiveTransaction': sensitiveTransaction,
+ 'biometricOnly': biometricOnly,
};
if (_platform.isIOS) {
args.addAll(iOSAuthStrings.args);
@@ -87,14 +114,13 @@ class LocalAuthentication {
args.addAll(androidAuthStrings.args);
} else {
throw PlatformException(
- code: otherOperatingSystem,
- message: 'Local authentication does not support non-Android/iOS '
- 'operating systems.',
- details: 'Your operating system is ${_platform.operatingSystem}');
+ code: otherOperatingSystem,
+ message: 'Local authentication does not support non-Android/iOS '
+ 'operating systems.',
+ details: 'Your operating system is ${_platform.operatingSystem}',
+ );
}
- final bool? result =
- await _channel.invokeMethod('authenticateWithBiometrics', args);
- return result!;
+ return (await _channel.invokeMethod('authenticate', args)) ?? false;
}
/// Returns true if auth was cancelled successfully.
@@ -104,9 +130,7 @@ class LocalAuthentication {
/// Returns [Future] bool true or false:
Future stopAuthentication() async {
if (_platform.isAndroid) {
- final bool? result =
- await _channel.invokeMethod('stopAuthentication');
- return result!;
+ return await _channel.invokeMethod('stopAuthentication') ?? false;
}
return true;
}
@@ -118,6 +142,13 @@ class LocalAuthentication {
(await _channel.invokeListMethod('getAvailableBiometrics'))!
.isNotEmpty;
+ /// Returns true if device is capable of checking biometrics or is able to
+ /// fail over to device credentials.
+ ///
+ /// Returns a [Future] bool true or false:
+ Future isDeviceSupported() async =>
+ (await _channel.invokeMethod('isDeviceSupported')) ?? false;
+
/// Returns a list of enrolled biometrics
///
/// Returns a [Future] List with the following possibilities:
@@ -125,10 +156,12 @@ class LocalAuthentication {
/// - BiometricType.fingerprint
/// - BiometricType.iris (not yet implemented)
Future> getAvailableBiometrics() async {
- final List? result =
- await _channel.invokeListMethod('getAvailableBiometrics');
+ final List result = (await _channel.invokeListMethod(
+ 'getAvailableBiometrics',
+ )) ??
+ [];
final List biometrics = [];
- result!.forEach((String value) {
+ result.forEach((String value) {
switch (value) {
case 'face':
biometrics.add(BiometricType.face);
diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml
index 050cedb5d7d0..0f5a58835c3c 100644
--- a/packages/local_auth/pubspec.yaml
+++ b/packages/local_auth/pubspec.yaml
@@ -1,6 +1,6 @@
name: local_auth
-description: Flutter plugin for Android and iOS device authentication sensors
- such as Fingerprint Reader and Touch ID.
+description: Flutter plugin for Android and iOS devices to allow local
+ authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern.
homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth
version: 1.0.0-nullsafety.3
diff --git a/packages/local_auth/test/local_auth_test.dart b/packages/local_auth/test/local_auth_test.dart
index 52b8dbf21f72..f4bf9fe0314d 100644
--- a/packages/local_auth/test/local_auth_test.dart
+++ b/packages/local_auth/test/local_auth_test.dart
@@ -30,61 +30,135 @@ void main() {
log.clear();
});
- test('authenticate with no args on Android.', () async {
- setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android'));
- await localAuthentication.authenticateWithBiometrics(
- localizedReason: 'Needs secure');
- expect(
- log,
- [
- isMethodCall('authenticateWithBiometrics',
- arguments: {
- 'localizedReason': 'Needs secure',
- 'useErrorDialogs': true,
- 'stickyAuth': false,
- 'sensitiveTransaction': true,
- }..addAll(const AndroidAuthMessages().args)),
- ],
- );
- });
+ group("With device auth fail over", () {
+ test('authenticate with no args on Android.', () async {
+ setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android'));
+ await localAuthentication.authenticate(
+ localizedReason: 'Needs secure',
+ biometricOnly: true,
+ );
+ expect(
+ log,
+ [
+ isMethodCall('authenticate',
+ arguments: {
+ 'localizedReason': 'Needs secure',
+ 'useErrorDialogs': true,
+ 'stickyAuth': false,
+ 'sensitiveTransaction': true,
+ 'biometricOnly': true,
+ }..addAll(const AndroidAuthMessages().args)),
+ ],
+ );
+ });
- test('authenticate with no args on iOS.', () async {
- setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios'));
- await localAuthentication.authenticateWithBiometrics(
- localizedReason: 'Needs secure');
- expect(
- log,
- [
- isMethodCall('authenticateWithBiometrics',
- arguments: {
- 'localizedReason': 'Needs secure',
- 'useErrorDialogs': true,
- 'stickyAuth': false,
- 'sensitiveTransaction': true,
- }..addAll(const IOSAuthMessages().args)),
- ],
- );
+ test('authenticate with no args on iOS.', () async {
+ setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios'));
+ await localAuthentication.authenticate(
+ localizedReason: 'Needs secure',
+ biometricOnly: true,
+ );
+ expect(
+ log,
+ [
+ isMethodCall('authenticate',
+ arguments: {
+ 'localizedReason': 'Needs secure',
+ 'useErrorDialogs': true,
+ 'stickyAuth': false,
+ 'sensitiveTransaction': true,
+ 'biometricOnly': true,
+ }..addAll(const IOSAuthMessages().args)),
+ ],
+ );
+ });
+
+ test('authenticate with no sensitive transaction.', () async {
+ setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android'));
+ await localAuthentication.authenticate(
+ localizedReason: 'Insecure',
+ sensitiveTransaction: false,
+ useErrorDialogs: false,
+ biometricOnly: true,
+ );
+ expect(
+ log,
+ [
+ isMethodCall('authenticate',
+ arguments: {
+ 'localizedReason': 'Insecure',
+ 'useErrorDialogs': false,
+ 'stickyAuth': false,
+ 'sensitiveTransaction': false,
+ 'biometricOnly': true,
+ }..addAll(const AndroidAuthMessages().args)),
+ ],
+ );
+ });
});
- test('authenticate with no sensitive transaction.', () async {
- setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android'));
- await localAuthentication.authenticateWithBiometrics(
- localizedReason: 'Insecure',
- sensitiveTransaction: false,
- useErrorDialogs: false,
- );
- expect(
- log,
- [
- isMethodCall('authenticateWithBiometrics',
- arguments: {
- 'localizedReason': 'Insecure',
- 'useErrorDialogs': false,
- 'stickyAuth': false,
- 'sensitiveTransaction': false,
- }..addAll(const AndroidAuthMessages().args)),
- ],
- );
+ group("With biometrics only", () {
+ test('authenticate with no args on Android.', () async {
+ setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android'));
+ await localAuthentication.authenticate(
+ localizedReason: 'Needs secure',
+ );
+ expect(
+ log,
+ [
+ isMethodCall('authenticate',
+ arguments: {
+ 'localizedReason': 'Needs secure',
+ 'useErrorDialogs': true,
+ 'stickyAuth': false,
+ 'sensitiveTransaction': true,
+ 'biometricOnly': false,
+ }..addAll(const AndroidAuthMessages().args)),
+ ],
+ );
+ });
+
+ test('authenticate with no args on iOS.', () async {
+ setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios'));
+ await localAuthentication.authenticate(
+ localizedReason: 'Needs secure',
+ );
+ expect(
+ log,
+ [
+ isMethodCall('authenticate',
+ arguments: {
+ 'localizedReason': 'Needs secure',
+ 'useErrorDialogs': true,
+ 'stickyAuth': false,
+ 'sensitiveTransaction': true,
+ 'biometricOnly': false,
+ }..addAll(const IOSAuthMessages().args)),
+ ],
+ );
+ });
+
+ test('authenticate with no sensitive transaction.', () async {
+ setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android'));
+ await localAuthentication.authenticate(
+ localizedReason: 'Insecure',
+ sensitiveTransaction: false,
+ useErrorDialogs: false,
+ );
+ expect(
+ log,
+ [
+ isMethodCall('authenticate',
+ arguments: {
+ 'localizedReason': 'Insecure',
+ 'useErrorDialogs': false,
+ 'stickyAuth': false,
+ 'sensitiveTransaction': false,
+ 'biometricOnly': false,
+ }..addAll(const AndroidAuthMessages().args)),
+ ],
+ );
+ });
});
});
}