DEV Community

Colin Hale
Colin Hale

Posted on

Learning Firebase and React Native: Authentication

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",
  },
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Project Folder Structure

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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%",
  },
});

Enter fullscreen mode Exit fullscreen mode

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%",
  },
});

Enter fullscreen mode Exit fullscreen mode

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",
  },
});
Enter fullscreen mode Exit fullscreen mode

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)