This blog post is a complimentary resource to support the video on Expo Router v2
In the video, I walk you through the code for an authentication flow using Expo Router and Appwrite.
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!
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/ to get started
Create Account
Click Create Project Button
Enter Project Name
Under "Add Platform", Click "Web App"
Provide a Name For Your App
Be sure to enter "cloud.appwrite.io" as your domain
Install The Appwrite SDK
All Done
Be sure to copy project id, you will need it for the environment file
Create Your Expo SDK 49 Project + Expo Router v2
- see blog for additional information 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
EXPO_PUBLIC_APPWRITE_PROJECT_ID=[your project id]
Then we need to add the code for the library
// /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
This is the link to the original source code for auth context.
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.
// /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
// /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".
// /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
// /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
// /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.
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.
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
aaronksaunders / expo-router-v2-authflow-appwrite
Expo Router - File Based Routing for React Native, tabs template with auth flow using context api
expo-router-v2-authflow-appwrite
Expo Router - File Based Routing for React Native, tabs template with auth flow using context API
Click Here If You Are Looking For Supabase Source Code - SUPABASE AUTH FLOW
This is the source code from the video on Expo Router v2
In the video, I walk you through the code for an authentication flow using Expo Router and Appwrite.
I explain how to set up the environment variables, install the router, and incorporate Apprite 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!
Video
Social Media
- Twitter - https://twitter.com/aaronksaunders
- Facebook - https://www.facebook.com/ClearlyInnovativeInc
- Instagram - https://www.instagram.com/aaronksaunders/
Top comments (12)
I think I'm missing something as when I have a user it falls through to a [...missing] page.
Here I think is my issue as there is a missing edge case in:
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("/");
}
Potentially missing...
if (user && !inAuthGroup) { ...
Everything else works perfectly. Just trying to figure out where I went wrong.
do you have a simple project somewhere i could take a look at?
Yes, sure - where do I share it?
You can DM me on Twitter if you donโt want to post link to project here
Wouldn't let me DM so dropped you an email hope you don't mind. Have a great day man!
i dont see an email?
HYG, github.com/davidlintin/expo-router... here are all the Authentication files let me know if you need more.
this is not a project :-(
Ah sorry, finally figured out GitHub :) [(github.com/davidlintin/expo-router...]
Saved my day!!! Post upgrading to SDK 49 and router v2 I was struggling with authentication issues!! Thank you for the wonderful solution.
Thank you for this tutotial. I tried this solution and replaced it with firebase-auth but I am getting an unmatched route on app load, Been on this for some days, Pls can you help take a look?
do you have a sample project i can take a look at?