Biometric login allows you to use FaceID and TouchID (iOS) or the Biometric Prompt (Android) to authenticate the user with a face or fingerprint scan.
This guide is intended for React Native projects using Expo.
-
Install expo LocalAuthentication to be able to authenticate users using biometrics
-
Install expo SecureStore to securely store the user's credentials and be able to login using its email and password
expo install expo-local-authentication expo-secure-store
-
Add
NSFaceIDUsageDescription
to yourInfo.plist
(Required in bare React Native projects):<key>NSFaceIDUsageDescription</key> <string>Necesitamos usar FaceID para iniciar sesión</string>
-
In your Login with email and password component check if the device supports biometrics and if it's enabled
useEffect(() => { (async () => { try { const compatible = await LocalAuthentication.hasHardwareAsync(); setIsCompatible(compatible); const value = await AsyncStorage.getItem("biometrics"); const biometrics = value ? JSON.parse(value) : false; setIsBiometricsEnabled(biometrics); } catch (error) { console.log(error); } })(); }, []);
-
Then add the function to handle the biometric login
const handleBiometricAuth = async () => { try { // Check if biometrics are saved on the user’s device const savedBiometrics = await LocalAuthentication.isEnrolledAsync(); if (!savedBiometrics) { throw new Error("No se encontraron huellas digitales registradas"); } const email = await SecureStore.getItemAsync("email"); if (!email) { throw new Error( "Inicia sesión con tu correo para poder usar la huella" ); } const biometricAuth = await LocalAuthentication.authenticateAsync({ promptMessage: "Huella digital", cancelLabel: "Usar contraseña", }); if (!biometricAuth.success) return; const password = await SecureStore.getItemAsync("password"); if (!password) { throw new Error( "Inicia sesión con tu correo para poder usar la huella" ); } onSubmitSuccess(email, password); // signInWithEmailAndPassword } catch (e) { Toast.show(String(e).substring(7), { position: Toast.positions.CENTER, }); } };
-
In your
onSubmitSuccess
function, save the user's credentials after it successfuly sings intry { await signInWithEmailAndPassword(email, password); // Save user's credential in the Secure Store await SecureStore.setItemAsync("email", email); await SecureStore.setItemAsync("password", password); ...
Those are the main steps to successfuly implement biometric login, it is recommended to add an option to enable or disable biometric authentication in your app's settings so the user can choose if he wants to use biometric login or not.
Also, when implementing biometric login, it's a common behaviour that your app will sign out the user after a certain amount of time. To implement that functionality, you can use React Native AppState and the following custom hook.
// useIdleSignOut customHook
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useNavigation } from "@react-navigation/native";
import dayjs from "dayjs";
import { useCallback, useEffect, useRef } from "react";
import { AppState, AppStateStatus } from "react-native";
import useSignOut from "./useSignOut";
import useAuth from "contexts/auth/auth.context.hooks";
import { RootNavigatorPropList } from "navigation/Navigator.types";
const useIdleSignOut = () => {
const { navigate } = useNavigation<RootNavigatorPropList>();
const { isAnonymous } = useAuth();
const signOut = useSignOut();
const appState = useRef<AppStateStatus | undefined>();
const idleHandler = useCallback(async () => {
const lastTimeActive = dayjs(await AsyncStorage.getItem("idleTime"));
const timeDifference = dayjs().diff(lastTimeActive, "minutes");
if (timeDifference >= 10) {
signOut();
navigate("LoginScreen");
}
AsyncStorage.removeItem("idleTime");
}, [navigate, signOut]);
const appStateChangeHandler = useCallback(
(nextAppState: AppStateStatus) => {
if (isAnonymous) return;
if (nextAppState === "background") {
AsyncStorage.setItem("idleTime", dayjs().toString());
}
if (
!appState.current ||
(appState.current.match(/inactive|background/) &&
nextAppState === "active")
) {
idleHandler();
}
appState.current = nextAppState;
},
[idleHandler, isAnonymous]
);
useEffect(() => {
AppState.addEventListener("change", appStateChangeHandler);
return () => {
AppState.removeEventListener("change", appStateChangeHandler);
};
}, [appStateChangeHandler]);
useEffect(() => {
if (appState.current || isAnonymous) return;
idleHandler();
appState.current = "active";
}, [idleHandler, isAnonymous]);
};
export default useIdleSignOut;