Face Id

Introduction

  • The biometric recognition is actually a kind of local authentication of mobile device

  • Usually used for retrieve sensitive data (secure-store of mobile device) when required to login

Implementation

  • The global store for biometric state

biometricStore.ts
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as LocalAuthentication from "expo-local-authentication";
import * as SecureStore from "expo-secure-store";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";

const CREDENTIALS_KEY = "biometric_credentials";

interface BiometricState {
  isBiometricEnabled: boolean;
  storedUserEmail: string | null;
  // Modal state (not persisted)
  isModalOpen: boolean;
  pendingCredentials: { email: string; password: string } | null;
}

interface BiometricActions {
  enableBiometric: (email: string, password: string) => Promise<void>;
  disableBiometric: () => Promise<void>;
  clearBiometric: () => Promise<void>;
  authenticateWithBiometric: () => Promise<{
    email: string;
    password: string;
  } | null>;
  checkBiometricAvailability: () => Promise<boolean>;
  // Modal actions
  promptEnableBiometric: (email: string, password: string) => void;
  confirmEnableBiometric: () => Promise<void>;
  dismissEnableBiometric: () => void;
}

type BiometricStore = BiometricState & BiometricActions;

export const useBiometric = create<BiometricStore>()(
  persist(
    (set, get) => ({
      isBiometricEnabled: false,
      storedUserEmail: null,
      isModalOpen: false,
      pendingCredentials: null,

      checkBiometricAvailability: async () => {
        const hasHardware = await LocalAuthentication.hasHardwareAsync();
        if (!hasHardware) return false;

        const isEnrolled = await LocalAuthentication.isEnrolledAsync();
        return isEnrolled;
      },

      enableBiometric: async (email: string, password: string) => {
        // Store credentials securely with biometric protection
        await SecureStore.setItemAsync(
          CREDENTIALS_KEY,
          JSON.stringify({ email, password }),
          {
            requireAuthentication: true,
            authenticationPrompt: "Authenticate to enable Face ID login",
          },
        );

        set({ isBiometricEnabled: true, storedUserEmail: email });
      },
      clearBiometric: async () => {
        await SecureStore.deleteItemAsync(CREDENTIALS_KEY);
        set({ isBiometricEnabled: false, storedUserEmail: null });
      },
      authenticateWithBiometric: async () => {
        try {
          // Retrieve stored credentials
          const credentialsJson = await SecureStore.getItemAsync(
            CREDENTIALS_KEY,
            {
              requireAuthentication: true,
              authenticationPrompt: "Authenticate to retrieve credentials",
            },
          );

          if (!credentialsJson) {
            return null;
          }

          return JSON.parse(credentialsJson) as {
            email: string;
            password: string;
          };
        } catch {
          return null;
        }
      },

      promptEnableBiometric: (email: string, password: string) => {
        set({ isModalOpen: true, pendingCredentials: { email, password } });
      },

      confirmEnableBiometric: async () => {
        const { pendingCredentials } = get();
        if (pendingCredentials) {
          // Store credentials securely with biometric protection
          await SecureStore.setItemAsync(
            CREDENTIALS_KEY,
            JSON.stringify(pendingCredentials),
            {
              requireAuthentication: true,
              authenticationPrompt: "Authenticate to enable Face ID login",
            },
          );
          set({
            isBiometricEnabled: true,
            storedUserEmail: pendingCredentials.email,
            isModalOpen: false,
            pendingCredentials: null,
          });
        }
      },

      dismissEnableBiometric: () => {
        set({
          isModalOpen: false,
          pendingCredentials: null,
        });
      },
    }),
    {
      name: "biometric-storage",
      storage: createJSONStorage(() => AsyncStorage),
      partialize: (state) => ({
        isBiometricEnabled: state.isBiometricEnabled,
        storedUserEmail: state.storedUserEmail,
      }),
    },
  ),
);
  • The login UI with biometric handling

  • The biometric login prompting modal

Last updated