Skip to content

Commit 61b3c54

Browse files
[local_auth] Adopt structured errors, and remove useErrorDialogs (#9981)
Switches from `PlatformException`s to new `LocalAuthException`s, which contain structured error information (specifically, a code that is a documented enum, rather than a list of magic strings that are used inconsistently and not all documented), bringing the plugin into alignment with [our current best practices](https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#platform-exception-handling. In addition to being clearer and easier to use for clients, this adds significantly more granular error codes than we currently have, which allows clients to better handle specific cases. Since this must be done as a breaking change (since the current `PlatformException`s are a combination of documented and de-facto API), this batches another breaking change, which is removing the error dialogs from the plugin, for the reasons described in flutter/flutter#175125. Since this is adding far more specific error codes, the PR also makes the internal change of passing essentially all native failure cases across the language boundary and then doing mapping to the cross-platform codes in Dart code, in keeping with our general recent practice of moving logic to the Dart side when there's no need for it to be native. Fixes flutter/flutter#113687 Fixes flutter/flutter#175125 Fixes flutter/flutter#151513 Fixes flutter/flutter#132757 Fixes flutter/flutter#148947 Fixes flutter/flutter#173506 Fixes flutter/flutter#174191 Closes flutter/flutter#96646 Fixes flutter/flutter#117810 Closes flutter/flutter#141283 Closes flutter/flutter#173142 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 6d10279 commit 61b3c54

File tree

10 files changed

+124
-198
lines changed

10 files changed

+124
-198
lines changed

packages/local_auth/local_auth/CHANGELOG.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
## NEXT
2-
1+
## 3.0.0
2+
3+
* **BREAKING CHANGES:**
4+
* Throws `LocalAuthException`s rather than `PlatformException`s for most
5+
failures cases, allowing structured error handling using the specific
6+
`LocalAuthExceptionCode` values.
7+
* Replaces `AuthenticationOptions` in `authenticate` with specific parameters.
8+
* `AuthenticationOptions.stickyAuth` corresponds to
9+
`persistAcrossBackgrounding`.
10+
* `AuthenticationOptions.useErrorDialogs` has no replacement, as specific
11+
error-handling UI should be up to plugin clients to determine. Callers
12+
should use the new structured error codes to detect and handle failure
13+
modes that used to have native dialogs.
314
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.
4-
* Updates README to reflect that only Android API 24+ is supported.
15+
* Updates README to reflect that Android older than API 24 and iOS older than
16+
13.0 are no longer supported.
517

618
## 2.3.0
719

packages/local_auth/local_auth/README.md

Lines changed: 24 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ fingerprint or facial recognition.
1010

1111
| | Android | iOS | macOS | Windows |
1212
|-------------|---------|-------|--------|-------------|
13-
| **Support** | SDK 24+ | 12.0+ | 10.14+ | Windows 10+ |
13+
| **Support** | SDK 24+ | 13.0+ | 10.14+ | Windows 10+ |
1414

1515
## Usage
1616

@@ -50,8 +50,8 @@ types and only check that some biometric is enrolled:
5050

5151
<?code-excerpt "readme_excerpts.dart (Enrolled)"?>
5252
```dart
53-
final List<BiometricType> availableBiometrics =
54-
await auth.getAvailableBiometrics();
53+
final List<BiometricType> availableBiometrics = await auth
54+
.getAvailableBiometrics();
5555
5656
if (availableBiometrics.isNotEmpty) {
5757
// Some biometrics are enrolled.
@@ -66,72 +66,34 @@ if (availableBiometrics.contains(BiometricType.strong) ||
6666

6767
### Options
6868

69-
The `authenticate()` method uses biometric authentication when possible, but
70-
also allows fallback to pin, pattern, or passcode.
69+
#### Requiring Biometrics
7170

72-
<?code-excerpt "readme_excerpts.dart (AuthAny)"?>
73-
```dart
74-
try {
75-
final bool didAuthenticate = await auth.authenticate(
76-
localizedReason: 'Please authenticate to show account balance',
77-
);
78-
// ···
79-
} on PlatformException {
80-
// ...
81-
}
82-
```
83-
84-
To require biometric authentication, pass `AuthenticationOptions` with
85-
`biometricOnly` set to `true`.
71+
The `authenticate()` method uses biometric authentication when possible, but
72+
by default also allows fallback to pin, pattern, or passcode. To require
73+
biometric authentication, set `biometricOnly` to `true`.
8674

8775
<?code-excerpt "readme_excerpts.dart (AuthBioOnly)"?>
8876
```dart
8977
final bool didAuthenticate = await auth.authenticate(
9078
localizedReason: 'Please authenticate to show account balance',
91-
options: const AuthenticationOptions(biometricOnly: true),
79+
biometricOnly: true,
9280
);
9381
```
9482

9583
*Note*: `biometricOnly` is not supported on Windows since the Windows implementation's underlying API (Windows Hello) doesn't support selecting the authentication method.
9684

97-
#### Dialogs
85+
#### Background Handling
9886

99-
The plugin provides default dialogs for the following cases:
87+
On mobile platforms, authentication may be canceled by the system if the app
88+
is backgrounded. This might happen if the user receives a phone call before
89+
they get a chance to authenticate, for example. Setting
90+
`persistAcrossBackgrounding` to true will cause the plugin to instead wait until
91+
the app is foregrounded again, retry the authentication, and only return once
92+
that new attempt completes.
10093

101-
1. Passcode/PIN/Pattern Not Set: The user has not yet configured a passcode on
102-
iOS or PIN/pattern on Android.
103-
2. Biometrics Not Enrolled: The user has not enrolled any biometrics on the
104-
device.
105-
106-
If a user does not have the necessary authentication enrolled when
107-
`authenticate` is called, they will be given the option to enroll at that point,
108-
or cancel authentication.
109-
110-
If you don't want to use the default dialogs, set the `useErrorDialogs` option
111-
to `false` to have `authenticate` immediately return an error in those cases.
112-
113-
<?code-excerpt "readme_excerpts.dart (NoErrorDialogs)"?>
114-
```dart
115-
import 'package:local_auth/error_codes.dart' as auth_error;
116-
// ···
117-
try {
118-
final bool didAuthenticate = await auth.authenticate(
119-
localizedReason: 'Please authenticate to show account balance',
120-
options: const AuthenticationOptions(useErrorDialogs: false),
121-
);
122-
// ···
123-
} on PlatformException catch (e) {
124-
if (e.code == auth_error.notAvailable) {
125-
// Add handling of no hardware here.
126-
} else if (e.code == auth_error.notEnrolled) {
127-
// ...
128-
} else {
129-
// ...
130-
}
131-
}
132-
```
94+
#### Dialog customization
13395

134-
If you want to customize the messages in the dialogs, you can pass
96+
If you want to customize the messages in the system dialogs, you can pass
13597
`AuthMessages` for each platform you support. These are platform-specific, so
13698
you will need to import the platform-specific implementation packages. For
13799
instance, to customize Android and iOS:
@@ -158,29 +120,26 @@ each platform.
158120

159121
### Exceptions
160122

161-
`authenticate` throws `PlatformException`s in many error cases. See
162-
`error_codes.dart` for known error codes that you may want to have specific
163-
handling for. For example:
123+
`authenticate` throws `LocalAuthException`s in most failure cases. See
124+
`LocalAuthExceptionCodes` for known error codes that you may want to have
125+
specific handling for. For example:
164126

165127
<?code-excerpt "readme_excerpts.dart (ErrorHandling)"?>
166128
```dart
167-
import 'package:flutter/services.dart';
168-
import 'package:local_auth/error_codes.dart' as auth_error;
169129
import 'package:local_auth/local_auth.dart';
170130
// ···
171131
final LocalAuthentication auth = LocalAuthentication();
172132
// ···
173133
try {
174134
final bool didAuthenticate = await auth.authenticate(
175135
localizedReason: 'Please authenticate to show account balance',
176-
options: const AuthenticationOptions(useErrorDialogs: false),
177136
);
178137
// ···
179-
} on PlatformException catch (e) {
180-
if (e.code == auth_error.notEnrolled) {
138+
} on LocalAuthException catch (e) {
139+
if (e.code == LocalAuthExceptionCode.noBiometricHardware) {
181140
// Add handling of no hardware here.
182-
} else if (e.code == auth_error.lockedOut ||
183-
e.code == auth_error.permanentlyLockedOut) {
141+
} else if (e.code == LocalAuthExceptionCode.temporaryLockout ||
142+
e.code == LocalAuthExceptionCode.biometricLockout) {
184143
// ...
185144
} else {
186145
// ...
@@ -287,12 +246,3 @@ the Android theme directly in `android/app/src/main/AndroidManifest.xml`:
287246
</application>
288247
...
289248
```
290-
291-
## Sticky Auth
292-
293-
You can set the `stickyAuth` option on the plugin to true so that plugin does not
294-
return failure if the app is put to background by the system. This might happen
295-
if the user receives a phone call before they get a chance to authenticate. With
296-
`stickyAuth` set to false, this would result in plugin returning failure result
297-
to the Dart app. If set to true, the plugin will retry authenticating when the
298-
app resumes.

packages/local_auth/local_auth/example/lib/main.dart

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,9 @@ class _MyAppState extends State<MyApp> {
3434
super.initState();
3535
auth.isDeviceSupported().then(
3636
(bool isSupported) => setState(
37-
() =>
38-
_supportState =
39-
isSupported
40-
? _SupportState.supported
41-
: _SupportState.unsupported,
37+
() => _supportState = isSupported
38+
? _SupportState.supported
39+
: _SupportState.unsupported,
4240
),
4341
);
4442
}
@@ -86,16 +84,27 @@ class _MyAppState extends State<MyApp> {
8684
});
8785
authenticated = await auth.authenticate(
8886
localizedReason: 'Let OS determine authentication method',
89-
options: const AuthenticationOptions(stickyAuth: true),
87+
persistAcrossBackgrounding: true,
9088
);
9189
setState(() {
9290
_isAuthenticating = false;
9391
});
92+
} on LocalAuthException catch (e) {
93+
print(e);
94+
setState(() {
95+
_isAuthenticating = false;
96+
if (e.code != LocalAuthExceptionCode.userCanceled &&
97+
e.code != LocalAuthExceptionCode.systemCanceled) {
98+
_authorized =
99+
'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}';
100+
}
101+
});
102+
return;
94103
} on PlatformException catch (e) {
95104
print(e);
96105
setState(() {
97106
_isAuthenticating = false;
98-
_authorized = 'Error - ${e.message}';
107+
_authorized = 'Unexpected error - ${e.message}';
99108
});
100109
return;
101110
}
@@ -118,20 +127,29 @@ class _MyAppState extends State<MyApp> {
118127
authenticated = await auth.authenticate(
119128
localizedReason:
120129
'Scan your fingerprint (or face or whatever) to authenticate',
121-
options: const AuthenticationOptions(
122-
stickyAuth: true,
123-
biometricOnly: true,
124-
),
130+
persistAcrossBackgrounding: true,
131+
biometricOnly: true,
125132
);
126133
setState(() {
127134
_isAuthenticating = false;
128135
_authorized = 'Authenticating';
129136
});
137+
} on LocalAuthException catch (e) {
138+
print(e);
139+
setState(() {
140+
_isAuthenticating = false;
141+
if (e.code != LocalAuthExceptionCode.userCanceled &&
142+
e.code != LocalAuthExceptionCode.systemCanceled) {
143+
_authorized =
144+
'Error - ${e.code.name}${e.description != null ? ': ${e.description}' : ''}';
145+
}
146+
});
147+
return;
130148
} on PlatformException catch (e) {
131149
print(e);
132150
setState(() {
133151
_isAuthenticating = false;
134-
_authorized = 'Error - ${e.message}';
152+
_authorized = 'Unexpected Error - ${e.message}';
135153
});
136154
return;
137155
}

packages/local_auth/local_auth/example/lib/readme_excerpts.dart

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99

1010
import 'package:flutter/material.dart';
1111
// #docregion ErrorHandling
12-
import 'package:flutter/services.dart';
13-
// #docregion NoErrorDialogs
14-
import 'package:local_auth/error_codes.dart' as auth_error;
15-
// #enddocregion NoErrorDialogs
1612
// #docregion CanCheck
1713
import 'package:local_auth/local_auth.dart';
1814
// #enddocregion CanCheck
@@ -64,8 +60,8 @@ class _MyAppState extends State<MyApp> {
6460

6561
Future<void> getEnrolledBiometrics() async {
6662
// #docregion Enrolled
67-
final List<BiometricType> availableBiometrics =
68-
await auth.getAvailableBiometrics();
63+
final List<BiometricType> availableBiometrics = await auth
64+
.getAvailableBiometrics();
6965

7066
if (availableBiometrics.isNotEmpty) {
7167
// Some biometrics are enrolled.
@@ -79,68 +75,30 @@ class _MyAppState extends State<MyApp> {
7975
// #enddocregion Enrolled
8076
}
8177

82-
Future<void> authenticate() async {
83-
// #docregion AuthAny
84-
try {
85-
final bool didAuthenticate = await auth.authenticate(
86-
localizedReason: 'Please authenticate to show account balance',
87-
);
88-
// #enddocregion AuthAny
89-
print(didAuthenticate);
90-
// #docregion AuthAny
91-
} on PlatformException {
92-
// ...
93-
}
94-
// #enddocregion AuthAny
95-
}
96-
9778
Future<void> authenticateWithBiometrics() async {
9879
// #docregion AuthBioOnly
9980
final bool didAuthenticate = await auth.authenticate(
10081
localizedReason: 'Please authenticate to show account balance',
101-
options: const AuthenticationOptions(biometricOnly: true),
82+
biometricOnly: true,
10283
);
10384
// #enddocregion AuthBioOnly
10485
print(didAuthenticate);
10586
}
10687

107-
Future<void> authenticateWithoutDialogs() async {
108-
// #docregion NoErrorDialogs
109-
try {
110-
final bool didAuthenticate = await auth.authenticate(
111-
localizedReason: 'Please authenticate to show account balance',
112-
options: const AuthenticationOptions(useErrorDialogs: false),
113-
);
114-
// #enddocregion NoErrorDialogs
115-
print(didAuthenticate ? 'Success!' : 'Failure');
116-
// #docregion NoErrorDialogs
117-
} on PlatformException catch (e) {
118-
if (e.code == auth_error.notAvailable) {
119-
// Add handling of no hardware here.
120-
} else if (e.code == auth_error.notEnrolled) {
121-
// ...
122-
} else {
123-
// ...
124-
}
125-
}
126-
// #enddocregion NoErrorDialogs
127-
}
128-
12988
Future<void> authenticateWithErrorHandling() async {
13089
// #docregion ErrorHandling
13190
try {
13291
final bool didAuthenticate = await auth.authenticate(
13392
localizedReason: 'Please authenticate to show account balance',
134-
options: const AuthenticationOptions(useErrorDialogs: false),
13593
);
13694
// #enddocregion ErrorHandling
13795
print(didAuthenticate ? 'Success!' : 'Failure');
13896
// #docregion ErrorHandling
139-
} on PlatformException catch (e) {
140-
if (e.code == auth_error.notEnrolled) {
97+
} on LocalAuthException catch (e) {
98+
if (e.code == LocalAuthExceptionCode.noBiometricHardware) {
14199
// Add handling of no hardware here.
142-
} else if (e.code == auth_error.lockedOut ||
143-
e.code == auth_error.permanentlyLockedOut) {
100+
} else if (e.code == LocalAuthExceptionCode.temporaryLockout ||
101+
e.code == LocalAuthExceptionCode.biometricLockout) {
144102
// ...
145103
} else {
146104
// ...

packages/local_auth/local_auth/example/pubspec.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth plugin.
33
publish_to: none
44

55
environment:
6-
sdk: ^3.7.0
7-
flutter: ">=3.29.0"
6+
sdk: ^3.9.0
7+
flutter: ">=3.35.0"
88

99
dependencies:
1010
flutter:
@@ -16,8 +16,8 @@ dependencies:
1616
# The example app is bundled with the plugin so we use a path dependency on
1717
# the parent directory to use the current plugin's version.
1818
path: ../
19-
local_auth_android: ^1.0.0
20-
local_auth_darwin: ^1.2.1
19+
local_auth_android: ^2.0.0
20+
local_auth_darwin: ^2.0.0
2121

2222
dev_dependencies:
2323
build_runner: ^2.1.10

0 commit comments

Comments
 (0)