local_biometrics_manager

Advanced biometric authentication for Flutter on Android and iOS.

Detect the biometrics a device supports (fingerprint, face, iris, voice), check enrolment and lockout state, run authentication with a customizable prompt and optional device-credential (PIN/pattern/passcode) fallback, and subscribe to a live stream of status changes.

Feature Android iOS
Fingerprint ✅ (Touch ID)
Face ✅ (Face ID)
Iris ✅ (device dependent)
Voice
Device credential fallback ✅ (passcode)
Custom prompt colors / icon ➖ (system sheet)
Status stream

On iOS the OS renders the Face ID / Touch ID sheet, so the Android-only theming fields (BiometricUIConfig) are accepted but ignored.

Supported versions

  • Android: minSdk 21+ (uses AndroidX BiometricPrompt, which falls back to FingerprintManager on older devices automatically).
  • iOS: 12.0+ (uses LocalAuthentication).

Installation

dependencies:
  local_biometrics_manager: ^0.1.0

Android

No code changes are required — the plugin ships its own manifest permissions (USE_BIOMETRIC, and the legacy USE_FINGERPRINT for API ≤ 27). Your host Activity must be a FlutterActivity (the default), which is a FragmentActivity — this is required by BiometricPrompt.

iOS

Add a Face ID usage description to ios/Runner/Info.plist:

<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to verify it's really you.</string>

Usage

import 'package:local_biometrics_manager/local_biometrics_manager.dart';

final manager = LocalBiometricsManager.instance;

// 1. Check what the device can do.
final status = await manager.getBiometricStatus();
if (!status.isAvailable) {
  // No usable hardware, or nothing enrolled.
}

// 2. Authenticate.
final result = await manager.authenticate(
  const BiometricRequest(
    title: 'Verify your identity',
    subtitle: 'Access your account',
    description: 'Use your fingerprint or face to continue.',
    allowDeviceFallback: true,   // offer PIN/passcode if biometrics fail
    requireConfirmation: true,
    timeoutSeconds: 30,
    uiConfig: BiometricUIConfig(  // Android only
      primaryColorArgb: 0xFF6200EE,
      negativeButtonText: 'Cancel',
    ),
  ),
);

if (result.isSuccess) {
  // Unlock.
} else {
  switch (result.errorCode) {
    case BiometricErrorCode.canceled:   // user backed out
    case BiometricErrorCode.lockout:    // too many tries
    case BiometricErrorCode.notEnrolled:
    // …handle each case
  }
}

Check a specific type

final hasFace = await manager.isBiometricSupported(BiometricType.face);

Cancel an in-flight prompt

await manager.cancelAuthentication();

Listen for status changes

final sub = manager.onBiometricStatusChanged.listen((status) {
  // React to enrolment / lockout / availability changes.
});
// ...
await sub.cancel();

Hardware presence vs. enrollment (important)

Two different questions, two different fields — don't confuse them:

  • "Does the device have the sensor?"availableTypes, hardwareInfo.has*Sensor, and isBiometricSupported(type). These report hardware only and are true even if nothing is enrolled. availableTypes means the same thing on Android and iOS.
  • "Can I actually authenticate right now?"isAvailable / isEnrolled (and the enrollment-aware hardwareInfo.canAuthenticateStrong / canAuthenticateWeak). Gate your "Authenticate" button on isAvailable.

Platform notes:

  • On Android the OS can tell you that a biometric is enrolled, but not which modality — you can't ask "is a face enrolled" separately from "a fingerprint".
  • Many Android phones expose face only for the lock screen, not to apps, so hasFaceSensor / face may be false even though the phone face-unlocks.
  • allowedTypes is advisory: neither platform lets an app force a single modality. Listing deviceCredentials (or any) is honored — it permits the device PIN/passcode.

Error codes

All failures report one of these stable string codes on AuthenticationResult.errorCode (see BiometricErrorCode):

Code Meaning
BIOMETRIC_UNAVAILABLE Hardware not available
BIOMETRIC_NOT_ENROLLED No biometric enrolled
BIOMETRIC_LOCKOUT Too many failed attempts
BIOMETRIC_CANCELED User cancelled
BIOMETRIC_TIMEOUT Attempt timed out
BIOMETRIC_FAILED General failure

Routine outcomes (e.g. the user cancelling) come back as a normal AuthenticationResult with isSuccess == falseauthenticate does not throw for these, so you rarely need a try/catch.

Edge cases handled

  • No biometric hardware, or hardware present but nothing enrolled.
  • User cancelling mid-authentication (via the negative button or system back).
  • App backgrounding during authentication (the OS dismisses; you get a result).
  • Temporary and permanent lockout.
  • Configuration changes such as rotation (Android re-binds the activity).
  • Wide OS ranges: Android API 21–34+, iOS 12–17+.

Security notes

  • Authentication uses the platform's own secure prompt (BiometricPrompt / LocalAuthentication), which evaluates against hardware-backed biometrics where available. No biometric data ever crosses the platform channel — only a pass/fail result.
  • Contexts are freshly created per attempt and invalidated on cancel/timeout.
  • Keep any secret you unlock on the native side or in secure storage; do not hold it in Dart longer than necessary.

Testing

flutter test

The bundled tests cover the Dart facade, model (de)serialisation and the method channel with a mocked platform.

License

MIT — see LICENSE.

Libraries

local_biometrics_manager
Advanced biometric authentication for Flutter.