In this tutorial I show you what I learned along my journey to making a simple test application to better learn Firebase and React Native. We will start a project with expo-routing. Then build the UI with react-native-paper. Then connect the UI to Firebase Authentication.
To get started I want a basic React Native template that uses the expo-router package. To do that we can use their template to get started:
npx create-expo-app@latest --example with-router
After creating the project we need to use expo-router to set up our files and folder structure. The entry point for expo router is a folder named "app". At the same time we will also set up that app to use react-native-paper as our component library. First install react-native-paper.
npm install react-native-paper
Now we need to create a theme for the project. I like to keep this in a constants folder named theme.
mkdir constants
touch constants/theme.js
Open up theme.js and add the code to get started with a simple dark theme.
import { DefaultTheme } from "react-native-paper";
export const theme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary: "#5eada6",
secondary: "#ea568c",
text: "#eee",
background: "#303030",
paper: "#424242",
},
};
We now need to set up the entry point into the application. expo-router default is set up to be at "app/index.js". We want to do some routing in the application so instead of an index.js we will make a _layout.js file that describes the stack to expo.
Create the file:
mkdir app
touch app/_layouts.js
Because this is the entry point of the entire app, it is a good place to set up our theme from paper. So we will import that and have the react-native-paper provider encompass out entire application. Then we will set up two different navigation pathways, (auth) and (tabs). Where auth handles the login, register and resetPassword pages and tabs would be the rest of the application in this example.
import { Stack } from "expo-router";
import { Provider as PaperProvider } from "react-native-paper";
import { theme } from "../constants/theme";
export default function RootLayout() {
return (
<PaperProvider theme={theme}>
<Stack
screenOptions={{
headerStyle: { backgroundColor: theme.colors.background },
headerTintColor: "#ffffff",
}}
>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(app)" options={{ headerShown: false }} />
</Stack>
</PaperProvider>
);
}
Now that we have the entry point for the application set up. I think it would be a good time to explain the folder structure for the rest of the project. Since we have to navigation pathways, (auth) and (tabs). This means we will need a _layout.js in each directory and then we will also put the corresponding pages in their directories as well. The completed files and folder structure will look like this.
Let start's here by creating the navigation components for auth and tabs.
The auth navigation will be:
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
import { Tabs } from "expo-router";
import { useTheme } from "react-native-paper";
export default function AuthTabLayout() {
const theme = useTheme();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: theme.colors.primary,
tabBarStyle: { backgroundColor: theme.colors.background },
headerStyle: { backgroundColor: theme.colors.background },
headerTintColor: "#ffffff",
}}
>
<Tabs.Screen
name="index"
options={{
title: "Login",
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
size={28}
style={{ marginBottom: -3 }}
color={color}
name="login"
/>
),
}}
/>
<Tabs.Screen
name="register"
options={{
title: "Register",
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
size={28}
style={{ marginBottom: -3 }}
color={color}
name="account-plus"
/>
),
}}
/>
<Tabs.Screen
name="resetPassword"
options={{
title: "Reset Password",
href: null,
}}
/>
</Tabs>
);
}
The tabs navigator will be very similar to the auth one. But here we want to add a button in the header right to allow users to logout. To sign out a user in firebase we can import the signOut.
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
import { Tabs, useRouter } from "expo-router";
import { TouchableOpacity } from "react-native-gesture-handler";
import { useTheme } from "react-native-paper";
import { signOut } from "firebase/auth";
import { auth } from "../../firebaseConfig";
export default function AppTabLayout() {
const theme = useTheme();
const router = useRouter();
const onLogoutHander = async () => {
await signOut(auth);
router.replace("/");
};
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: theme.colors.primary,
tabBarStyle: { backgroundColor: theme.colors.background },
headerStyle: { backgroundColor: theme.colors.background },
headerTintColor: "#ffffff",
}}
>
<Tabs.Screen
name="index"
options={{
title: "One",
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
size={28}
style={{ marginBottom: -3 }}
color={color}
name="numeric-1-box"
/>
),
headerRight: () => (
<TouchableOpacity onPress={onLogoutHander}>
<MaterialCommunityIcons
size={28}
style={{ marginBottom: -3 }}
color="#fff"
name="logout"
/>
</TouchableOpacity>
),
}}
/>
<Tabs.Screen
name="screen2"
options={{
title: "Two",
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
size={28}
style={{ marginBottom: -3 }}
color={color}
name="numeric-2-box"
/>
),
}}
/>
</Tabs>
);
}
Since we haven't set up the firebase config file let's do that now. You can go to https://console.firebase.google.com/u/0/ then start a new project. Once you have the project set up go to the authentication tab and select get started. There will be several option in this example we are using the setup authentication with email and password option. After you have that you van add your information to the config file. I called my file firebaseConfig.js but you can name this anything.
// Import the functions you need from the SDKs you need
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: '',
authDomain: '',
projectId: '',
storageBucket: '',
messagingSenderId: '',
appId: '',
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const firestore = getFirestore();
export const auth = getAuth(app);
Now that Authentication is enabled and we have set up our firebase files lets get to the login, register and reset password screens.
For the register screen we want to use two specific function for the firebase/auth:
import { createUserWithEmailAndPassword, updateProfile } from "firebase/auth";
With the createUserWithEmailAndPassword, we can generate a new user with a unqiue id when we send firebase an email and a password. After the user is created we can update the user profile with the updateProfile function. In this example we are setting the displayName to the users name. Here is what the completed register component looks like:
import { createUserWithEmailAndPassword, updateProfile } from "firebase/auth";
import * as React from "react";
import { StyleSheet } from "react-native";
import { TextInput } from "react-native-paper";
import { Text, View, Button } from "../../components/Themed";
import ErrorDisplay from "../../components/ErrorDisplay";
import { auth } from "../../firebaseConfig";
import { useRouter } from "expo-router";
export default function Register() {
const [name, setName] = React.useState("");
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [confirmPassword, setConfirmPassword] = React.useState("");
const [passwordVisible, setPasswordVisible] = React.useState(true);
const [error, setError] = React.useState("");
const [loading, setLoading] = React.useState(false);
const router = useRouter();
const registerUser = async (name) => {
setError("");
setLoading(true);
try {
const res = await createUserWithEmailAndPassword(auth, email, password);
if (auth.currentUser)
await updateProfile(auth.currentUser, {
displayName: name.toLowerCase(),
});
router.push("/(tabs)");
setLoading(false);
} catch (error) {
setError(error.message);
setLoading(false);
}
};
return (
<View style={styles.container}>
<View>
<Text>Name</Text>
<TextInput
placeholder="Name"
value={name}
onChangeText={(text) => setName(text)}
/>
</View>
<View>
<Text>Email</Text>
<TextInput
placeholder="Email"
value={email}
onChangeText={(text) => setEmail(text)}
/>
</View>
<View>
<Text>Password</Text>
<TextInput
secureTextEntry={passwordVisible}
placeholder="Password"
value={password}
onChangeText={(text) => setPassword(text)}
right={
<TextInput.Icon
icon="eye"
onPress={() => setPasswordVisible((p) => !p)}
/>
}
/>
</View>
<View>
<Text>Confirm Password</Text>
<TextInput
secureTextEntry={passwordVisible}
placeholder="Confirm Password"
value={confirmPassword}
onChangeText={(text) => setConfirmPassword(text)}
right={
<TextInput.Icon
icon="eye"
onPress={() => setPasswordVisible((p) => !p)}
/>
}
/>
</View>
<View>
<Button mode="contained" onPress={() => registerUser(name)}>
{loading ? "Loading..." : "Register"}
</Button>
<ErrorDisplay message={error} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
flex: 1,
rowGap: 20,
},
header: {
padding: 6,
borderColor: "#eeeeee60",
borderBottomWidth: 1,
width: "100%",
},
title: {
color: "#eee",
fontSize: 20,
fontWeight: "bold",
},
separator: {
marginVertical: 30,
height: 1,
width: "80%",
},
});
The login screen will be very similar to the register screen but instead of logging the user in we will be using the signInWithEmailAndPassword function from "firebase/auth". Here is the login screen:
import { useRouter } from "expo-router";
import { signInWithEmailAndPassword } from "firebase/auth";
import React from "react";
import { StyleSheet, TouchableOpacity } from "react-native";
import { TextInput } from "react-native-paper";
import { Text, View, Button } from "../../components/Themed";
import { auth } from "../../firebaseConfig";
import ErrorDisplay from "../../components/ErrorDisplay";
export default function Login() {
const [email, setEmail] = React.useState("halecolin4@gmail.com");
const [password, setPassword] = React.useState("chatapp");
const [passwordVisible, setPasswordVisible] = React.useState(true);
const [error, setError] = React.useState("");
const [loading, setLoading] = React.useState(false);
const router = useRouter();
const onSubmitHandler = async ({ email, password, auth }) => {
setError("");
setLoading(true);
await signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// set user context here
router.push("/(tabs)");
})
.catch((error) => {
setError(error.message);
});
setLoading(false);
};
return (
<View style={styles.container}>
<View>
<Text>Email</Text>
<TextInput
value={email}
placeholder="Email"
onChangeText={(t) => setEmail(t)}
/>
</View>
<View>
<Text>Password</Text>
<TextInput
value={password}
placeholder="Password"
onChangeText={(t) => setPassword(t)}
secureTextEntry={passwordVisible}
right={
<TextInput.Icon
icon="eye"
onPress={() => setPasswordVisible((p) => !p)}
/>
}
/>
</View>
<View>
<Button
mode="contained"
onPress={() => onSubmitHandler({ email, password, auth })}
disabled={loading}
>
{loading ? "Loading..." : "Login"}
</Button>
<ErrorDisplay message={error} />
</View>
<View>
<TouchableOpacity onPress={() => router.push("/(auth)/resetPassword")}>
<Text>Reset password?</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
flex: 1,
rowGap: 20,
},
header: {
padding: 6,
borderColor: "#eeeeee60",
borderBottomWidth: 1,
width: "100%",
},
title: {
color: "#eee",
fontSize: 20,
fontWeight: "bold",
},
separator: {
marginVertical: 30,
height: 1,
width: "80%",
},
});
The last part of this app is the reset password. What this screen does is send an email to the user which allows them to reset their password through a link there. So all we need to have on that page is their email and firebase authentication will do the rest.
import { Text, View, Button } from "../../components/Themed";
import ErrorDisplay from "../../components/ErrorDisplay";
import { auth } from "../../firebaseConfig";
export default function Login() {
const [visible, setVisible] = React.useState(false);
const [email, setEmail] = React.useState("");
const [error, setError] = React.useState("");
const [loading, setLoading] = React.useState(false);
const router = useRouter();
const onPasswordResetHandler = async ({ auth }) => {
setError("");
setLoading(true);
await sendPasswordResetEmail(auth, email)
.then(() => {
setVisible(true);
})
.catch((error) => {
setError(error.message);
});
setLoading(false);
};
return (
<View style={styles.container}>
<View>
<Text>Email</Text>
<TextInput
value={email}
placeholder="Email"
onChangeText={(t) => setEmail(t)}
/>
</View>
<View style={styles.buttons}>
<Button
onPress={() => router.push("/(auth)")}
mode="contained"
style={{ flex: 1 }}
>
Back
</Button>
<Button
mode="contained"
onPress={() => onPasswordResetHandler({ auth })}
style={{ flex: 1 }}
>
{loading ? "Loading..." : "Send Reset Link"}
</Button>
</View>
<ErrorDisplay message={error} />
<Snackbar
visible={visible}
onDismiss={() => setVisible(false)}
action={{
label: "Close",
onPress: () => setVisible(false),
}}
>
<Text>
An email link to reset your password has been sent to {email}.
</Text>
</Snackbar>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
flex: 1,
rowGap: 20,
},
buttons: {
flexDirection: "row",
columnGap: 12,
alignItems: "stretch",
},
});
The last two screen in the tabs directory are just empty screens to show how you can get into the app after you have successfully logged in. If you want to look through the entire project you can find it here on github.
Top comments (0)