Expo Router V2 - Authentication Flow with Appwrite
Tags: expo, reactnative, appwrite, exporouter
Published: 2023-07-10 17:20:38

This blog post is a complimentary resource to support the video on [Expo Router v2](https://youtu.be/Ud_GWxu1_Xg)

In the video, I walk you through the code for an authentication flow using [Expo Router](https://docs.expo.dev/routing/introduction/) and [Appwrite](https://appwrite.io/docs/getting-started-for-web). 

I explain how to set up the environment variables, install the router, and incorporate Appwrite as the authentication provider. I also demonstrate how to create login and signup pages, manage authentication state, and handle navigation. 

The video provides a step-by-step guide with code examples and explanations. Check it out to learn how to build a secure authentication flow for your Expo app!

{% youtube Ud_GWxu1_Xg %}

## Create your Appwrite Project 
Appwrite is a backend platform for developing Web, Mobile, and Flutter applications. Built with the open source community and optimized for developer experience in the coding languages you love.

Go to [https://cloud.appwrite.io/](https://cloud.appwrite.io/) to get started

#### Create Account

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m7pscld131po92f1wnyt.png)

#### Click Create Project Button

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxsod2r6v6zubq8wg6z9.png)

#### Enter Project Name

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zrwq04m5x5u90e2stg0q.png)

#### Under "Add Platform", Click "Web App"

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2pwpbr7n9t0z6l6kznmg.png)

#### Provide a Name For Your App
Be sure to enter "cloud.appwrite.io" as your domain

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/18id6n0i7h8pdhqusyvh.png)

#### Install The Appwrite SDK

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/watzybmsqu5tqg2i4apc.png)

#### All Done
Be sure to copy project id, you will need it for the environment file

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s9p9cpp0n66isivoq8s3.png)

## Create Your Expo SDK 49 Project + Expo Router v2
- see blog for additional information [https://blog.expo.dev/](https://blog.expo.dev/)

```
npx create-expo-app@latest --template tabs@49
```
This command will create the default app with the latest version of expo sdk and expo router. We will be adding an authentication flow to the application and integrating Appwrite Account Creation and Login.

### Add Appwrite Library To Project
This code will set up the Appwrite client and provides a convenient interface to interact with the Appwrite server in a react native application.

First you need to create the `.env` file to hold the Appwrite Project Id

```javascript
EXPO_PUBLIC_APPWRITE_PROJECT_ID=[your project id]
```
Then we need to add the code for the library

```react
// /app/lib/appwrite-service.ts
//---- (1) ----
import {
  Account,
  Client,
  ID,
} from "appwrite";

// ---- (2) ----
const client = new Client();
client
  .setEndpoint("https://cloud.appwrite.io/v1")
  .setProject(process.env.EXPO_PUBLIC_APPWRITE_PROJECT_ID as string);

// ---- (3) ----
export const appwrite = {
  client,
  account: new Account(client),
  ID
};
```

1) Importing the required dependencies
2) Creating a new Appwrite client instance and Configuring the client. This is where we using the value from the `.env` file
3) Exporting the Appwrite client and related objects; we need the account object for the authentication and account creation functionality. The ID object generates unique identifiers that are utilized when creating the new user account

### Add Auth ProviderBased On Expo Router Documentation
I won't spend a lot of time explaining what the Provider is and how React Context works, you can click here for additional information on [React Context API](https://react.dev/learn/passing-data-deeply-with-context)

This is the link to the [original source code for auth context](https://docs.expo.dev/router/reference/authentication/).

I will cover the changes made to support Typescript and the integration of the `appwrite-service` library.

This code sets up an authentication context and provider in a React application, provides authentication-related functions, and allows other components to consume the authentication context using the useAuth hook. It handles user authentication, route protection, and authentication-related API interactions with the Appwrite service.

```react
// /app/context/auth.ts
// ---- (1) ----
import { useRootNavigation, useRouter, useSegments } from "expo-router";
import React, { useContext, useEffect, useState } from "react";
import { appwrite } from "../lib/appwrite-service";
import { Models } from "appwrite";

// ---- (2) ----
// Define the AuthContextValue interface
interface SignInResponse {
  data: Models.User<Models.Preferences> | undefined;
  error: Error | undefined;
}

interface SignOutResponse {
  error: any | undefined;
  data: {} | undefined;
}

interface AuthContextValue {
  signIn: (e: string, p: string) => Promise<SignInResponse>;
  signUp: (e: string, p: string, n: string) => Promise<SignInResponse>;
  signOut: () => Promise<SignOutResponse>;
  user: Models.User<Models.Preferences> | null;
  authInitialized: boolean;
}

// Define the Provider component
interface ProviderProps {
  children: React.ReactNode;
}

// ---- (3) ----
// Create the AuthContext
const AuthContext = React.createContext<AuthContextValue | undefined>(
  undefined
);

// ---- (4) ----
export function Provider(props: ProviderProps) {

// ---- (5) ----
  const [user, setAuth] =
    React.useState<Models.User<Models.Preferences> | null>(null);
  const [authInitialized, setAuthInitialized] = React.useState<boolean>(false);

  // This hook will protect the route access based on user authentication.
  // ---- (6) ----
  const useProtectedRoute = (user: Models.User<Models.Preferences> | null) => {
    const segments = useSegments();
    const router = useRouter();

    // checking that navigation is all good;
    // ---- (7) ----
    const [isNavigationReady, setNavigationReady] = useState(false);
    const rootNavigation = useRootNavigation();
    
    // ---- (8) ----
    useEffect(() => {
      const unsubscribe = rootNavigation?.addListener("state", (event) => {
        setNavigationReady(true);
      });
      return function cleanup() {
        if (unsubscribe) {
          unsubscribe();
        }
      };
    }, [rootNavigation]);

    // ---- (9) ----
    React.useEffect(() => {
      if (!isNavigationReady) {
        return;
      }

      const inAuthGroup = segments[0] === "(auth)";

      if (!authInitialized) return;

      if (
        // If the user is not signed in and the initial segment is not anything in the auth group.
        !user &&
        !inAuthGroup
      ) {
        // Redirect to the sign-in page.
        router.push("/sign-in");
      } else if (user && inAuthGroup) {
        // Redirect away from the sign-in page.
        router.push("/");
      }
    }, [user, segments, authInitialized, isNavigationReady]);
  };

  // ---- (10) ----
  useEffect(() => {
    (async () => {
      try {
        const user = await appwrite.account.get();
        console.log(user);
        setAuth(user);
      } catch (error) {
        console.log("error", error);
        setAuth(null);
      }

      setAuthInitialized(true);
      console.log("initialize ", user);
    })();
  }, []);

  /**
   *
   * @returns
   */
  // ---- (11) ----
  const logout = async (): Promise<SignOutResponse> => {
    try {
      const response = await appwrite.account.deleteSession("current");
      return { error: undefined, data: response };
    } catch (error) {
      return { error, data: undefined };
    } finally {
      setAuth(null);
    }
  };

  /**
   *
   * @param email
   * @param password
   * @returns
   */
  // ---- (12) ----
  const login = async (
    email: string,
    password: string
  ): Promise<SignInResponse> => {
    try {
      console.log(email, password);
      const response = await appwrite.account.createEmailSession(
        email,
        password
      );

      const user = await appwrite.account.get();
      setAuth(user);
      return { data: user, error: undefined };
    } catch (error) {
      setAuth(null);
      return { error: error as Error, data: undefined };
    }
  };

  /**
   * 
   * @param email 
   * @param password 
   * @param username 
   * @returns 
   */
  // ---- (13) ----
  const createAcount = async (
    email: string,
    password: string,
    username: string
  ): Promise<SignInResponse> => {
    try {
      console.log(email, password, username);

      // create the user
      await appwrite.account.create(
        appwrite.ID.unique(),
        email,
        password,
        username
      );

      // create the session by logging in
      await appwrite.account.createEmailSession(email, password);

      // get Account information for the user
      const user = await appwrite.account.get();
      setAuth(user);
      return { data: user, error: undefined };
    } catch (error) {
      setAuth(null);
      return { error: error as Error, data: undefined };
    }
  };

  useProtectedRoute(user);

  // ---- (14) ----
  return (
    <AuthContext.Provider
      value={{
        signIn: login,
        signOut: logout,
        signUp: createAcount,
        user,
        authInitialized,
      }}
    >
      {props.children}
    </AuthContext.Provider>
  );
}

// Define the useAuth hook
// ---- (15) ----
export const useAuth = () => {
  const authContext = useContext(AuthContext);

  if (!authContext) {
    throw new Error("useAuth must be used within an AuthContextProvider");
  }

  return authContext;
};

```
1) Imports
2) Interfaces for defining the shape of the data returned by the context and responses from the API calls that are utilized in the provider and exposed to the application. The api calls are all setup to return `data` and `error`.
3) The `AuthContext` is created using React.createContext, and the initial value is set to undefined. This context will hold the authentication-related functions and values to be shared with other components.
4)The Provider component is exported. It serves as the provider for the authentication context and wraps its children with the `AuthContext.Provider` component.
5) Set state variables for the Provider, we keep track of the user with `user` and we set the variable using `setAuth`. We use `authInitialized` to let us know when the application has completed it's check for an existing session that hasn't expired
6) `useProtectedRouter` is hook called to properly redirect the application to the `sign-in` route if there is no valid session.
7) local state variables for the hook. `isNavigationReady` is a check to make sure the navigation is all set up before we attempt to route anywhere in the application
8) `useEffect` to set up the listener for the `rootNavigation` state
9) `useEffect` to route user to proper app location based on what segment the route is in and what the `user` state variable is set to. This is not called unless `authInitialized` and `isNavigationReady`
10) `useEffect` call when the component is mounted to see if there is a valid user session, We use the appwrite API `appwrite.account.get()` and set `user` state with `setAuth`
11) use Appwrite API call `appwrite.account.deleteSession` to log the user out
12) use Appwrite API call `appwrite.account. createEmailSession` to log the user out
13) `createAccount` is a bit more detailed, you need to first create the user account using `appwrite.account.create()`, the log the user in using the `appwrite.account.createEmailSession` api call and then finally get the user information with `appwrite.account.get()` and set `user` state with `setAuth`
14) The `Provider` component returns the `AuthContext.Provider` component, which wraps the props.children. It provides the authentication-related values and functions as the context value.
15) The `useAuth` hook is exported, which allows other components to access the authentication context and retrieve the authentication-related values and functions.

### Controlling Navigation Stack In _layout.tsx
This code represents the root layout of a mobile application using React Native and Expo. It sets up the navigation, themes, fonts, and authentication context for the application.

In the code below, the important part is how the code renders the application's content wrapped in the authentication Provider. We need to wrap it in the `Provider` so we have access to the `useAuth` hook in the `RootLayoutNav`

```react
// /app/_layout.tsx
export default function RootLayout() {
  const [loaded, error] = useFonts({
    SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
    ...FontAwesome.font,
  });

  // Expo Router uses Error Boundaries to catch errors in the navigation tree.
  useEffect(() => {
    if (error) throw error;
  }, [error]);

  useEffect(() => {
    if (loaded) {
      SplashScreen.hideAsync();
    }
  }, [loaded]);

  if (!loaded) {
    return null;
  }

  return (
    <Provider>
      <RootLayoutNav />
    </Provider>
  );
}
```

In the code below we use the useAuth hook to check if the authentication has been initialized and if a user is authenticated. If the authentication is not initialized or there is no user, it returns null to render nothing. Otherwise, it renders the application's content wrapped in the ThemeProvider and sets up two screens in a Stack navigator: "(tabs)" and "modal".
```react
// /app/_layout.tsx
function RootLayoutNav() {
  const colorScheme = useColorScheme();
  const { authInitialized, user } = useAuth();

  if (!authInitialized && !user) return null;
  return (
    <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
      <Stack screenOptions={{ headerShown: false }}>
        <Stack.Screen name="(tabs)" />
        <Stack.Screen name="modal" options={{ presentation: "modal" }} />
      </Stack>
    </ThemeProvider>
  );
}
```
## The (auth) Segment
The folder named `(auth)` is where we place the `sign-in` and `sign-up` screens. Since we are using file based routing we just need to place the files in the folder and expo router does the rest for us.

**Sign In Page**
```react
// /app/(auth)/sign-in
import {
  Text,
  TextInput,
  View,
  StyleSheet,
  TouchableOpacity,
} from "react-native";
import { useAuth } from "../context/auth";
import { Stack, useRouter } from "expo-router";
import { useRef } from "react";

export default function SignIn() {
  const { signIn } = useAuth();
  const router = useRouter();

  const emailRef = useRef("");
  const passwordRef = useRef("");
  return (
    <>
      <Stack.Screen options={{ title: "sign up", headerShown: false }} />
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <View>
          <Text style={styles.label}>Email</Text>
          <TextInput
            placeholder="email"
            autoCapitalize="none"
            nativeID="email"
            onChangeText={(text) => {
              emailRef.current = text;
            }}
            style={styles.textInput}
          />
        </View>
        <View>
          <Text style={styles.label}>Password</Text>
          <TextInput
            placeholder="password"
            secureTextEntry={true}
            nativeID="password"
            onChangeText={(text) => {
              passwordRef.current = text;
            }}
            style={styles.textInput}
          />
        </View>

        <TouchableOpacity
          onPress={async () => {
            const { data, error } = await signIn(
              emailRef.current,
              passwordRef.current
            );
            if (data) {
              router.replace("/");
            } else {
              console.log(error);
              // Alert.alert("Login Error", resp.error?.message);
            }
          }}
          style={styles.button}
        >
          <Text style={styles.buttonText}>Login</Text>
        </TouchableOpacity>
        <View style={{ marginTop: 32 }}>
          <Text
            style={{ fontWeight: "500" }}
            onPress={() => router.push("/sign-up")}
          >
            Click Here To Create A New Account
          </Text>
        </View>
      </View>
    </>
  );
}


const styles = StyleSheet.create({
  label: {
    marginBottom: 4,
    color: "#455fff",
  },
  textInput: {
    width: 250,
    borderWidth: 1,
    borderRadius: 4,
    borderColor: "#455fff",
    paddingHorizontal: 8,
    paddingVertical: 4,
    marginBottom: 16,
  },
  button: {
    backgroundColor: "blue",
    padding: 10,
    width: 250,
    borderRadius: 5,
    marginTop: 16,
  },
  buttonText: {
    color: "white",
    textAlign: "center",
    fontSize: 16,
  },
});

```
Nothing special happening in this file other than importing `useAuth` so we have access to the `signIn` function from the `AuthContext`.

**Sign Up Page**
```react
// /app/(auth)/sign-up
import {
  Text,
  View,
  StyleSheet,
  TextInput,
  TouchableOpacity,
} from "react-native";
import { useAuth } from "../context/auth";
import { Stack, useRouter } from "expo-router";
import { useRef } from "react";

export default function SignUp() {
  const { signUp } = useAuth();
  const router = useRouter();

  const emailRef = useRef("");
  const passwordRef = useRef("");
  const userNameRef = useRef("");

  return (
    <>
      <Stack.Screen options={{ title: "sign up", headerShown: false }} />
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <View>
          <Text style={styles.label}>UserName</Text>
          <TextInput
            placeholder="Username"
            autoCapitalize="none"
            nativeID="userName"
            onChangeText={(text) => {
              userNameRef.current = text;
            }}
            style={styles.textInput}
          />
        </View>
        <View>
          <Text style={styles.label}>Email</Text>
          <TextInput
            placeholder="email"
            autoCapitalize="none"
            nativeID="email"
            onChangeText={(text) => {
              emailRef.current = text;
            }}
            style={styles.textInput}
          />
        </View>
        <View>
          <Text style={styles.label}>Password</Text>
          <TextInput
            placeholder="password"
            secureTextEntry={true}
            nativeID="password"
            onChangeText={(text) => {
              passwordRef.current = text;
            }}
            style={styles.textInput}
          />
        </View>

        <TouchableOpacity
          onPress={async () => {
            const { data, error } = await signUp(
              emailRef.current,
              passwordRef.current,
              userNameRef.current
            );
            if (data) {
              router.replace("/");
            } else {
              console.log(error);
              // Alert.alert("Login Error", resp.error?.message);
            }
          }}
          style={styles.button}
        >
          <Text style={styles.buttonText}>Create Account</Text>
        </TouchableOpacity>

        <View style={{ marginTop: 32 }}>
          <Text
            style={{ fontWeight: "500" }}
            onPress={() => router.replace("/sign-in")}
          >
            Click Here To Return To Sign In Page
          </Text>
        </View>
      </View>
    </>
  );
}

const styles = StyleSheet.create({
  label: {
    marginBottom: 4,
    color: "#455fff",
  },
  textInput: {
    width: 250,
    borderWidth: 1,
    borderRadius: 4,
    borderColor: "#455fff",
    paddingHorizontal: 8,
    paddingVertical: 4,
    marginBottom: 16,
  },
  button: {
    backgroundColor: "blue",
    padding: 10,
    width: 250,
    borderRadius: 5,
    marginTop: 16,
  },
  buttonText: {
    color: "white",
    textAlign: "center",
    fontSize: 16,
  },
});

```
Nothing special happening in this file other than importing `useAuth` so we have access to the `signUp` function from the `AuthContext`.

### Handling Sign Out
We modified the first Tab Page content to include a logout button. In the page we once again import `useAuth` to get access to the signOut function.

```react
import { StyleSheet } from 'react-native';

import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
import { useAuth } from '../context/auth';

export default function TabOneScreen() {
  const { signOut, user } = useAuth();
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Tab One</Text>
      <View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
      <EditScreenInfo path="app/(tabs)/index.tsx" />
      <Text onPress={() => signOut()}>Sign Out - {user?.email}</Text>

    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  separator: {
    marginVertical: 30,
    height: 1,
    width: '80%',
  },
});
```

## Wrap Up
This pattern can be used with any account management solution. I used Appwrite only becuase it was something different and I like to mix things up a bit. You could have easily integrated Firebase or Supabase into the AuthContext and the application will work perfectly for you.

I hope you found this helpful, please check out the video and the rest of the content on my [YouTube Channel](https://bit.ly/aks-youtube).


#### Related Links
- Tabs Template v2 - https://docs.expo.dev/routing/installation/
- Environment Variables -  https://docs.expo.dev/guides/environment-variables/
- Expo Router Auth Documentation -  https://docs.expo.dev/router/reference/authentication/

#### Related Videos
- Expo router v1 - https://youtu.be/WNZbARN7lMM
- Expo router v1 Using Firebase - https://youtu.be/Os5_DRhN2Aw



## Full Source Code
{% github aaronksaunders/expo-router-v2-authflow-appwrite %}

#### Social Media 
- Twitter - https://twitter.com/aaronksaunders
- Facebook - https://www.facebook.com/ClearlyInnovativeInc
- Instagram - https://www.instagram.com/aaronksaunders/