Skip to content
OTFotf
All posts

Biometric enable in Expo apps ships when every fallback is handled

D
DaveAuthor
6 min read
Biometric enable in Expo apps ships when every fallback is handled

Why biometrics belong in your auth flow

Passwords are the weakest link in most mobile apps. Users reuse them, forget them, and abandon sign-in screens that demand them too often. Biometric authentication — Face ID on iOS, fingerprint or face enable on Android — removes that friction. One glance or touch and the user is in.

But adding biometrics is not just dropping in a prompt. Done carelessly, it creates lockout scenarios, confused fallback states, and a false sense of security. This guide covers the production-ready pattern for Expo apps: when to offer biometrics, how to wire expo-local-authentication, and the edge cases that separate a demo from a shippable feature.

Check what the device can actually do

Not every device supports biometrics, and among those that do, the user may not have enrolled anything. Never assume. Your first step at runtime is capability detection:

import * as LocalAuthentication from 'expo-local-authentication';

async function getBiometricState() {
  const compatible = await LocalAuthentication.hasHardwareAsync();
  const enrolled = await LocalAuthentication.isEnrolledAsync();
  const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
  return { compatible, enrolled, types };
}

There are three distinct states to handle: no hardware support, hardware present but nothing enrolled, and ready to use. Each deserves its own UI. On a device with no biometric hardware, never show the toggle at all. When hardware exists but nothing is enrolled, show the option as disabled with a hint to enroll in system settings — deep-linking the user to settings on iOS is a nice touch.

The supportedAuthenticationTypesAsync call tells you whether you are dealing with facial recognition, fingerprint, or iris scanning. This matters for your copy: saying "use Face ID" on an Android fingerprint device looks sloppy. Use generic wording like "biometric enable" unless you have branched on the actual type.

One codebase. iOS, Android, and web.

The Fitness Kit ships with auth, a database, and a backend already connected — no setup. Live demo at fitness-preview.otf-kit.dev.

See the live demo

Enroll explicitly, never silently

The biggest product mistake with biometrics is enabling them without clear user consent. The correct flow: after a successful password or SSO sign-in, offer an opt-in screen — "enable faster next time with Face ID?" — with equal-weight allow and skip buttons. Record the choice in secure storage so you only ask once.

import * as SecureStore from 'expo-secure-store';

const BIOMETRIC_KEY = 'biometric-opt-in';

async function setBiometricOptIn(enabled: boolean) {
  await SecureStore.setItemAsync(BIOMETRIC_KEY, enabled ? '1' : '0');
}

async function isBiometricOptedIn() {
  return (await SecureStore.getItemAsync(BIOMETRIC_KEY)) === '1';
}

Why expo-secure-store and not plain async storage? The opt-in flag gates a security-sensitive path, so it belongs in the keychain or keystore alongside the refresh token it protects. Keep the threat model simple: anything that decides whether a user gets in without a password lives behind hardware-backed storage.

Also provide a settings screen toggle so users can change their mind later. Disabling biometrics must immediately clear any stored convenience credentials — never leave a biometric-unlockable token sitting around after the user turned the feature off.

Authenticate with a clear fallback path

The authenticate call itself is straightforward, but the options you pass determine how solid the experience is:

async function unlockWithBiometrics(): Promise<boolean> {
  const result = await LocalAuthentication.authenticateAsync({
    promptMessage: 'enable MyApp',
    cancelLabel: 'Use password instead',
    disableDeviceFallback: false,
    requireConfirmation: false,
  });
  return result.success;
}

Keep disableDeviceFallback set to false on your main enable screen. If biometrics fail repeatedly — wet fingers, face mask, changed appearance — the OS can fall back to the device PIN or passcode, and the user stays unblocked. Reserve the strict biometric-only mode for genuinely sensitive actions like confirming a payment or revealing an API key, where falling back to a device PIN would weaken the guarantee.

Always handle the three failure shapes: user cancel (return to the password screen quietly), system cancel like an incoming call (retry affordance, no error toast), and lockout after too many attempts (explain the cooldown and offer the password path). Logging these as distinct analytics events will tell you within days whether your prompt timing or copy needs work.

Never store the password behind biometrics

A common anti-pattern: saving the raw password in secure storage and replaying it after a biometric check. This turns biometrics into a password-retrieval machine and expands your breach surface for zero benefit. The correct approach is token-based: after the initial full sign-in, keep a refresh token in secure storage, and gate access to that token behind the biometric check.

In practice the flow looks like this: app launches, sees a stored session, checks the biometric opt-in flag, prompts for biometrics, and only on success reads the refresh token and mints a fresh session. If your auth layer already handles session refresh — and a hardened session setup should — this slots in cleanly as a pre-step before the normal refresh call. Pair this with solid session hygiene so expired or revoked sessions fall back to full sign-in rather than looping on a dead token (see our guide on keeping Supabase auth sessions alive in Expo).

On sign-out, wipe everything: refresh token, opt-in flag state stays (it is a preference, not a credential), but any cached session material must go. A signed-out device that still holds a refresh token readable after a biometric prompt is a finding in any serious security review.

Handle re-enrollment and hardware changes

Biometrics are tied to what is enrolled on the device right now. If the user adds or removes a fingerprint, iOS invalidates the keychain items protected by biometrics, and Android may change the cryptographic key. Your app must survive this gracefully.

Concretely: wrap every secure-store read in the biometric flow with error handling that treats a key-invalidation error as "biometric state reset." Clear the opt-in flag, drop the stored token, and route the user through full sign-in with a friendly message — "Your device biometrics changed, please sign in again to re-enable quick enable." This happens rarely, but when it does, a crash or a silent hang destroys trust instantly.

Test this path on real hardware before release. Simulators can fake a successful biometric match, but enrollment-change invalidation behaves differently on physical devices across OS versions. Add it to your release checklist next to push-notification and deep-link smoke tests.

Combine biometrics with route guards

Biometric enable should sit in front of your navigation, not inside individual screens. The cleanest architecture: an enable gate at the root layout that blocks the entire authenticated stack until the biometric check passes. Unauthenticated routes like sign-in and password reset stay outside the gate, and your existing auth guards keep working unchanged underneath.

This composes well with Expo Router auth guards that survive expired sessions and deep links — the biometric gate runs first, then the normal session and role checks run as before. Two layers, each with one job, each testable in isolation.

One subtlety: deep links that arrive while the app is locked. Queue the incoming URL, complete the biometric enable, then deliver the link to the router. Dropping a deep link because the lock screen was showing is the kind of bug users report as "sharing is broken" — technically wrong, but you will still lose the argument.

What to ship this week

If biometrics are on your roadmap, here is the smallest shippable slice: capability detection with three-state UI, explicit opt-in after sign-in, token-gated enable with device-PIN fallback, secure wipe on sign-out and biometric reset, and a root-level gate in front of your auth stack. That is roughly a day of work with expo-local-authentication and expo-secure-store, and it covers the scenarios real users will hit.

Biometric enable is one of those features users never praise and always punish when it is missing or broken. Ship it quietly, handle every fallback, and your sign-in funnel will thank you.

Sources

react-nativecross-platformsupabase
OTF Fitness Kit

Stop wiring. Start shipping.

  • Login, database, and backend already connected — nothing to set up
  • iOS + Android + web from one codebase
  • AI configs pre-tuned + 40+ tested prompts included