Skip to content
Back to archipelago.banuacoder.com

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 status to 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 ​

bash
archipelago generate biometric_auth_sdk

The 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.

text
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 BiometricAuthImplPackageModule in lib/di/injector.dart
  • adds the local_auth keep rules to android/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:

xml
<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:

xml
<uses-permission android:name="android.permission.USE_FINGERPRINT" />

Step 6: Check the Capability ​

getCapability() is safe on every platform and never throws.

dart
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:

dart
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:

dart
await biometricSDK.setBiometricEnabled(enabled: true);
final enabled = await biometricSDK.isBiometricEnabled();

Step 9: Typical Integration, an App Resume Guard ​

dart
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 ​

CustomizationWhere to Change
Prompt textThe reason argument of authenticate()
Keep the prompt open when the app backgroundsThe stickyAuth argument
Skip on debug buildsA kDebugMode check in your resume guard
Fall back to a PINShow your PIN screen on any BiometricAuthFailure

Next Steps ​