DEV Community

Cover image for Expo Router Supabase Authentication Flow and Tab Navigation
Aaron K Saunders
Aaron K Saunders

Posted on

Expo Router Supabase Authentication Flow and Tab Navigation

Overview

This blog post is to complement the Expo Router Tab and Supabase Authentication Video I create to help you learn to use Supabase with Expo Router by building a simple authentication flow based on a Tab-based application template.

In this tutorial, we’ll be using Expo, Supabase, and Expo Router. I stuck to the basics like the other related videos in this series so you can build your own idea based on this video.

Install required packages

npm install @supabase/supabase-js
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
npx expo install expo-secure-store
Enter fullscreen mode Exit fullscreen mode

Create a Supabase client library

Normally I used the AsyncStorage library from the react-native community, but since we are working with Expo and this is the library used in the blog post, I figured I would give it a try.

The Supabase credentials come from an .env file that you will need to create.

EXPO_PUBLIC_SUPABASE_URL=
EXPO_PUBLIC_SUPABASE_ANON_KEY=
Enter fullscreen mode Exit fullscreen mode

The original source is from a Supabase project that is required for the application, that topic is covered here in the Supabase documentation. Create New Project In Supabase Dashboard.

// app/lib/supabase-client.js
import 'react-native-url-polyfill/auto'
import * as SecureStore from 'expo-secure-store'
import { createClient } from '@supabase/supabase-js'

const ExpoSecureStoreAdapter = {
  getItem: (key) => {
    return SecureStore.getItemAsync(key)
  },
  setItem: (key, value) => {
    SecureStore.setItemAsync(key, value)
  },
  removeItem: (key) => {
    SecureStore.deleteItemAsync(key)
  },
}

const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    storage: ExpoSecureStoreAdapter,
    autoRefreshToken: true
  },
})
Enter fullscreen mode Exit fullscreen mode

Index Page

Checking for authentication status using the supabase client we created above.

in the useEffect hook we check for a current session using supabase.auth.getSession() and redirect to a route if we have a user.

the supabase.auth.onAuthStateChange() is setup to allow the app to automatically redirect to appropriate route when the user's authentication state changes; meaning when you logout we dont need to directly tell the application to route to the login page, this code will do it for you.

// app/index.js

import { router } from "expo-router";
import { useEffect } from "react";
import { supabase } from "./lib/supabase-client";

export default function IndexPage() {
  useEffect(() => {
    supabase.auth.getSession().then(({ data: { session } }) => {
      if (session) {
        router.replace("/(tabs)/home/");
      } else {
        console.log("no user");
      }
    });

    supabase.auth.onAuthStateChange((_event, session) => {
      if (session) {
        router.replace("/(tabs)/home/");
      } else {
        console.log("no user");
        router.replace("/(auth)/login");
      }
    });
  }, []);

}
Enter fullscreen mode Exit fullscreen mode

The (auth) Segment

AuthLayout

We add this code for the _layout

// app/(auth)/_layout.js

import { Slot } from "expo-router";

export default function AuthLayout() {
  return <Slot />;
}
Enter fullscreen mode Exit fullscreen mode

Login / Create Account Page

We have created a simple page to create an account or login to the application. This code is straight from the Supabase documentation except for the styling.

// app/(auth)/login.js

import React, { useState } from "react";
import { Alert, StyleSheet, TextInput, View, Button, Text } from "react-native";
import { supabase } from "../lib/supabase-client";
import { TouchableOpacity } from "react-native-gesture-handler";
import { Stack } from "expo-router";

export default function AuthPage() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);

  async function signInWithEmail() {
    setLoading(true);
    const { error } = await supabase.auth.signInWithPassword({
      email: email,
      password: password,
    });

    if (error) Alert.alert("Sign In Error", error.message);
    setLoading(false);
  }

  async function signUpWithEmail() {
    setLoading(true);
    const { error } = await supabase.auth.signUp({
      email: email,
      password: password,
    });

    if (error) Alert.alert("Sign Up Error", error.message);
    setLoading(false);
  }

  return (
    <View style={styles.container}>
      <Stack.Screen options={{ headerShown: true, title: "Supabase Expo Router App" }} />
      <View style={[styles.verticallySpaced, styles.mt20]}>
        <TextInput
          style={styles.textInput}
          label="Email"
          onChangeText={(text) => setEmail(text)}
          value={email}
          placeholder="email@address.com"
          autoCapitalize={"none"}
        />
      </View>
      <View style={styles.verticallySpaced}>
        <TextInput
          style={styles.textInput}
          label="Password"
          onChangeText={(text) => setPassword(text)}
          value={password}
          secureTextEntry={true}
          placeholder="Password"
          autoCapitalize={"none"}
        />
      </View>
      <View style={[styles.verticallySpaced, styles.mt20]}>
        <TouchableOpacity
          disabled={loading}
          onPress={() => signInWithEmail()}
          style={styles.buttonContainer}
        >
          <Text style={styles.buttonText}>SIGN IN</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.verticallySpaced}>
        <TouchableOpacity
          disabled={loading}
          onPress={() => signUpWithEmail()}
          style={styles.buttonContainer}
        >
          <Text style={styles.buttonText}>SIGN UP</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    marginTop: 40,
    padding: 12,
  },
  verticallySpaced: {
    paddingTop: 4,
    paddingBottom: 4,
    alignSelf: "stretch",
  },
  mt20: {
    marginTop: 20,
  },
  buttonContainer: {
    backgroundColor: "#000968",
    borderRadius: 10,
    paddingVertical: 10,
    paddingHorizontal: 12,
    margin: 8,
  },
  buttonText: {
    fontSize: 18,
    color: "#fff",
    fontWeight: "bold",
    alignSelf: "center",
    textTransform: "uppercase",
  },
  textInput: {
    borderColor: "#000968",
    borderRadius: 4,
    borderStyle: "solid",
    borderWidth: 1,
    padding: 12,
    margin: 8,
  },
});

Enter fullscreen mode Exit fullscreen mode

Settings Page

In the settings page we show the current authenticated user using the Supabase client and we also provide a logout button to log the user our and clear the session.

Notice there is no router redirect here because the authentication listener we created in the IndexPage will detect the change in state and redirect the application to the LoginPage

// app/(settings)/index.js

import { Stack } from "expo-router";
import { SafeAreaView, Text, View, TouchableOpacity, StyleSheet } from "react-native";
import { supabase } from "../../lib/supabase-client";
import { useEffect, useState } from "react";

export default function SettingsPage() {
  const [user, setUser] = useState(null);
  useEffect(() => {
    supabase.auth.getUser().then(({ data: { user } }) => {
      if (user) {
        setUser(user);
      } else {
        Alert.alert("Error Accessing User");
      }
    });
  }, []);

  const doLogout = async () => {
    const {error} = await supabase.auth.signOut();
    if (error) {
      Alert.alert("Error Signing Out User", error.message);
    }
  }

  return (
    <SafeAreaView style={{ flex: 1 }}>
      <Stack.Screen options={{ headerShown: true, title: "Settings" }} />
      <View style={{ padding: 16 }}>
        <Text>{JSON.stringify(user, null, 2)}</Text>
        <TouchableOpacity onPress={doLogout} style={styles.buttonContainer}>
          <Text style={styles.buttonText}>LOGOUT</Text>
        </TouchableOpacity>
      </View>
    </SafeAreaView>
  );
}


const styles = StyleSheet.create({
  container: {
    marginTop: 40,
    padding: 12,
  },
  verticallySpaced: {
    paddingTop: 4,
    paddingBottom: 4,
    alignSelf: "stretch",
  },
  mt20: {
    marginTop: 20,
  },
  buttonContainer: {
    backgroundColor: "#000968",
    borderRadius: 10,
    paddingVertical: 10,
    paddingHorizontal: 12,
    margin: 8,
  },
  buttonText: {
    fontSize: 18,
    color: "#fff",
    fontWeight: "bold",
    alignSelf: "center",
    textTransform: "uppercase",
  },
  textInput: {
    borderColor: "#000968",
    borderRadius: 4,
    borderStyle: "solid",
    borderWidth: 1,
    padding: 12,
    margin: 8,
  },
});

Enter fullscreen mode Exit fullscreen mode

Links

Social Media

Top comments (2)

Collapse
 
dominikwozniak profile image
Dominik Woźniak

Great article!
Two comments that occurred to me while reading:

  • Supabase recommends using singleton pattern when using @supabase/supabase-js in an application. You can make a Context/Provider and initialize the client instance there
  • it's also worth doing unsubscribe for supabase.auth.onAuthStateChange listener - I didn't find this in the Supabase for React Native docs, but for Remix it was described.
Collapse
 
alexbriannaughton profile image
Alex Naughton • Edited

This is a great write up and was so helpful, thank you!!

Have you worked with Supabase's password recovery flow? I'm currently having an issue getting the PASSWORD_RECOVERY event to fire--I'm instead just getting an INITIAL_SESSION event when I click the recovery link that gets sent to my email. Here's a link to a minimal expo router project that reproduces the issue on my end, if you wanted to take a peek.
github.com/alexbriannaughton/expo-...