Feature SDKs
How to add Face ID and fingerprint unlock to a Flutter app
Pro Reference for biometric_auth_sdk: every variable, option and package it generates. Command: archipelago generate biometric_auth_sdk
Generate the Biometric Auth SDK, add iOS and Android settings, use biometrics as an unlock gate and not a login, and handle each failure reason.
What you'll learn
- Generating the Biometric Auth SDK
- Adding the iOS and Android platform settings
- Using biometrics as an unlock gate, not a primary credential
- Checking capability and handling each failure reason
Prerequisites
- An existing Archipelago monorepo (see Monorepo Scaffolding)
- Auth SDK generated and wired up (see Auth SDK Setup)
- A Pro subscription. Run
archipelago auth statusto confirm your tier.
The brick needs storage_sdk to save the enabled flag. It generates storage_sdk for you if it is missing.
Step 1: Generate the Biometric Auth SDK
archipelago generate biometric_auth_sdkThe brick has no variables. It needs archipelago.yaml in the workspace. It prints the iOS and Android reminders before it generates.
Step 2: Understand the Generated Structure
Other features depend on the contract without the implementation, so the brick uses the API/Impl split.
features/biometric_auth/
├── biometric_auth_api/
│ └── lib/src/
│ ├── biometric_auth_sdk.dart
│ └── models/ # Result, capability, type and failure reason
└── biometric_auth_impl/
└── lib/src/
├── biometric_auth_sdk_impl.dart # local_auth wrapper
└── di/Step 3: Check the Wiring
The post-generation hook:
- adds the packages to the workspace and the impl to the app
pubspec.yaml - registers
BiometricAuthImplPackageModuleinlib/di/injector.dart - adds the
local_authkeep rules toandroid/app/proguard-rules.pro
The SDK is a lazy singleton. Nothing runs at boot.
Step 4: Add the iOS Setting
The hook does not edit Info.plist. Add this key to ios/Runner/Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to quickly verify your identity.</string>Without it, the app crashes when it asks for Face ID. Touch ID needs no key.
Step 5: Add the Android Setting
Android 9 and later (API 28+) needs no permission. For API 23 to 27, add this to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.USE_FINGERPRINT" />Step 6: Check the Capability
getCapability() is safe on every platform and never throws.
final biometricSDK = getIt<BiometricAuthSDK>();
final capability = await biometricSDK.getCapability();
if (!capability.isDeviceSupported || !capability.isEnrolled) {
// Fall back to a password or PIN flow
return;
}BiometricCapability also lists availableTypes such as face and fingerprint.
On an Android emulator with no enrolled fingerprint, isEnrolled is false. Enroll one in the emulator settings or use a real device.
Step 7: Authenticate and Restore the Session
Biometrics verify the user on the device. They create no backend session. The SDK never calls loginSuccess for you. Your code does that after a success:
final result = await biometricSDK.authenticate(
reason: 'Verify your identity to continue.',
);
switch (result) {
case BiometricAuthSuccess():
getIt<AuthSDK>().loginSuccess(userId: storedUserId);
case BiometricAuthFailure(:final reason):
if (reason != BiometricAuthFailureReason.userCancelled) {
showError(reason);
}
}Read storedUserId from your secure storage. Write it during the first password login.
The failure reasons are notAvailable, notEnrolled, lockedOut, permanentlyLockedOut, passcodeNotSet, userCancelled and unknown.
Step 8: Let the User Turn It On or Off
The SDK keeps an enabled flag in storage_sdk:
await biometricSDK.setBiometricEnabled(enabled: true);
final enabled = await biometricSDK.isBiometricEnabled();Step 9: Typical Integration, an App Resume Guard
class AppLifecycleGuard extends StatefulWidget {
const AppLifecycleGuard({required this.child, super.key});
final Widget child;
@override
State<AppLifecycleGuard> createState() => _AppLifecycleGuardState();
}
class _AppLifecycleGuardState extends State<AppLifecycleGuard>
with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
unawaited(_prompt());
}
}
Future<void> _prompt() async {
final sdk = getIt<BiometricAuthSDK>();
if (!await sdk.isBiometricEnabled()) return;
final result = await sdk.authenticate(reason: 'Confirm it is you.');
if (result is BiometricAuthSuccess) {
getIt<AuthSDK>().loginSuccess(userId: storedUserId);
}
}
@override
Widget build(BuildContext context) => widget.child;
}Register the observer in initState. Remove it in dispose. Resolve getIt in a DI file or pass the SDK in by constructor, because the no_service_locator_* lints flag GetIt use in feature code.
Common Customizations
| Customization | Where to Change |
|---|---|
| Prompt text | The reason argument of authenticate() |
| Keep the prompt open when the app backgrounds | The stickyAuth argument |
| Skip on debug builds | A kDebugMode check in your resume guard |
| Fall back to a PIN | Show your PIN screen on any BiometricAuthFailure |
Next Steps
- Set up Connectivity SDK to handle offline states
- Configure monitoring to track biometric failure rates