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

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

1expo install expo-local-authentication expo-secure-store expo-checkbox @react-native-async-storage/async-storage

2. Creating a Custom Hook

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:

useSettings.tsxjsx
1import { useState, useEffect } from 'react';
2import AsyncStorage from '@react-native-async-storage/async-storage';
3import * as LocalAuthentication from 'expo-local-authentication';
4import { useToast } from '@/providers/useToast';
5
6interface BiometricAuthStatus {
7  isFaceIDEnabled: boolean;
8  isTouchIDEnabled: boolean;
9}
10
11export const useSettings = () => {
12  const [biometricAuth, setBiometricAuth] = useState<BiometricAuthStatus>({
13    isFaceIDEnabled: false,
14    isTouchIDEnabled: false,
15  });
16
17  const [availableBiometrics, setAvailableBiometrics] = useState<LocalAuthentication.AuthenticationType[]>([]);
18
19  const { showToast } = useToast();
20
21  const checkAvailableBiometrics = async () => {
22    try {
23      const biometrics = await LocalAuthentication.supportedAuthenticationTypesAsync();
24      setAvailableBiometrics(biometrics);
25    } catch (error) {
26      console.log(error);
27    }
28  };
29
30  const enableBiometricAuth = async (biometricType: 'FaceID' | 'TouchID'): Promise<boolean> => {
31    try {
32      const isBiometricAvailable = await LocalAuthentication.hasHardwareAsync();
33      if (!isBiometricAvailable) {
34        showToast('error', 'Your device does not support biometric authentication.');
35        return false;
36      }
37
38      const savedBiometric = await LocalAuthentication.isEnrolledAsync();
39      if (!savedBiometric) {
40        showToast('error', 'No biometric records found.');
41        return false;
42      }
43
44      const result = await LocalAuthentication.authenticateAsync({
45        promptMessage: 'Authenticate with biometrics',
46        fallbackLabel: 'Enter password',
47      });
48
49      if (result.success) {
50        setBiometricAuth((prevState) => {
51          const updatedState = { ...prevState };
52          if (biometricType === 'FaceID') {
53            updatedState.isFaceIDEnabled = !prevState.isFaceIDEnabled;
54            showToast(
55              'success',
56              `Face ID ${updatedState.isFaceIDEnabled ? 'enabled' : 'disabled'} successfully.`
57            );
58          } else if (biometricType === 'TouchID') {
59            updatedState.isTouchIDEnabled = !prevState.isTouchIDEnabled;
60            showToast(
61              'success',
62              `Touch ID ${updatedState.isTouchIDEnabled ? 'enabled' : 'disabled'} successfully.`
63            );
64          }
65          // Save the updated status
66          AsyncStorage.setItem('biometricAuthStatus', JSON.stringify(updatedState));
67          return updatedState;
68        });
69
70        return true;
71      } else {
72        showToast('error', 'Authentication failed.');
73        return false;
74      }
75    } catch (error) {
76      console.log(error);
77      showToast('error', 'An error occurred while enabling biometric authentication.');
78      return false;
79    }
80  };
81
82  const checkIfBiometricEnabled = async (): Promise<void> => {
83    const biometricStatus = await AsyncStorage.getItem('biometricAuthStatus');
84    if (biometricStatus) {
85      const parsedStatus: BiometricAuthStatus = JSON.parse(biometricStatus);
86      setBiometricAuth(parsedStatus);
87    } else {
88      setBiometricAuth({
89        isFaceIDEnabled: false,
90        isTouchIDEnabled: false,
91      });
92    }
93  };
94
95  useEffect(() => {
96    checkAvailableBiometrics();
97    checkIfBiometricEnabled();
98  }, []);
99
100  return {
101    biometricAuth,
102    enableBiometricAuth,
103    checkIfBiometricEnabled,
104    availableBiometrics,
105  };
106};

  • 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:

FaceID_TouchID.tsxtsx
1import React from "react";
2import { View, Image, Text, StyleSheet, TouchableOpacity } from "react-native";
3import GlobalButton from "@/components/shared/Button";
4import Header from "@/components/shared/Header";
5import HeaderTitle from "@/components/shared/HeaderTitle";
6import Subtitle from "@/components/shared/Subtitle";
7import { Colors, globalStyle } from "@/constants/Colors";
8import { useRouter } from "expo-router";
9import { useSettings } from "@/hooks/settings/useSettings";
10import * as LocalAuthentication from "expo-local-authentication";
11
12const FaceID_TouchID = () => {
13  const { biometricAuth, enableBiometricAuth, availableBiometrics } = useSettings();
14  const router = useRouter();
15
16  const isFaceIdAvailable = availableBiometrics.includes(
17    LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION
18  );
19
20  const isTouchIDAvailable = availableBiometrics.includes(
21    LocalAuthentication.AuthenticationType.FINGERPRINT
22  );
23
24  const isAnyBiometricAvailable = isFaceIdAvailable || isTouchIDAvailable;
25
26  const handleEnableBiometricAuth = async (biometricType: 'FaceID' | 'TouchID') => {
27    const success = await enableBiometricAuth(biometricType);
28    if (success) {
29      router.push("/(auth)/OnboardingFlow/NextOnboardingScreenAfterThisOne");
30    }
31  };
32
33  return (
34    <>
35      <Header title="Enable Biometric Authentication" />
36      <HeaderTitle title={`Easy Access\nBetter Support`} />
37      <View style={styles.container}>
38        {isAnyBiometricAvailable ? (
39          <>
40            {isFaceIdAvailable && (
41              <Image
42                source={require("@/assets/images/onboarding/FaceIDImage.png")}
43                resizeMode="contain"
44                style={styles.image}
45              />
46            )}
47            {isTouchIDAvailable && (
48              <Image
49                source={require("@/assets/images/TouchID.png")}
50                resizeMode="contain"
51                style={styles.image}
52              />
53            )}
54          </>
55        ) : (
56          <Text style={styles.notAvailableText}>
57            Biometric authentication is not available on this device.
58          </Text>
59        )}
60        {isAnyBiometricAvailable && (
61          <Subtitle
62            style={{ marginTop: 30 }}
63            subtitle="Enable biometric authentication for quick, secure, and effortless access to your support whenever you need it."
64          />
65        )}
66      </View>
67      <View style={styles.buttonContainer}>
68        {isAnyBiometricAvailable && (
69          <>
70            {isFaceIdAvailable && (
71              <GlobalButton
72                title={
73                  biometricAuth.isFaceIDEnabled
74                    ? "Face ID Enabled"
75                    : "Enable Face ID"
76                }
77                disabled={false}
78                buttonColor={Colors.button.backgroundButtonDark}
79                textColor={Colors.button.textLight}
80                showIcon={false}
81                onPress={() => handleEnableBiometricAuth("FaceID")}
82              />
83            )}
84            {isTouchIDAvailable && (
85              <GlobalButton
86                title={
87                  biometricAuth.isTouchIDEnabled
88                    ? "Touch ID Enabled"
89                    : "Enable Touch ID"
90                }
91                disabled={false}
92                buttonColor={Colors.button.backgroundButtonDark}
93                textColor={Colors.button.textLight}
94                showIcon={false}
95                onPress={() => handleEnableBiometricAuth("TouchID")}
96              />
97            )}
98          </>
99        )}
100      </View>
101      <TouchableOpacity
102        style={styles.skipButton}
103        onPress={() => router.push("/(auth)/OnboardingFlow/screenThirteen")}
104      >
105        <Text style={styles.skipText}>
106          {isAnyBiometricAvailable ? "Enable later" : "Skip"}
107        </Text>
108      </TouchableOpacity>
109    </>
110  );
111};
112
113export default FaceID_TouchID;
114
115const styles = StyleSheet.create({
116  container: {
117    flex: 0.9,
118    alignItems: "center",
119    justifyContent: "center",
120  },
121  image: {
122    height: 250,
123    width: 250,
124  },
125  buttonContainer: {
126    marginHorizontal: globalStyle.container.marginHorizontal,
127    marginBottom: 10,
128  },
129  skipButton: {
130    justifyContent: "center",
131    alignItems: "center",
132  },
133  skipText: {
134    textAlign: "center",
135    fontFamily: globalStyle.font.fontFamilyBold,
136    color: Colors.button.textDark,
137  },
138  notAvailableText: {
139    textAlign: "center",
140    fontFamily: globalStyle.font.fontFamilyMedium,
141    color: Colors.button.textDark,
142  },
143});


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

FaceID_TouchID.tsxjsx
1router.push("/(auth)/OnboardingFlow/NextOnboardingScreenAfterThisOne");

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:

Login.tsxtsx
1import React, { useState, useEffect, useRef } from "react";
2import { View, Text, StyleSheet, ScrollView, Keyboard } from "react-native";
3import * as SecureStore from "expo-secure-store";
4import * as LocalAuthentication from "expo-local-authentication";
5import AsyncStorage from "@react-native-async-storage/async-storage";
6import { Feather } from "@expo/vector-icons";
7import { useLocalSearchParams } from "expo-router";
8import GlobalButton from "@/components/shared/Button";
9import CustomTextInput from "@/components/shared/CustomTextInput";
10import Header from "@/components/shared/Header";
11import HeaderTitle from "@/components/shared/HeaderTitle";
12import PasswordStrengthDisplay from "@/components/shared/PasswordStrengthDisplay";
13import Checkbox from "expo-checkbox";
14import { Colors, globalStyle } from "@/constants/Colors";
15import { useSession } from "@/providers/Auth/AuthProvider";
16import { useToast } from "@/providers/useToast";
17import PsyvatarLogo from "@/assets/images/Logo";
18
19const Login = () => {
20  const { params } = useLocalSearchParams();
21  const { showToast } = useToast();
22  const { signIn, isLoading, signUp } = useSession();
23
24  const [email, setEmail] = useState("");
25  const [password, setPassword] = useState("");
26  const [name, setName] = useState("");
27  const [securePassword, setSecurePassword] = useState(true);
28  const [isChecked, setChecked] = useState(false);
29
30  const [isBiometricSupported, setIsBiometricSupported] = useState(false);
31  const [biometricType, setBiometricType] = useState("");
32  const [isBiometricEnabled, setIsBiometricEnabled] = useState(false);
33
34  const emailRef = useRef(null);
35  const passwordRef = useRef(null);
36  const nameRef = useRef(null);
37
38  const isInvalidRegister = name === "" || email === "" || password === "";
39  const isInvalidLogin = email === "" || password === "";
40
41  useEffect(() => {
42    setTimeout(() => {
43      if (params === "register") {
44        nameRef?.current?.focus();
45      } else {
46        emailRef?.current?.focus();
47      }
48    }, 800);
49
50    const getEmailFromStore = async () => {
51      try {
52        const storedEmail = await SecureStore.getItemAsync("userEmail");
53        if (storedEmail) {
54          setEmail(storedEmail);
55          setChecked(true);
56        }
57      } catch (error) {
58        console.log("Error fetching email from SecureStore", error);
59      }
60    };
61
62    const checkBiometricSupport = async () => {
63      const compatible = await LocalAuthentication.hasHardwareAsync();
64      setIsBiometricSupported(compatible);
65
66      if (compatible) {
67        const savedBiometrics = await LocalAuthentication.isEnrolledAsync();
68        if (savedBiometrics) {
69          const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
70          if (
71            types.includes(
72              LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION
73            )
74          ) {
75            setBiometricType("Face ID");
76          } else if (
77            types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)
78          ) {
79            setBiometricType("Touch ID");
80          } else {
81            setBiometricType("Biometrics");
82          }
83        }
84      }
85
86      const biometricEnabled = await AsyncStorage.getItem("biometricAuthStatus");
87      if (biometricEnabled) {
88        const parsedStatus = JSON.parse(biometricEnabled);
89        setIsBiometricEnabled(
90          parsedStatus.isFaceIDEnabled || parsedStatus.isTouchIDEnabled
91        );
92      }
93    };
94
95    getEmailFromStore();
96    checkBiometricSupport();
97  }, []);
98
99  const handleRegister = () => {
100    if (params === "register" && isInvalidRegister) {
101      return showToast("warning", "One or more fields are missing");
102    }
103    signUp(email, password, name);
104  };
105
106  const handleLogin = async () => {
107    if (params === "login" && isInvalidLogin) {
108      return showToast("warning", "One or more fields are missing");
109    }
110    Keyboard.dismiss();
111
112    const loginSuccess = await signIn(email, password);
113
114    if (loginSuccess) {
115      if (isChecked) {
116        try {
117          await SecureStore.setItemAsync("userEmail", email);
118        } catch (error) {
119          console.log("Error saving email to SecureStore", error);
120        }
121      } else {
122        await SecureStore.deleteItemAsync("userEmail");
123      }
124
125      if (isBiometricEnabled) {
126        try {
127          await SecureStore.setItemAsync("userEmail", email);
128          await SecureStore.setItemAsync("userPassword", password);
129        } catch (error) {
130          console.log("Error saving credentials to SecureStore", error);
131        }
132      }
133    }
134  };
135
136  const handleBiometricLogin = async () => {
137    try {
138      const result = await LocalAuthentication.authenticateAsync({
139        promptMessage: `Login with ${biometricType}`,
140        fallbackLabel: "Enter password",
141      });
142
143      if (result.success) {
144        const storedEmail = await SecureStore.getItemAsync("userEmail");
145        const storedPassword = await SecureStore.getItemAsync("userPassword");
146
147        if (storedEmail && storedPassword) {
148          await signIn(storedEmail, storedPassword);
149        } else {
150          showToast("error", "No credentials found. Please log in manually.");
151          setIsBiometricSupported(false);
152        }
153      } else {
154        showToast("error", "Biometric authentication failed.");
155        setIsBiometricSupported(false);
156      }
157    } catch (error) {
158      console.log("Biometric authentication error:", error);
159      showToast("error", "An error occurred during biometric authentication.");
160      setIsBiometricSupported(false);
161    }
162  };
163
164  const getButtonProps = () => {
165    if (params === "login" && isBiometricEnabled && isBiometricSupported) {
166      return {
167        title: `Login with ${biometricType}`,
168        onPress: handleBiometricLogin,
169      };
170    } else if (params === "login") {
171      return {
172        title: "Sign In",
173        onPress: handleLogin,
174      };
175    } else {
176      return {
177        title: "Sign Up",
178        onPress: handleRegister,
179      };
180    }
181  };
182
183  const { title, onPress } = getButtonProps();
184
185  return (
186    <>
187      <Header logo={<Logo />} BackButton />
188      <HeaderTitle
189        title={`${params === "register" ? "Welcome!" : "Welcome Back!"}`}
190      />
191      <View style={styles.container}>
192        <ScrollView
193          style={styles.container}
194          showsVerticalScrollIndicator={false}
195        >
196          {params === "register" && (
197            <CustomTextInput
198              keyboardType="default"
199              label="Name"
200              placeholder="Your name"
201              leftIcon={
202                <Feather
203                  name="user"
204                  size={Colors.button.iconSize}
205                  color={Colors.button.iconColorDark}
206                />
207              }
208              secureTextEntry={false}
209              onChangeText={(text) => setName(text)}
210              value={name}
211              height={50}
212              ref={nameRef}
213              onSubmitEditing={() => emailRef?.current?.focus()}
214            />
215          )}
216          <CustomTextInput
217            ref={emailRef}
218            keyboardType="email-address"
219            label="Email address"
220            placeholder="Enter your email"
221            leftIcon={
222              <Feather
223                name="mail"
224                size={Colors.button.iconSize}
225                color={Colors.button.iconColorDark}
226              />
227            }
228            secureTextEntry={false}
229            onChangeText={setEmail}
230            value={email}
231            autoCapitalize="none"
232            height={50}
233            onSubmitEditing={() => passwordRef?.current?.focus()}
234          />
235          <CustomTextInput
236            ref={passwordRef}
237            value={password}
238            keyboardType="visible-password"
239            label="Password"
240            placeholder="Enter your password"
241            leftIcon={
242              <Feather
243                name="lock"
244                size={Colors.button.iconSize}
245                color={Colors.button.iconColorDark}
246              />
247            }
248            rightIcon={
249              <Feather
250                name={securePassword ? "eye-off" : "eye"}
251                size={Colors.button.iconSize}
252                color={Colors.button.iconColorDark}
253              />
254            }
255            rightIconPressable={() => setSecurePassword(!securePassword)}
256            secureTextEntry={securePassword}
257            onChangeText={setPassword}
258            autoCapitalize="none"
259            height={50}
260            onSubmitEditing={() => Keyboard.dismiss()}
261          />
262
263          {params === "register" && (
264            <PasswordStrengthDisplay password={password} />
265          )}
266
267          <View style={styles.rememberMeContainer}>
268            <Checkbox
269              style={styles.checkbox}
270              value={isChecked}
271              onValueChange={setChecked}
272              color={isChecked ? "#4096C1" : undefined}
273            />
274            <Text style={styles.rememberText}>Remember me</Text>
275          </View>
276        </ScrollView>
277
278        <View style={styles.buttonWrapper}>
279          <GlobalButton
280            title={title}
281            onPress={onPress}
282            disabled={isLoading}
283            loading={isLoading}
284            buttonColor={Colors.button.backgroundButtonDark}
285            textColor={Colors.button.textLight}
286            showIcon={false}
287          />
288
289       {params === "login" && isBiometricEnabled && isBiometricSupported && (
290            <TouchableOpacity
291              onPress={() => setIsBiometricSupported(false)}
292              style={{
293                margin: 10,
294              }}
295            >
296              <Text
297                style={{
298                  textAlign: "center",
299                  fontFamily: globalStyle.font.fontFamilyBold,
300                  color: Colors.button.textDark,
301                }}
302              >
303                Sign In manually
304              </Text>
305            </TouchableOpacity>
306          )}
307        </View>
308      </View>
309    </>
310  );
311};
312
313export default Login;
314
315const styles = StyleSheet.create({
316  container: {
317    flex: 1,
318    marginHorizontal: globalStyle.container.marginHorizontal,
319  },
320  rememberText: {
321    color: "#282545",
322    fontWeight: "400",
323    fontSize: 14,
324  },
325  rememberMeContainer: {
326    marginVertical: 10,
327    marginBottom: 0,
328    flexDirection: "row",
329    alignItems: "center",
330  },
331  checkbox: {
332    margin: 8,
333    borderRadius: 4,
334  },
335  buttonWrapper: {
336    marginHorizontal: globalStyle.container.marginHorizontal,
337    marginBottom: 15,
338  },
339});

  • 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:

SettingsScreen.tsxtsx
1import React, { useState } from "react";
2import {
3  View,
4  Text,
5  StyleSheet,
6  ScrollView,
7  TouchableOpacity,
8  Image,
9  Modal,
10  Linking,
11} from "react-native";
12import { Feather, FontAwesome } from "@expo/vector-icons";
13import { SettingOption, SettingToggle } from "./components";
14import { useSession } from "@/providers/Auth/AuthProvider";
15import GlobalButton from "@/components/shared/Button";
16import { Colors, globalStyle } from "@/constants/Colors";
17import HeaderTitle from "@/components/shared/HeaderTitle";
18import Subtitle from "@/components/shared/Subtitle";
19import AnimatedWrapper from "@/components/shared/animation";
20import { useRouter } from "expo-router";
21import { useSettings } from "@/hooks/settings/useSettings";
22import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
23import { useHandleApiError } from "@/hooks/settings/useHandleApiError";
24import * as LocalAuthentication from "expo-local-authentication";
25
26export const BASE_DELAY = 50;
27
28export default function SettingsScreen() {
29  const { signOut, deleteUserRecord, isLoading } = useSession();
30  const { biometricAuth, enableBiometricAuth, availableBiometrics } = useSettings();
31
32  const isFaceIDAvailable = availableBiometrics.includes(
33    LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION
34  );
35
36  const isTouchIDAvailable = availableBiometrics.includes(
37    LocalAuthentication.AuthenticationType.FINGERPRINT
38  );
39
40  // Set up biometric toggle based on availability
41  let biometricLabel = "";
42  let isBiometricEnabled = false;
43  let toggleBiometricAuth = null;
44  let biometricIcon = null;
45
46  if (isFaceIDAvailable) {
47    biometricLabel = "Face ID";
48    isBiometricEnabled = biometricAuth.isFaceIDEnabled;
49    toggleBiometricAuth = () => enableBiometricAuth("FaceID");
50    biometricIcon = (
51      <MaterialCommunityIcons
52        name="face-recognition"
53        size={20}
54        color="#4096C1"
55      />
56    );
57  } else if (isTouchIDAvailable) {
58    biometricLabel = "Touch ID";
59    isBiometricEnabled = biometricAuth.isTouchIDEnabled;
60    toggleBiometricAuth = () => enableBiometricAuth("TouchID");
61    biometricIcon = (
62      <MaterialCommunityIcons name="fingerprint" size={20} color="#4096C1" />
63    );
64  }
65
66  const router = useRouter();
67
68  const [isPushEnabled, setIsPushEnabled] = useState(false);
69
70  const togglePushNotifications = () => setIsPushEnabled(!isPushEnabled);
71
72  const [modalVisible, setModalVisible] = useState(false);
73  const handleApiError = useHandleApiError();
74
75  const handleDelete = () => {
76    setModalVisible(true);
77  };
78
79  const deleteAccount = async () => {
80    try {
81      const response = await deleteUserRecord();
82      if (response.success) {
83        setTimeout(() => {
84          setModalVisible(!modalVisible);
85        }, 500);
86      } else {
87        handleApiError(response);
88      }
89    } catch (error) {
90      console.log(error);
91    }
92  };
93
94  const cancelDelete = () => {
95    setModalVisible(!modalVisible);
96  };
97
98  return (
99    <View style={styles.container}>
100      <ScrollView showsVerticalScrollIndicator={false}>
101        <AnimatedWrapper delay={BASE_DELAY * 1}>
102          <Text style={styles.sectionTitle}>GENERAL</Text>
103        </AnimatedWrapper>
104        <SettingOption
105          delay={BASE_DELAY * 2}
106          label="Get Help"
107          icon={
108            <FontAwesome name="question-circle-o" size={20} color="#4096C1" />
109          }
110          onPress={() => router.navigate("/(auth)/help")}
111        />
112        <SettingOption
113          delay={BASE_DELAY * 3}
114          label="Contact Us"
115          icon={<Feather name="mail" size={20} color="#4096C1" />}
116          onPress={() => router.navigate("/(auth)/help/contact_us")}
117        />
118        <SettingOption
119          delay={BASE_DELAY * 4}
120          label="Subscription Details"
121          subLabel="View current plan & upgrade"
122          icon={<FontAwesome name="credit-card" size={20} color="#4096C1" />}
123          onPress={() => console.log("Subscription Details Pressed")}
124        />
125
126        <SettingOption
127          delay={BASE_DELAY * 5}
128          label="Select/Change Therapist"
129          subLabel="Select or change your therapist"
130          icon={<Feather name="user" size={20} color="#4096C1" />}
131          onPress={() => router.push("/(auth)/OnboardingFlow/selectAvatar")}
132        />
133
134        <SettingOption
135          delay={BASE_DELAY * 5}
136          label="Join Psyvatar community"
137          subLabel="Join our discord community"
138          icon={
139            <MaterialCommunityIcons
140              name="account-group-outline"
141              size={20}
142              color="#4096C1"
143            />
144          }
145          onPress={() => Linking.openURL("https://discord.gg/dcBzhh5e")}
146        />
147
148        <AnimatedWrapper delay={BASE_DELAY * 6}>
149          <Text style={styles.sectionTitle}>NOTIFICATIONS</Text>
150        </AnimatedWrapper>
151        <AnimatedWrapper delay={BASE_DELAY * 7}>
152          <SettingToggle
153            label="Push Notifications"
154            subLabel="For daily updates and others."
155            icon={<Feather name="bell" size={20} color="#4096C1" />}
156            isEnabled={isPushEnabled}
157            toggleSwitch={togglePushNotifications}
158          />
159        </AnimatedWrapper>
160        {biometricLabel !== "" && (
161          <AnimatedWrapper delay={BASE_DELAY * 8}>
162            <SettingToggle
163              label={biometricLabel}
164              subLabel={`Enable or disable ${biometricLabel}`}
165              icon={biometricIcon}
166              isEnabled={isBiometricEnabled}
167              toggleSwitch={toggleBiometricAuth}
168            />
169          </AnimatedWrapper>
170        )}
171        <AnimatedWrapper delay={BASE_DELAY * 9}>
172          <Text style={styles.sectionTitle}>MORE</Text>
173        </AnimatedWrapper>
174        <SettingOption
175          delay={BASE_DELAY * 10}
176          label="Logout"
177          icon={<Feather name="log-out" size={24} color="#4096C1" />}
178          onPress={signOut}
179        />
180
181        <AnimatedWrapper delay={BASE_DELAY * 11}>
182          <GlobalButton
183            title="Delete Account"
184            disabled={false}
185            buttonColor={"red"}
186            textColor={Colors.button.textLight}
187            showIcon={false}
188            onPress={handleDelete}
189          />
190        </AnimatedWrapper>
191      </ScrollView>
192
193      <Modal
194        animationType="fade"
195        transparent={true}
196        visible={modalVisible}
197        onRequestClose={() => setModalVisible(false)}
198      >
199        <View style={styles.centeredView}>
200          <View style={styles.modalView}>
201            <Image
202              resizeMode="contain"
203              style={{
204                width: 200,
205                height: 200,
206                marginHorizontal: "auto",
207              }}
208              source={require("@/assets/images/DeleteAccountImage.png")}
209            />
210            <HeaderTitle title="You sure?" />
211            <Subtitle
212              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."
213              style={{
214                marginTop: -10,
215              }}
216            />
217
218            <GlobalButton
219              title="Delete"
220              disabled={isLoading}
221              buttonColor={"#FB6C6C"}
222              textColor={Colors.button.textLight}
223              showIcon={false}
224              onPress={deleteAccount}
225              loading={isLoading}
226            />
227
228            <TouchableOpacity onPress={cancelDelete}>
229              <Text style={styles.cancelText}>Cancel</Text>
230            </TouchableOpacity>
231          </View>
232        </View>
233      </Modal>
234    </View>
235  );
236}
237
238const styles = StyleSheet.create({
239  container: {
240    flex: 1,
241    backgroundColor: "white",
242    paddingHorizontal: 16,
243  },
244  sectionTitle: {
245    fontSize: 14,
246    fontWeight: "600",
247    color: "#4096C1",
248    marginTop: 20,
249    marginBottom: 10,
250    fontFamily: globalStyle.font.fontFamilyMedium,
251  },
252
253  centeredView: {
254    flex: 1,
255    justifyContent: "center",
256    alignItems: "center",
257    backgroundColor: "rgba(0, 0, 0, 0.85)",
258  },
259
260  modalView: {
261    width: "85%",
262    backgroundColor: "white",
263    borderRadius: 20,
264    padding: 20,
265    shadowColor: "#000",
266    shadowOffset: {
267      width: 0,
268      height: 2,
269    },
270    shadowOpacity: 0.25,
271    shadowRadius: 4,
272    elevation: 5,
273  },
274  cancelText: {
275    marginTop: 10,
276    color: "#7B7B7A",
277    fontFamily: globalStyle.font.fontFamilyBold,
278    textAlign: "center",
279  },
280});

  • 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:

SettingToggle.tsxtsx
1// components/SettingToggle.tsx
2
3import React from "react";
4import { View, Text, StyleSheet, Switch } from "react-native";
5import { globalStyle } from "@/constants/Colors";
6import AnimatedWrapper from "@/components/shared/animation";
7
8interface SettingToggleProps {
9  label: string;
10  subLabel?: string;
11  icon?: JSX.Element;
12  isEnabled: boolean;
13  toggleSwitch: () => void;
14  delay?: number;
15}
16
17export const SettingToggle = ({
18  label,
19  subLabel,
20  icon,
21  isEnabled,
22  toggleSwitch,
23  delay,
24}: SettingToggleProps) => {
25  return (
26    <AnimatedWrapper delay={delay}>
27      <View style={styles.optionContainer}>
28        {icon && <View style={styles.optionIcon}>{icon}</View>}
29        <View style={styles.optionTextContainer}>
30          <Text style={styles.optionLabel}>{label}</Text>
31          {subLabel && <Text style={styles.optionSubLabel}>{subLabel}</Text>}
32        </View>
33        <Switch
34          trackColor={{ false: "#767577", true: "#4096C1" }}
35          thumbColor="#f4f3f4"
36          ios_backgroundColor="#3e3e3e"
37          onValueChange={toggleSwitch}
38          value={isEnabled}
39          style={{ transform: [{ scaleX: 0.75 }, { scaleY: 0.75 }] }}
40        />
41      </View>
42    </AnimatedWrapper>
43  );
44};
45
46const styles = StyleSheet.create({
47  optionContainer: {
48    flexDirection: "row",
49    alignItems: "center",
50    paddingVertical: 15,
51    borderBottomWidth: 1,
52    borderBottomColor: "#EDEDED",
53    marginVertical: 15,
54  },
55  optionIcon: {
56    marginRight: 15,
57  },
58  optionTextContainer: {
59    flex: 1,
60  },
61  optionLabel: {
62    fontSize: 16,
63    fontWeight: "500",
64    fontFamily: globalStyle.font.fontFamilyMedium,
65  },
66  optionSubLabel: {
67    fontSize: 12,
68    color: "#7C7C7C",
69    fontFamily: globalStyle.font.fontFamilyMedium,
70  },
71});

  • 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.

Drag