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
expo install expo-local-authentication expo-secure-store expo-checkbox @react-native-async-storage/async-storage
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,
};
};
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,
},
});
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");
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,
},
});
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",
},
});
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:
// 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,
},
});
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.
Top comments (0)