DEV Community

DETL INC
DETL INC

Posted on

How to Implement Face ID and Touch ID in React Native Expo in your Onboarding flow, Login Screen and Settings Screen

Image description

At DETL, we specialize in crafting stellar software applications that seamlessly blend hardcore engineering with sleek design. Our mission is to deliver both mobile and web applications that provide exceptional user experience without compromising on aesthetics or functionality. In this blog post we will guide you through implementing Face ID and Touch ID authentication in your React Native Expo app’s onboarding flow, login screen and settings screen.

Introduction

Biometric authentication has become the standard in modern applications, offering users a quick, reliable and secure way to access their data. It is very important that engineers who are developing applications implement Face ID and Touch ID to enhance their apps user experience.

In this blog post we will cover the following

  1. Required Packages to install: The essential packages you need to install.
  2. Custom Hook Creation (useSetting.tsx): How to create a custom hook for managing biometric authentication.
  3. Onboarding Implementation: Integrating Face ID and Touch ID in the onboarding screen.
  4. Login Screen Integration: Implementing biometric login in your login screen.
  5. Settings Screen Toggle: Allowing users to enable or disable biometric authentication from the settings screen.

1. Required Packages to install

Before we tell you how to implement Face ID or Touch ID, we will require you to first install the following packages.

  1. expo-local-authentication: Provides access to biometric authentication capabilities. expo install expo-local-authentication
  2. expo-secure-store: Allows you to securely store sensitive data like user credentials. expo install expo-secure-store
  3. @react-native-async-storage/async-storage: A simple, unencrypted, asynchronous storage system. yarn add @react-native-async-storage/async-storage.
  4. expo-checkbox: A customizable checkbox component. expo install expo-checkbox
  5. Other Dependencies: Ensure you have expo-router, expo-font, and any other dependencies your project requires.

Install all dependencies together

expo install expo-local-authentication expo-secure-store expo-checkbox @react-native-async-storage/async-storage
Enter fullscreen mode Exit fullscreen mode

2. Creating a Custom Hook: useSettings

To manage biometric authentication settings across your app, we’ll create a custom hook named useSettings. This hook will handle enabling/disabling Face ID and Touch ID, checking available biometric types, and storing the authentication status.

Creating the Hook

In your hooks directory, create a new file called useSettings.tsx and add the following code:

import { useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as LocalAuthentication from 'expo-local-authentication';
import { useToast } from '@/providers/useToast';

interface BiometricAuthStatus {
  isFaceIDEnabled: boolean;
  isTouchIDEnabled: boolean;
}

export const useSettings = () => {
  const [biometricAuth, setBiometricAuth] = useState<BiometricAuthStatus>({
    isFaceIDEnabled: false,
    isTouchIDEnabled: false,
  });

  const [availableBiometrics, setAvailableBiometrics] = useState<LocalAuthentication.AuthenticationType[]>([]);

  const { showToast } = useToast();

  const checkAvailableBiometrics = async () => {
    try {
      const biometrics = await LocalAuthentication.supportedAuthenticationTypesAsync();
      setAvailableBiometrics(biometrics);
    } catch (error) {
      console.log(error);
    }
  };

  const enableBiometricAuth = async (biometricType: 'FaceID' | 'TouchID'): Promise<boolean> => {
    try {
      const isBiometricAvailable = await LocalAuthentication.hasHardwareAsync();
      if (!isBiometricAvailable) {
        showToast('error', 'Your device does not support biometric authentication.');
        return false;
      }

      const savedBiometric = await LocalAuthentication.isEnrolledAsync();
      if (!savedBiometric) {
        showToast('error', 'No biometric records found.');
        return false;
      }

      const result = await LocalAuthentication.authenticateAsync({
        promptMessage: 'Authenticate with biometrics',
        fallbackLabel: 'Enter password',
      });

      if (result.success) {
        setBiometricAuth((prevState) => {
          const updatedState = { ...prevState };
          if (biometricType === 'FaceID') {
            updatedState.isFaceIDEnabled = !prevState.isFaceIDEnabled;
            showToast(
              'success',
              `Face ID ${updatedState.isFaceIDEnabled ? 'enabled' : 'disabled'} successfully.`
            );
          } else if (biometricType === 'TouchID') {
            updatedState.isTouchIDEnabled = !prevState.isTouchIDEnabled;
            showToast(
              'success',
              `Touch ID ${updatedState.isTouchIDEnabled ? 'enabled' : 'disabled'} successfully.`
            );
          }
          // Save the updated status
          AsyncStorage.setItem('biometricAuthStatus', JSON.stringify(updatedState));
          return updatedState;
        });

        return true;
      } else {
        showToast('error', 'Authentication failed.');
        return false;
      }
    } catch (error) {
      console.log(error);
      showToast('error', 'An error occurred while enabling biometric authentication.');
      return false;
    }
  };

  const checkIfBiometricEnabled = async (): Promise<void> => {
    const biometricStatus = await AsyncStorage.getItem('biometricAuthStatus');
    if (biometricStatus) {
      const parsedStatus: BiometricAuthStatus = JSON.parse(biometricStatus);
      setBiometricAuth(parsedStatus);
    } else {
      setBiometricAuth({
        isFaceIDEnabled: false,
        isTouchIDEnabled: false,
      });
    }
  };

  useEffect(() => {
    checkAvailableBiometrics();
    checkIfBiometricEnabled();
  }, []);

  return {
    biometricAuth,
    enableBiometricAuth,
    checkIfBiometricEnabled,
    availableBiometrics,
  };
};
Enter fullscreen mode Exit fullscreen mode
  • State Management: We use useState to manage the biometric authentication status and the available biometric types.

  • Check Available Biometrics: checkAvailableBiometrics checks what biometric authentication types are supported on the device.

  • Enable Biometric Authentication: enableBiometricAuth prompts the user to authenticate using biometrics and updates the state accordingly.

  • Persisting State: We use AsyncStorage to persist the biometric authentication status across app sessions.

  • Initialization: The useEffect hook initializes the available biometrics and checks if biometric authentication is enabled.

3. Implementing Face ID and Touch ID in the Onboarding Screen

Next, we’ll integrate biometric authentication options into the onboarding flow, allowing users to enable Face ID or Touch ID during onboarding.

Onboarding Screen Component

Create or update your onboarding screen component, for example, FaceID_TouchID.tsx:

import React from "react";
import { View, Image, Text, StyleSheet, TouchableOpacity } from "react-native";
import GlobalButton from "@/components/shared/Button";
import Header from "@/components/shared/Header";
import HeaderTitle from "@/components/shared/HeaderTitle";
import Subtitle from "@/components/shared/Subtitle";
import { Colors, globalStyle } from "@/constants/Colors";
import { useRouter } from "expo-router";
import { useSettings } from "@/hooks/settings/useSettings";
import * as LocalAuthentication from "expo-local-authentication";

const FaceID_TouchID = () => {
  const { biometricAuth, enableBiometricAuth, availableBiometrics } = useSettings();
  const router = useRouter();

  const isFaceIdAvailable = availableBiometrics.includes(
    LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION
  );

  const isTouchIDAvailable = availableBiometrics.includes(
    LocalAuthentication.AuthenticationType.FINGERPRINT
  );

  const isAnyBiometricAvailable = isFaceIdAvailable || isTouchIDAvailable;

  const handleEnableBiometricAuth = async (biometricType: 'FaceID' | 'TouchID') => {
    const success = await enableBiometricAuth(biometricType);
    if (success) {
      router.push("/(auth)/OnboardingFlow/NextOnboardingScreenAfterThisOne");
    }
  };

  return (
    <>
      <Header title="Enable Biometric Authentication" />
      <HeaderTitle title={`Easy Access\nBetter Support`} />
      <View style={styles.container}>
        {isAnyBiometricAvailable ? (
          <>
            {isFaceIdAvailable && (
              <Image
                source={require("@/assets/images/onboarding/FaceIDImage.png")}
                resizeMode="contain"
                style={styles.image}
              />
            )}
            {isTouchIDAvailable && (
              <Image
                source={require("@/assets/images/TouchID.png")}
                resizeMode="contain"
                style={styles.image}
              />
            )}
          </>
        ) : (
          <Text style={styles.notAvailableText}>
            Biometric authentication is not available on this device.
          </Text>
        )}
        {isAnyBiometricAvailable && (
          <Subtitle
            style={{ marginTop: 30 }}
            subtitle="Enable biometric authentication for quick, secure, and effortless access to your support whenever you need it."
          />
        )}
      </View>
      <View style={styles.buttonContainer}>
        {isAnyBiometricAvailable && (
          <>
            {isFaceIdAvailable && (
              <GlobalButton
                title={
                  biometricAuth.isFaceIDEnabled
                    ? "Face ID Enabled"
                    : "Enable Face ID"
                }
                disabled={false}
                buttonColor={Colors.button.backgroundButtonDark}
                textColor={Colors.button.textLight}
                showIcon={false}
                onPress={() => handleEnableBiometricAuth("FaceID")}
              />
            )}
            {isTouchIDAvailable && (
              <GlobalButton
                title={
                  biometricAuth.isTouchIDEnabled
                    ? "Touch ID Enabled"
                    : "Enable Touch ID"
                }
                disabled={false}
                buttonColor={Colors.button.backgroundButtonDark}
                textColor={Colors.button.textLight}
                showIcon={false}
                onPress={() => handleEnableBiometricAuth("TouchID")}
              />
            )}
          </>
        )}
      </View>
      <TouchableOpacity
        style={styles.skipButton}
        onPress={() => router.push("/(auth)/OnboardingFlow/screenThirteen")}
      >
        <Text style={styles.skipText}>
          {isAnyBiometricAvailable ? "Enable later" : "Skip"}
        </Text>
      </TouchableOpacity>
    </>
  );
};

export default FaceID_TouchID;

const styles = StyleSheet.create({
  container: {
    flex: 0.9,
    alignItems: "center",
    justifyContent: "center",
  },
  image: {
    height: 250,
    width: 250,
  },
  buttonContainer: {
    marginHorizontal: globalStyle.container.marginHorizontal,
    marginBottom: 10,
  },
  skipButton: {
    justifyContent: "center",
    alignItems: "center",
  },
  skipText: {
    textAlign: "center",
    fontFamily: globalStyle.font.fontFamilyBold,
    color: Colors.button.textDark,
  },
  notAvailableText: {
    textAlign: "center",
    fontFamily: globalStyle.font.fontFamilyMedium,
    color: Colors.button.textDark,
  },
});
Enter fullscreen mode Exit fullscreen mode
  • Biometric Availability: We check which biometric types are available on the device.

  • Dynamic UI: The UI adapts based on the available biometrics, showing relevant images and buttons.

  • Handling Authentication: When the user taps “Enable Face ID” or “Enable Touch ID”, we call handleEnableBiometricAuth, which uses our custom hook to enable biometric authentication.

  • Navigation: Upon successful authentication, we navigate to the next onboarding screen which in this case we called NextOnboardingScreenAfterThisOne which is your next onboarding screen

router.push("/(auth)/OnboardingFlow/NextOnboardingScreenAfterThisOne");
Enter fullscreen mode Exit fullscreen mode

4. Implementing Face ID in the Login Screen

We’ll integrate biometric authentication into the login screen, allowing users to log in using Face ID or Touch ID if they have previously enabled it.

Login Screen Component

Update your Login.tsx component as follows:

import React, { useState, useEffect, useRef } from "react";
import { View, Text, StyleSheet, ScrollView, Keyboard } from "react-native";
import * as SecureStore from "expo-secure-store";
import * as LocalAuthentication from "expo-local-authentication";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Feather } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router";
import GlobalButton from "@/components/shared/Button";
import CustomTextInput from "@/components/shared/CustomTextInput";
import Header from "@/components/shared/Header";
import HeaderTitle from "@/components/shared/HeaderTitle";
import PasswordStrengthDisplay from "@/components/shared/PasswordStrengthDisplay";
import Checkbox from "expo-checkbox";
import { Colors, globalStyle } from "@/constants/Colors";
import { useSession } from "@/providers/Auth/AuthProvider";
import { useToast } from "@/providers/useToast";
import PsyvatarLogo from "@/assets/images/Logo";

const Login = () => {
  const { params } = useLocalSearchParams();
  const { showToast } = useToast();
  const { signIn, isLoading, signUp } = useSession();

  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [name, setName] = useState("");
  const [securePassword, setSecurePassword] = useState(true);
  const [isChecked, setChecked] = useState(false);

  const [isBiometricSupported, setIsBiometricSupported] = useState(false);
  const [biometricType, setBiometricType] = useState("");
  const [isBiometricEnabled, setIsBiometricEnabled] = useState(false);

  const emailRef = useRef(null);
  const passwordRef = useRef(null);
  const nameRef = useRef(null);

  const isInvalidRegister = name === "" || email === "" || password === "";
  const isInvalidLogin = email === "" || password === "";

  useEffect(() => {
    setTimeout(() => {
      if (params === "register") {
        nameRef?.current?.focus();
      } else {
        emailRef?.current?.focus();
      }
    }, 800);

    const getEmailFromStore = async () => {
      try {
        const storedEmail = await SecureStore.getItemAsync("userEmail");
        if (storedEmail) {
          setEmail(storedEmail);
          setChecked(true);
        }
      } catch (error) {
        console.log("Error fetching email from SecureStore", error);
      }
    };

    const checkBiometricSupport = async () => {
      const compatible = await LocalAuthentication.hasHardwareAsync();
      setIsBiometricSupported(compatible);

      if (compatible) {
        const savedBiometrics = await LocalAuthentication.isEnrolledAsync();
        if (savedBiometrics) {
          const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
          if (
            types.includes(
              LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION
            )
          ) {
            setBiometricType("Face ID");
          } else if (
            types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)
          ) {
            setBiometricType("Touch ID");
          } else {
            setBiometricType("Biometrics");
          }
        }
      }

      const biometricEnabled = await AsyncStorage.getItem("biometricAuthStatus");
      if (biometricEnabled) {
        const parsedStatus = JSON.parse(biometricEnabled);
        setIsBiometricEnabled(
          parsedStatus.isFaceIDEnabled || parsedStatus.isTouchIDEnabled
        );
      }
    };

    getEmailFromStore();
    checkBiometricSupport();
  }, []);

  const handleRegister = () => {
    if (params === "register" && isInvalidRegister) {
      return showToast("warning", "One or more fields are missing");
    }
    signUp(email, password, name);
  };

  const handleLogin = async () => {
    if (params === "login" && isInvalidLogin) {
      return showToast("warning", "One or more fields are missing");
    }
    Keyboard.dismiss();

    const loginSuccess = await signIn(email, password);

    if (loginSuccess) {
      if (isChecked) {
        try {
          await SecureStore.setItemAsync("userEmail", email);
        } catch (error) {
          console.log("Error saving email to SecureStore", error);
        }
      } else {
        await SecureStore.deleteItemAsync("userEmail");
      }

      if (isBiometricEnabled) {
        try {
          await SecureStore.setItemAsync("userEmail", email);
          await SecureStore.setItemAsync("userPassword", password);
        } catch (error) {
          console.log("Error saving credentials to SecureStore", error);
        }
      }
    }
  };

  const handleBiometricLogin = async () => {
    try {
      const result = await LocalAuthentication.authenticateAsync({
        promptMessage: `Login with ${biometricType}`,
        fallbackLabel: "Enter password",
      });

      if (result.success) {
        const storedEmail = await SecureStore.getItemAsync("userEmail");
        const storedPassword = await SecureStore.getItemAsync("userPassword");

        if (storedEmail && storedPassword) {
          await signIn(storedEmail, storedPassword);
        } else {
          showToast("error", "No credentials found. Please log in manually.");
          setIsBiometricSupported(false);
        }
      } else {
        showToast("error", "Biometric authentication failed.");
        setIsBiometricSupported(false);
      }
    } catch (error) {
      console.log("Biometric authentication error:", error);
      showToast("error", "An error occurred during biometric authentication.");
      setIsBiometricSupported(false);
    }
  };

  const getButtonProps = () => {
    if (params === "login" && isBiometricEnabled && isBiometricSupported) {
      return {
        title: `Login with ${biometricType}`,
        onPress: handleBiometricLogin,
      };
    } else if (params === "login") {
      return {
        title: "Sign In",
        onPress: handleLogin,
      };
    } else {
      return {
        title: "Sign Up",
        onPress: handleRegister,
      };
    }
  };

  const { title, onPress } = getButtonProps();

  return (
    <>
      <Header logo={<Logo />} BackButton />
      <HeaderTitle
        title={`${params === "register" ? "Welcome!" : "Welcome Back!"}`}
      />
      <View style={styles.container}>
        <ScrollView
          style={styles.container}
          showsVerticalScrollIndicator={false}
        >
          {params === "register" && (
            <CustomTextInput
              keyboardType="default"
              label="Name"
              placeholder="Your name"
              leftIcon={
                <Feather
                  name="user"
                  size={Colors.button.iconSize}
                  color={Colors.button.iconColorDark}
                />
              }
              secureTextEntry={false}
              onChangeText={(text) => setName(text)}
              value={name}
              height={50}
              ref={nameRef}
              onSubmitEditing={() => emailRef?.current?.focus()}
            />
          )}
          <CustomTextInput
            ref={emailRef}
            keyboardType="email-address"
            label="Email address"
            placeholder="Enter your email"
            leftIcon={
              <Feather
                name="mail"
                size={Colors.button.iconSize}
                color={Colors.button.iconColorDark}
              />
            }
            secureTextEntry={false}
            onChangeText={setEmail}
            value={email}
            autoCapitalize="none"
            height={50}
            onSubmitEditing={() => passwordRef?.current?.focus()}
          />
          <CustomTextInput
            ref={passwordRef}
            value={password}
            keyboardType="visible-password"
            label="Password"
            placeholder="Enter your password"
            leftIcon={
              <Feather
                name="lock"
                size={Colors.button.iconSize}
                color={Colors.button.iconColorDark}
              />
            }
            rightIcon={
              <Feather
                name={securePassword ? "eye-off" : "eye"}
                size={Colors.button.iconSize}
                color={Colors.button.iconColorDark}
              />
            }
            rightIconPressable={() => setSecurePassword(!securePassword)}
            secureTextEntry={securePassword}
            onChangeText={setPassword}
            autoCapitalize="none"
            height={50}
            onSubmitEditing={() => Keyboard.dismiss()}
          />

          {params === "register" && (
            <PasswordStrengthDisplay password={password} />
          )}

          <View style={styles.rememberMeContainer}>
            <Checkbox
              style={styles.checkbox}
              value={isChecked}
              onValueChange={setChecked}
              color={isChecked ? "#4096C1" : undefined}
            />
            <Text style={styles.rememberText}>Remember me</Text>
          </View>
        </ScrollView>

        <View style={styles.buttonWrapper}>
          <GlobalButton
            title={title}
            onPress={onPress}
            disabled={isLoading}
            loading={isLoading}
            buttonColor={Colors.button.backgroundButtonDark}
            textColor={Colors.button.textLight}
            showIcon={false}
          />

       {params === "login" && isBiometricEnabled && isBiometricSupported && (
            <TouchableOpacity
              onPress={() => setIsBiometricSupported(false)}
              style={{
                margin: 10,
              }}
            >
              <Text
                style={{
                  textAlign: "center",
                  fontFamily: globalStyle.font.fontFamilyBold,
                  color: Colors.button.textDark,
                }}
              >
                Sign In manually
              </Text>
            </TouchableOpacity>
          )}
        </View>
      </View>
    </>
  );
};

export default Login;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginHorizontal: globalStyle.container.marginHorizontal,
  },
  rememberText: {
    color: "#282545",
    fontWeight: "400",
    fontSize: 14,
  },
  rememberMeContainer: {
    marginVertical: 10,
    marginBottom: 0,
    flexDirection: "row",
    alignItems: "center",
  },
  checkbox: {
    margin: 8,
    borderRadius: 4,
  },
  buttonWrapper: {
    marginHorizontal: globalStyle.container.marginHorizontal,
    marginBottom: 15,
  },
});
Enter fullscreen mode Exit fullscreen mode
  • Biometric Support Check: On component mount, we check if the device supports biometric authentication and if it’s enabled.

  • Biometric Login Handling: handleBiometricLogin manages the biometric authentication flow. If successful, it retrieves stored credentials and logs the user in.

  • Dynamic Button Rendering: Based on whether biometric login is available, we dynamically set the button’s title and onPress handler.

  • Storing Credentials: Upon a successful manual login, if biometric authentication is enabled, we securely store the user’s credentials for future biometric logins.

  • We have a button on the bottom of the file where we allow a user to toggle whether they want to sign in manually, which is a fail-safe approach in case biometric authentication doesn’t work.

5. Adding Biometric Authentication Toggle in the Settings Screen

To give users control over biometric authentication, we’ll add toggle switches in the settings screen, allowing them to enable or disable Face ID or Touch ID at any time.

Settings Screen Component

Update your SettingsScreen.tsx component as follows:

import React, { useState } from "react";
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  TouchableOpacity,
  Image,
  Modal,
  Linking,
} from "react-native";
import { Feather, FontAwesome } from "@expo/vector-icons";
import { SettingOption, SettingToggle } from "./components";
import { useSession } from "@/providers/Auth/AuthProvider";
import GlobalButton from "@/components/shared/Button";
import { Colors, globalStyle } from "@/constants/Colors";
import HeaderTitle from "@/components/shared/HeaderTitle";
import Subtitle from "@/components/shared/Subtitle";
import AnimatedWrapper from "@/components/shared/animation";
import { useRouter } from "expo-router";
import { useSettings } from "@/hooks/settings/useSettings";
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
import { useHandleApiError } from "@/hooks/settings/useHandleApiError";
import * as LocalAuthentication from "expo-local-authentication";

export const BASE_DELAY = 50;

export default function SettingsScreen() {
  const { signOut, deleteUserRecord, isLoading } = useSession();
  const { biometricAuth, enableBiometricAuth, availableBiometrics } = useSettings();

  const isFaceIDAvailable = availableBiometrics.includes(
    LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION
  );

  const isTouchIDAvailable = availableBiometrics.includes(
    LocalAuthentication.AuthenticationType.FINGERPRINT
  );

  // Set up biometric toggle based on availability
  let biometricLabel = "";
  let isBiometricEnabled = false;
  let toggleBiometricAuth = null;
  let biometricIcon = null;

  if (isFaceIDAvailable) {
    biometricLabel = "Face ID";
    isBiometricEnabled = biometricAuth.isFaceIDEnabled;
    toggleBiometricAuth = () => enableBiometricAuth("FaceID");
    biometricIcon = (
      <MaterialCommunityIcons
        name="face-recognition"
        size={20}
        color="#4096C1"
      />
    );
  } else if (isTouchIDAvailable) {
    biometricLabel = "Touch ID";
    isBiometricEnabled = biometricAuth.isTouchIDEnabled;
    toggleBiometricAuth = () => enableBiometricAuth("TouchID");
    biometricIcon = (
      <MaterialCommunityIcons name="fingerprint" size={20} color="#4096C1" />
    );
  }

  const router = useRouter();

  const [isPushEnabled, setIsPushEnabled] = useState(false);

  const togglePushNotifications = () => setIsPushEnabled(!isPushEnabled);

  const [modalVisible, setModalVisible] = useState(false);
  const handleApiError = useHandleApiError();

  const handleDelete = () => {
    setModalVisible(true);
  };

  const deleteAccount = async () => {
    try {
      const response = await deleteUserRecord();
      if (response.success) {
        setTimeout(() => {
          setModalVisible(!modalVisible);
        }, 500);
      } else {
        handleApiError(response);
      }
    } catch (error) {
      console.log(error);
    }
  };

  const cancelDelete = () => {
    setModalVisible(!modalVisible);
  };

  return (
    <View style={styles.container}>
      <ScrollView showsVerticalScrollIndicator={false}>
        <AnimatedWrapper delay={BASE_DELAY * 1}>
          <Text style={styles.sectionTitle}>GENERAL</Text>
        </AnimatedWrapper>
        <SettingOption
          delay={BASE_DELAY * 2}
          label="Get Help"
          icon={
            <FontAwesome name="question-circle-o" size={20} color="#4096C1" />
          }
          onPress={() => router.navigate("/(auth)/help")}
        />
        <SettingOption
          delay={BASE_DELAY * 3}
          label="Contact Us"
          icon={<Feather name="mail" size={20} color="#4096C1" />}
          onPress={() => router.navigate("/(auth)/help/contact_us")}
        />
        <SettingOption
          delay={BASE_DELAY * 4}
          label="Subscription Details"
          subLabel="View current plan & upgrade"
          icon={<FontAwesome name="credit-card" size={20} color="#4096C1" />}
          onPress={() => console.log("Subscription Details Pressed")}
        />

        <SettingOption
          delay={BASE_DELAY * 5}
          label="Select/Change Therapist"
          subLabel="Select or change your therapist"
          icon={<Feather name="user" size={20} color="#4096C1" />}
          onPress={() => router.push("/(auth)/OnboardingFlow/selectAvatar")}
        />

        <SettingOption
          delay={BASE_DELAY * 5}
          label="Join Psyvatar community"
          subLabel="Join our discord community"
          icon={
            <MaterialCommunityIcons
              name="account-group-outline"
              size={20}
              color="#4096C1"
            />
          }
          onPress={() => Linking.openURL("https://discord.gg/dcBzhh5e")}
        />

        <AnimatedWrapper delay={BASE_DELAY * 6}>
          <Text style={styles.sectionTitle}>NOTIFICATIONS</Text>
        </AnimatedWrapper>
        <AnimatedWrapper delay={BASE_DELAY * 7}>
          <SettingToggle
            label="Push Notifications"
            subLabel="For daily updates and others."
            icon={<Feather name="bell" size={20} color="#4096C1" />}
            isEnabled={isPushEnabled}
            toggleSwitch={togglePushNotifications}
          />
        </AnimatedWrapper>
        {biometricLabel !== "" && (
          <AnimatedWrapper delay={BASE_DELAY * 8}>
            <SettingToggle
              label={biometricLabel}
              subLabel={`Enable or disable ${biometricLabel}`}
              icon={biometricIcon}
              isEnabled={isBiometricEnabled}
              toggleSwitch={toggleBiometricAuth}
            />
          </AnimatedWrapper>
        )}
        <AnimatedWrapper delay={BASE_DELAY * 9}>
          <Text style={styles.sectionTitle}>MORE</Text>
        </AnimatedWrapper>
        <SettingOption
          delay={BASE_DELAY * 10}
          label="Logout"
          icon={<Feather name="log-out" size={24} color="#4096C1" />}
          onPress={signOut}
        />

        <AnimatedWrapper delay={BASE_DELAY * 11}>
          <GlobalButton
            title="Delete Account"
            disabled={false}
            buttonColor={"red"}
            textColor={Colors.button.textLight}
            showIcon={false}
            onPress={handleDelete}
          />
        </AnimatedWrapper>
      </ScrollView>

      <Modal
        animationType="fade"
        transparent={true}
        visible={modalVisible}
        onRequestClose={() => setModalVisible(false)}
      >
        <View style={styles.centeredView}>
          <View style={styles.modalView}>
            <Image
              resizeMode="contain"
              style={{
                width: 200,
                height: 200,
                marginHorizontal: "auto",
              }}
              source={require("@/assets/images/DeleteAccountImage.png")}
            />
            <HeaderTitle title="You sure?" />
            <Subtitle
              subtitle="We suggest that you log out but if you insist on deleting your account we will be here if you need any mental health support."
              style={{
                marginTop: -10,
              }}
            />

            <GlobalButton
              title="Delete"
              disabled={isLoading}
              buttonColor={"#FB6C6C"}
              textColor={Colors.button.textLight}
              showIcon={false}
              onPress={deleteAccount}
              loading={isLoading}
            />

            <TouchableOpacity onPress={cancelDelete}>
              <Text style={styles.cancelText}>Cancel</Text>
            </TouchableOpacity>
          </View>
        </View>
      </Modal>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "white",
    paddingHorizontal: 16,
  },
  sectionTitle: {
    fontSize: 14,
    fontWeight: "600",
    color: "#4096C1",
    marginTop: 20,
    marginBottom: 10,
    fontFamily: globalStyle.font.fontFamilyMedium,
  },

  centeredView: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "rgba(0, 0, 0, 0.85)",
  },

  modalView: {
    width: "85%",
    backgroundColor: "white",
    borderRadius: 20,
    padding: 20,
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
  cancelText: {
    marginTop: 10,
    color: "#7B7B7A",
    fontFamily: globalStyle.font.fontFamilyBold,
    textAlign: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode
  • Biometric Toggle Setup: We check if Face ID or Touch ID is available and set up the toggle accordingly.

  • Dynamic Rendering: If biometric authentication is available, we render a SettingToggle component to allow users to enable or disable it.

  • Using Custom Hook: The enableBiometricAuth function from our useSettings hook is used to handle the toggle action.

  • UI Components: The settings screen includes various other settings options, and the biometric toggle is integrated seamlessly among them.

Integrating the Toggle Component

Ensure you have a SettingToggle component that accepts the necessary props:

// components/SettingToggle.tsx

import React from "react";
import { View, Text, StyleSheet, Switch } from "react-native";
import { globalStyle } from "@/constants/Colors";
import AnimatedWrapper from "@/components/shared/animation";

interface SettingToggleProps {
  label: string;
  subLabel?: string;
  icon?: JSX.Element;
  isEnabled: boolean;
  toggleSwitch: () => void;
  delay?: number;
}

export const SettingToggle = ({
  label,
  subLabel,
  icon,
  isEnabled,
  toggleSwitch,
  delay,
}: SettingToggleProps) => {
  return (
    <AnimatedWrapper delay={delay}>
      <View style={styles.optionContainer}>
        {icon && <View style={styles.optionIcon}>{icon}</View>}
        <View style={styles.optionTextContainer}>
          <Text style={styles.optionLabel}>{label}</Text>
          {subLabel && <Text style={styles.optionSubLabel}>{subLabel}</Text>}
        </View>
        <Switch
          trackColor={{ false: "#767577", true: "#4096C1" }}
          thumbColor="#f4f3f4"
          ios_backgroundColor="#3e3e3e"
          onValueChange={toggleSwitch}
          value={isEnabled}
          style={{ transform: [{ scaleX: 0.75 }, { scaleY: 0.75 }] }}
        />
      </View>
    </AnimatedWrapper>
  );
};

const styles = StyleSheet.create({
  optionContainer: {
    flexDirection: "row",
    alignItems: "center",
    paddingVertical: 15,
    borderBottomWidth: 1,
    borderBottomColor: "#EDEDED",
    marginVertical: 15,
  },
  optionIcon: {
    marginRight: 15,
  },
  optionTextContainer: {
    flex: 1,
  },
  optionLabel: {
    fontSize: 16,
    fontWeight: "500",
    fontFamily: globalStyle.font.fontFamilyMedium,
  },
  optionSubLabel: {
    fontSize: 12,
    color: "#7C7C7C",
    fontFamily: globalStyle.font.fontFamilyMedium,
  },
});
Enter fullscreen mode Exit fullscreen mode
  • Toggle Functionality: The Switch component is used to toggle biometric authentication on or off.

  • Dynamic Props: The component accepts dynamic props like label, subLabel, icon, isEnabled, and toggleSwitch.

  • Animation: An AnimatedWrapper is used for entry animations, enhancing the user experience.

Conclusion

Integrating Face ID and Touch ID into your React Native Expo app enhances security and provides a seamless user experience. By following this guide, you’ve learned how to:

  • Install and set up necessary packages.
  • Create a custom hook to manage biometric authentication settings.
  • Implement biometric options in your onboarding flow.
  • Enable biometric login in your login screen.
  • Add toggle switches in the settings screen to allow users to control biometric authentication.

At DETL, we believe in combining robust engineering with elegant design to create exceptional software solutions. Implementing biometric authentication is just one way to elevate your app’s user experience while maintaining high security standards.

Thank you for reading! If you’re interested in more tips and tutorials on building high-quality applications, stay tuned to our blog at DETL.

Top comments (0)