
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
- Required Packages to install: The essential packages you need to install.
- Custom Hook Creation (useSetting.tsx): How to create a custom hook for managing biometric authentication.
- Onboarding Implementation: Integrating Face ID and Touch ID in the onboarding screen.
- Login Screen Integration: Implementing biometric login in your login screen.
- 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.
- expo-local-authentication: Provides access to biometric authentication capabilities.
expo install expo-local-authentication
- expo-secure-store: Allows you to securely store sensitive data like user credentials.
expo install expo-secure-store
- @react-native-async-storage/async-storage: A simple, unencrypted, asynchronous storage system.
yarn add @react-native-async-storage/async-storage
. - expo-checkbox: A customizable checkbox component.
expo install expo-checkbox
- 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:
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
:
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
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:
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:
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 ouruseSettings
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:
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
, andtoggleSwitch
. - 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.