Building a mobile authentication flow for your SaaS with Expo and Supabase

Building a mobile authentication flow for your SaaS with Expo and Supabase

Expo is an invaluable tool when it comes to developing mobile apps, offering a rich SDK with a wide range of packages. With services like EAS Build, the process of publishing your app becomes effortless. However, if your goal is to create a SaaS (Software as a Service), it becomes essential to have a backend for functionalities such as authentication and information storage.

In such cases, you may consider using Supabase as your backend solution. Supabase provides a seamless way to handle authentication, data storage, and more, complementing Expo's frontend capabilities to create a robust and scalable mobile app for a SaaS.

What do you get from this tutorial?

https://i.imgur.com/2PxId3S.png

  • A new Supabase project with everything configured to handle authentication.
  • A working Expo app integrated with Supabase.
    • A global Supabase context with easy access to interact with the Supabase client
    • React-navigation setup for all the routes. You get IntelliSense for route names.
    • Four working screens (Login, Register, Forgot Password, Home)
    • Route security setup (only authenticated users can access the Home route).
  • Login flow using Supabase auth.
    • Your session info is securely persisted locally using Expo Secure Store.
    • Automatic refresh token flow
  • Register flow
  • Forgot Password flow
  • Setup to read environment variables from a .env file.

Expo-Supabase template is now available with SpiroKit!

For this demo app, I'll be using SpiroKit, which is a React Native UI kit I built. Given that is a paid product, feel free to follow this tutorial with your own UI.

With SpiroKit, you can use our new expo-supabase-typescript-template. It includes everything you’ll see in this tutorial.

Creating a new Supabase project

Supabase is an open-source project that offers a range of functionalities for hosting projects in the cloud. As of the time of writing, it allows users to host up to 2 projects in their free tier. If you have exceeded this quota, an alternative option is to host your own instance on a dedicated server. However, for the sake of simplicity, we will focus on utilizing the cloud project setup in this tutorial. Setting up a dedicated server falls outside the scope of this tutorial and will not be covered in detail.

  1. Visit https://supabase.com and sign up if you don’t have an account
  2. Once your account is ready, create your first project:

    Supabase new project button

  3. Choose a name for your project and a strong password. Make sure you are using the “Free” pricing plan if you are experimenting. You can update to a paid plan later if it’s required.

    Supabase new project form

  4. Hit the “Create new project” to confirm. If everything goes right, you’ll be redirected to the home page of your new Supabase project. You’ll need to copy the Project URL and the API key (anon) later to connect your project with your Expo app

    Supabase tokens

Setting up authentication on Supabase

Every Supabase project comes with a full Postgres database. We’ll be using it to support our basic authentication flow with email and password.

  1. From your supabase project dashboard, use the sidebar to visit the “SQL Editor” section

    Supabase SQL Editor

  2. Under the “Quick Start” section, you’ll find the “User Management Starter” card. This will help us setup all the required tables for the auth flow.

  3. You can click the “Run” button to execute the query as is, or you can make a few modifications before execution based on your needs. This script will perform the following tasks:

    1. Create Table: Defines a table called profiles with various columns to store information about public profiles, such as ID, updated timestamp, username, full name, avatar URL, and website. It also includes a constraint to ensure that the username length is at least 3 characters.
    2. Set up Row Level Security (RLS): Enables Row Level Security for the profiles table. RLS allows controlling access to individual rows based on policies. If you want lo learn more about this powerful feature, visit the following link
    3. Create Policies: Defines three policies to control access to the profiles table:
      • "Public profiles are viewable by everyone.": Grants read access to all rows in the profiles table.
      • "Users can insert their own profile.": Allows users to insert a row into the profiles table only if their authenticated user ID matches the "id" column.
      • "Users can update their own profile.": Permits users to update their own profile by matching their authenticated user ID with the "id" column.
    4. Create a function and a Trigger: Sets up a trigger function named "handle_new_user" and a trigger named "on_auth_user_created" to automatically create a profile entry when a new user signs up via Supabase Auth (we’ll use this on our Expo app later). The trigger function extracts relevant information from the newly created user and inserts it into the profiles table.
    5. Set up Storage: Inserts a row into the storage.buckets table to define a bucket named "avatars" for storing avatar images.
    6. Create Storage Policies: Defines two policies to control access to the "avatars" bucket in storage:

      • "Avatar images are publicly accessible.": Allows anyone to retrieve (select) objects from the "avatars" bucket.
      • "Anyone can upload an avatar.": Permits anyone to insert (upload) objects into the "avatars" bucket.

        For simplicity, I’ll execute the script as is.

  4. We can now visit the “Table Editor” section using the sidebar and check that the profiles table was created

    Supabase - Check that the tables were created

Creating an Expo app

Now that we are done with Supabase, it’s time to create our Expo app.

Because I’m using SpiroKit, I’ll be using the Expo Starter template that already comes with everything setup and ready to use. If you choose to also use SpiroKit, make sure to get your license and follow the installation instructions before running the following command:

npx create-spirokit-app --template expo-template-typescript

If you want to create a plain React Native app with Expo, run this command instead:

npx create-react-native-app -t with-typescript

# This is optional if you want to add icons to your screens. 
# It's already included with the SpiroKit template
yarn add react-native-heroicons

Setting up Supabase in your Expo app

  1. You will need to run the following command to install all the required packages

     yarn add @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill
    
     # Required for accessing env variables during development
     yarn add -D babel-plugin-inline-dotenv 
    
     # Required to encrypt the information in the device
     npx expo install expo-secure-store
    
  2. Because we are using Expo Secure Store later to store information, we will need to update our app.json file to avoid issues with App Store Connect during app submission. We will set the usesNonExemptEncryption option to false like this: **

     {
       "expo": {
         "ios": {
           "config": {
             "usesNonExemptEncryption": false
           }
         }
       }
     }
    

    Setting this property automatically handles the compliance information prompt, as described in the Expo docs

  3. Now, let’s create a SupabaseContext that will allow us to get interact with the supabase client from any screen of our app. Run the following commands to create the src folder, then the context folder inside, and finally, a few empty files to implement our context.

     mkdir src
     mkdir src/context
     touch ./src/context SupabaseContext.tsx
     touch ./src/context SupabaseProvider.tsx
     touch ./src/context useSupabase.tsx
    
  4. The SupabaseContext.tsx will define the contract for the provider. We’ll add a isLoggedIn flag to easily check the session status, and a few methods to interact with Supabase auth service

     import { createContext } from "react";
    
     type SupabaseContextProps = {
       isLoggedIn: boolean;
       login: (email: string, password: string) => Promise<void>;
       register: (email: string, password: string) => Promise<void>;
       forgotPassword: (email: string) => Promise<void>;
       logout: () => Promise<void>;
     };
    
     export const SupabaseContext = createContext<SupabaseContextProps>({
       isLoggedIn: false,
       login: async () => {},
       register: async () => {},
       forgotPassword: async () => {},
       logout: async () => {},
     });
    
  5. Then, add the following code to the SupabaseProvider.tsx file to initialize the Supabase client and implement all the required methods. We also need the isNavigationReady to load our navigation stack after the session info is available. That way, we can hide certain routes for anonymous users.

     import "react-native-url-polyfill/auto";
     import { createClient } from "@supabase/supabase-js";
     import React, { useState, useEffect } from "react";
     import * as SecureStore from "expo-secure-store";
     import { SupabaseContext } from "./SupabaseContext";
    
     // We are using Expo Secure Store to persist session info
     const ExpoSecureStoreAdapter = {
       getItem: (key: string) => {
         return SecureStore.getItemAsync(key);
       },
       setItem: (key: string, value: string) => {
         SecureStore.setItemAsync(key, value);
       },
       removeItem: (key: string) => {
         SecureStore.deleteItemAsync(key);
       },
     };
    
     type SupabaseProviderProps = {
       children: JSX.Element | JSX.Element[];
     };
    
     export const SupabaseProvider = (props: SupabaseProviderProps) => {
       const [isLoggedIn, setLoggedIn] = useState(false);
       const [isNavigationReady, setNavigationReady] = useState(false);
    
       const supabase = createClient(
         process.env.SUPABASE_URL,
         process.env.SUPABASE_ANON_KEY,
         {
           auth: {
             storage: ExpoSecureStoreAdapter,
             autoRefreshToken: true,
             persistSession: true,
             detectSessionInUrl: false,
           },
         }
       );
    
       const login = async (email: string, password: string) => {
         const { error } = await supabase.auth.signInWithPassword({
           email,
           password,
         });
         if (error) throw error;
         setLoggedIn(true);
       };
    
       const register = async (email: string, password: string) => {
         const { error } = await supabase.auth.signUp({
           email,
           password,
         });
         if (error) throw error;
       };
    
       const forgotPassword = async (email: string) => {
         const { error } = await supabase.auth.resetPasswordForEmail(email);
         if (error) throw error;
       };
    
       const logout = async () => {
         const { error } = await supabase.auth.signOut();
         if (error) throw error;
         setLoggedIn(false);
       };
    
       const checkIfUserIsLoggedIn = async () => {
         const result = await supabase.auth.getSession();
         setLoggedIn(result.data.session !== null);
         setNavigationReady(true);
       };
    
       useEffect(() => {
         checkIfUserIsLoggedIn();
       }, []);
    
       return (
         <SupabaseContext.Provider
           value={{ isLoggedIn, login, register, forgotPassword, logout }}
         >
           {isNavigationReady ? props.children : null}
         </SupabaseContext.Provider>
       );
     };
    
  6. Finally, we’ll use the useSupabase.tsx file to create a convenient hook:

     import React from "react";
     import { SupabaseContext } from "./SupabaseContext";
    
     export const useSupabase = () => React.useContext(SupabaseContext);
    
  7. Because we are using TypeScript, we’ll also need to create a app.d.ts to define the types for our environment variables

     touch app.d.ts
    

    Now let’s add our environment variables to the declaration file

     // app.d.ts
     declare namespace NodeJS {
       interface ProcessEnv {
         SUPABASE_URL: string;
         SUPABASE_ANON_KEY: string;
       }
     }
    

    If you still have typescript errors on your hook, run the “reload window” command in VSCode, or just close and open the IDE to refresh everything.

  8. To properly get access to our environment variables during development, we need to update our babel.config.js file to add the “inline-dotenv” plugin. It should look like this:

     module.exports = function (api) {
       api.cache(true);
       return {
         presets: ["babel-preset-expo"],
         plugins: ["inline-dotenv"], // -> Required to read env variables
       };
     };
    

    We’ll also need to create the .env file in the root of our project and provide a value for the SUPABASE_URL and SUPABASE_ANON_KEY variables:

     touch .env
    
     SUPABASE_URL="YOUR_SUPABASE_URL"
     SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
    

    If you already run your app before creating the .env file, please run the app with the expo r -c command to clear the expo cache. Otherwise, your environment variables won’t work. This is required every time the .env file is updated.

Setting up React Navigation

Because we are adding a few screens like Login, Register, ForgotPassword, and Home, we will need to add React Navigation to easily navigate through the screens.

  1. Let’s start by installing the required dependencies

     npx expo install @react-navigation/native @react-navigation/stack react-native-gesture-handler
    
  2. I’ll create a navigation folder to store all the required components here. But feel free to choose a different location for your files

     mkdir navigation
     touch navigation/GlobalNavigation.tsx
    
  3. We’ll also need to create a few dummy screens

     mkdir screens
     touch screens/LoginScreen.tsx
     touch screens/RegisterScreen.tsx
     touch screens/ForgotPasswordScreen.tsx
     touch screens/HomeScreen.tsx
    

    For all the screens, let’s add a temp UI

     // screens/LoginScreen.tsx
     import { Box, LargeTitle } from "@spirokit/core";
    
     const LoginScreen = () => {
       return (
         <Box safeArea>
           <LargeTitle>Login Screen</LargeTitle>
         </Box>
       );
     };
    
     export default LoginScreen;
    
     // screens/RegisterScreen.tsx
     import { Box, LargeTitle } from "@spirokit/core";
    
     const RegisterScreen = () => {
       return (
         <Box safeArea>
           <LargeTitle>Register Screen</LargeTitle>
         </Box>
       );
     };
    
     export default RegisterScreen;
    
     // screens/ForgotPasswordScreen.tsx
     import { Box, LargeTitle } from "@spirokit/core";
    
     const ForgotPasswordScreen = () => {
       return (
         <Box safeArea>
           <LargeTitle>Forgot Password Screen</LargeTitle>
         </Box>
       );
     };
    
     export default ForgotPasswordScreen;
    
     // screens/HomeScreen.tsx
     import { Box, LargeTitle } from "@spirokit/core";
    
     const HomeScreen = () => {
       return (
         <Box safeArea>
           <LargeTitle>Home Screen</LargeTitle>
         </Box>
       );
     };
    
     export default HomeScreen;
    
  4. Now that we have all our screens, we can go back to the GlobalNavigation.tsx file and add the Stack navigation with all our screens

     // navigation/GlobalNavigation.tsx
    
     import { NavigationContainer } from "@react-navigation/native";
     import { createStackNavigator } from "@react-navigation/stack";
     import React from "react";
     import ForgotPasswordScreen from "../screens/ForgotPasswordScreen";
     import HomeScreen from "../screens/HomeScreen";
     import LoginScreen from "../screens/LoginScreen";
     import RegisterScreen from "../screens/RegisterScreen";
     import { useSupabase } from "../context/useSupabase";
    
     const Stack = createStackNavigator();
    
     const GlobalNavigation = () => {
       // We check if the user is logged in
       const { isLoggedIn } = useSupabase();
    
       return (
         <NavigationContainer>
           <Stack.Navigator
             initialRouteName={isLoggedIn ? "Home" : "Login"}
             screenOptions={{ headerShown: false }}
           >
             {/* Only authenticated users can access the home */}
             {isLoggedIn ? (
               <Stack.Screen name="Home" component={HomeScreen} />
             ) : (
               <>
                 <Stack.Screen name="Login" component={LoginScreen} />
                 <Stack.Screen name="Register" component={RegisterScreen} />
                 <Stack.Screen
                   name="ForgotPassword"
                   component={ForgotPasswordScreen}
                 />
               </>
             )}
           </Stack.Navigator>
         </NavigationContainer>
       );
     };
    
     export default GlobalNavigation;
    
  5. Then, we need to update our App.tsx file to add our supabase context and our new global navigation.

     import { SpiroKitProvider, usePoppins, useSpiroKitTheme } from "@spirokit/core";
     import GlobalNavigation from "./src/navigation/GlobalNavigation";
     import { SupabaseProvider } from "./src/context/SupabaseProvider";
    
     const myTheme = useSpiroKitTheme();
    
     export default function App() {
       const fontLoaded = usePoppins();
    
       if (!fontLoaded) return <></>;
    
       return (
         <SpiroKitProvider theme={myTheme}>
           <SupabaseProvider>
             <GlobalNavigation></GlobalNavigation>
           </SupabaseProvider>
         </SpiroKitProvider>
       );
     }
    
  6. Finally, let’s create a GlobalParamList.tsx inside our navigation folder to add proper IntelliSense to our routes while using navigation.navigate("routeName") to move between screens

     touch navigation/GlobalParamList.tsx
    
     import { StackNavigationProp } from "@react-navigation/stack";
    
     export type GlobalParamList = {
       Home: undefined;
       Login: undefined;
       Register: undefined;
       ForgotPassword: undefined;
     };
    
     export type ScreenNavigationProp = StackNavigationProp<GlobalParamList>;
    
     declare global {
       namespace ReactNavigation {
         interface RootParamList extends GlobalParamList {}
       }
     }
    

Updating the Login screen

With the Supabase context set and our navigation stack in place, we need to update all the different screens. Let’s start with the Login with a few comments:

  • We are using the useSupabase hook to call the login method
  • We wrap the screen with a KeyboardAvoidingView to prevent annoying issues with the keyboard (this applies to all our screens below)
// screens/LoginScreen.tsx

import {
  Body,
  Button,
  Input,
  KeyboardAvoidingView,
  Pressable,
  TitleTwo,
  VStack,
  Image,
  Subhead,
} from "@spirokit/core";
import React from "react";
import { Platform, ScrollView } from "react-native";
import { LockClosedIcon, MailIcon } from "react-native-heroicons/outline";
import { useNavigation } from "@react-navigation/native";
import { useHeaderHeight } from "@react-navigation/elements";
import { useSupabase } from "../context/useSupabase";

const LoginScreen = () => {
  const navigation = useNavigation();
  const height = useHeaderHeight();
  const { login } = useSupabase();

  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [loading, setLoading] = React.useState(false);

  const onSignInTapped = async () => {
    try {
      setLoading(true);
      await login(email, password);
    } catch (error) {
      console.log(error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <KeyboardAvoidingView
      flex={1}
      keyboardVerticalOffset={height}
      backgroundColor={"primaryGray.100"}
      behavior={Platform.OS === "ios" ? "padding" : "height"}
    >
      <ScrollView contentContainerStyle={{ flexGrow: 1 }}>
        <VStack safeAreaTop padding={4} flex={1}>
          <VStack space={4} marginTop={5} width="full" flex={1}>
            <Image
              source={{ uri: "https://i.imgur.com/FawVClJ.png" }}
              width="full"
              height={200}
              alt="Login icon"
              resizeMode="contain"
            ></Image>
            <TitleTwo fontWeight="medium">Sign in</TitleTwo>
            <Input
              placeholder="Enter your email"
              IconLeftComponent={MailIcon}
              onChangeText={(text) => setEmail(text)}
            ></Input>
            <Input
              placeholder="Enter your password"
              secureTextEntry={true}
              IconLeftComponent={LockClosedIcon}
              onChangeText={(text) => setPassword(text)}
            ></Input>
            <Pressable onPress={() => navigation.navigate("ForgotPassword")}>
              <Subhead textAlign={"right"} paddingBottom="2">
                Forgot Password?
              </Subhead>
            </Pressable>

            <Button
              isDisabled={loading}
              onPress={() => onSignInTapped()}
              marginBottom={5}
            >
              {loading ? "Loading..." : "Sign in"}
            </Button>
          </VStack>
        </VStack>

        <VStack padding={4} backgroundColor={"white"} safeAreaBottom>
          <Body textAlign={"center"}>
            Have an account?{" "}
            <Pressable onPress={() => navigation.navigate("Register")}>
              <Body fontWeight="bold" textDecorationLine="underline">
                Sign up
              </Body>
            </Pressable>
          </Body>
        </VStack>
      </ScrollView>
    </KeyboardAvoidingView>
  );
};

export default LoginScreen;

Updating the Register screen

With the Login screen done, let’s work on the register screen:

// screens/RegisterScreen.tsx

import {
  Body,
  Button,
  Input,
  KeyboardAvoidingView,
  Pressable,
  TitleTwo,
  VStack,
  Image,
} from "@spirokit/core";
import React from "react";
import { Platform, ScrollView } from "react-native";
import { LockClosedIcon, MailIcon } from "react-native-heroicons/outline";

import { useNavigation } from "@react-navigation/native";
import { useHeaderHeight } from "@react-navigation/elements";
import { useSupabase } from "../context/useSupabase";

const RegisterScreen = () => {
  const navigation = useNavigation();
  const height = useHeaderHeight();

  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [loading, setLoading] = React.useState(false);

  const { register } = useSupabase();

  const onSignUpTapped = async () => {
    try {
      setLoading(true);
      await register(email, password);
      navigation.navigate("Login");
    } catch (error) {
      console.log(error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <KeyboardAvoidingView
      flex={1}
      keyboardVerticalOffset={height}
      backgroundColor={"primaryGray.100"}
      behavior={Platform.OS === "ios" ? "padding" : "height"}
    >
      <ScrollView contentContainerStyle={{ flexGrow: 1 }}>
        <VStack
          safeAreaTop
          padding={4}
          alignItems="flex-start"
          width="full"
          flex={1}
        >
          <VStack space={4} marginTop={5} width="full" flex={1}>
            <Image
              source={{ uri: "https://i.imgur.com/oNY0QGb.png" }}
              width="full"
              height={200}
              alt="Register icon"
              resizeMode="contain"
            ></Image>
            <TitleTwo fontWeight="medium">Sign up</TitleTwo>
            <Input
              placeholder="Enter your email"
              IconLeftComponent={MailIcon}
              onChangeText={(text) => setEmail(text)}
            ></Input>
            <Input
              placeholder="Enter your password"
              secureTextEntry={true}
              IconLeftComponent={LockClosedIcon}
              onChangeText={(text) => setPassword(text)}
            ></Input>
            <Button
              isDisabled={loading}
              marginBottom={5}
              onPress={() => onSignUpTapped()}
            >
              {loading ? "Loading..." : "Sign up"}
            </Button>
          </VStack>
        </VStack>
        <VStack padding={4} safeAreaBottom>
          <Body textAlign={"center"}>
            If you have an account,{" "}
            <Pressable onPress={() => navigation.navigate("Login")}>
              <Body fontWeight="bold" textDecorationLine="underline">
                Sign in
              </Body>
            </Pressable>
          </Body>
        </VStack>
      </ScrollView>
    </KeyboardAvoidingView>
  );
};

export default RegisterScreen;

Updating the Forgot Password screen

  • We are using the useSupabase hook to call the forgotPassword method
  • We are using an Alert component to show a message after the recovery email was sent.
  • Feel free to improve the error handling in the onSendTapped to show a custom error if something goes wrong.
// screens/ForgotPasswordScreen.tsx

import {
  Body,
  Button,
  Input,
  KeyboardAvoidingView,
  Pressable,
  TitleTwo,
  VStack,
  Image,
  Alert,
  TitleOne,
} from "@spirokit/core";
import React from "react";
import { Platform, ScrollView } from "react-native";
import { MailIcon } from "react-native-heroicons/outline";

import { useNavigation } from "@react-navigation/native";
import { useHeaderHeight } from "@react-navigation/elements";
import { useSupabase } from "../context/useSupabase";

const ForgotPasswordScreen = () => {
  const navigation = useNavigation();
  const height = useHeaderHeight();

  const [email, setEmail] = React.useState("");
  const [loading, setLoading] = React.useState(false);
  const [showResultModal, setShowResultModal] = React.useState(false);

  const { forgotPassword } = useSupabase();

  const onFinishTapped = () => {
    setShowResultModal(false);
    navigation.navigate("Login");
  };

  const onSendTapped = async () => {
    try {
      setLoading(true);
      await forgotPassword(email);
      setShowResultModal(true);
    } catch (error) {
      console.log(error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <KeyboardAvoidingView
      flex={1}
      keyboardVerticalOffset={height}
      backgroundColor={"primaryGray.100"}
      behavior={Platform.OS === "ios" ? "padding" : "height"}
    >
      <ScrollView contentContainerStyle={{ flexGrow: 1 }}>
        <VStack
          safeAreaTop
          padding={4}
          alignItems="flex-start"
          width="full"
          flex={1}
        >
          <VStack space={4} marginTop={5} width="full" flex={1}>
            <Image
              source={{ uri: "https://i.imgur.com/sDzRjS4.png" }}
              width="full"
              alt="Forgot password icon"
              height={200}
              resizeMode="contain"
            ></Image>
            <TitleTwo fontWeight="medium">Forgot password?</TitleTwo>
            <Input
              placeholder="Enter your email"
              IconLeftComponent={MailIcon}
              onChangeText={(text) => setEmail(text)}
            ></Input>
            <Button
              isDisabled={loading}
              marginBottom={5}
              onPress={() => onSendTapped()}
            >
              {loading ? "Loading..." : "Send"}
            </Button>
          </VStack>
        </VStack>
        <VStack padding={4} safeAreaBottom>
          <Body textAlign={"center"}>
            If you have an account,{" "}
            <Pressable onPress={() => navigation.navigate("Login")}>
              <Body fontWeight="bold" textDecorationLine="underline">
                Sign in
              </Body>
            </Pressable>
          </Body>
        </VStack>
        <Alert
          isVisible={showResultModal}
          onClose={() => setShowResultModal(false)}
          TitleComponent={<TitleOne>Email sent</TitleOne>}
          ConfirmButtonComponent={
            <Button onPress={() => onFinishTapped()}>Ok</Button>
          }
        ></Alert>
      </ScrollView>
    </KeyboardAvoidingView>
  );
};

export default ForgotPasswordScreen;

Updating the Home Screen


  • Primarily used to test routing security and allow users to logout
  • After logout, the Supabase Context is updated, and the Global Navigation will trigger a re-render. The user is automatically redirected to the Login screen.
// screens/Home.tsx

import { Button, LargeTitle, Image, VStack } from "@spirokit/core";
import { useSupabase } from "../context/useSupabase";

const HomeScreen = () => {
  const { logout } = useSupabase();
  return (
    <VStack space={8} safeArea padding={4} flex={1} justifyContent="center">
      <LargeTitle textAlign={"center"}>Welcome!</LargeTitle>
      <Image
        source={{ uri: "https://i.imgur.com/k78EnxY.png" }}
        width="full"
        alt="Hello icon"
        height={200}
        resizeMode="contain"
      ></Image>
      <Button onPress={() => logout()}>Logout</Button>
    </VStack>
  );
};

export default HomeScreen;

Final thoughts

That was an intense article! But I wanted to cover the entire flow. There are many things we could improve or add, but I’m thinking of building a series around it. We could add a working example retrieving information from Supabase db or include additional auth mechanisms.

I would love to hear your thoughts! Your input can help me shape the upcoming articles.