diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 611b514922..2a15b0d4bc 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -620,6 +620,10 @@ export const auth: NavMenuConstant = { name: 'React Native', url: '/guides/auth/quickstarts/react-native' as `/${string}`, }, + { + name: 'React Native with Expo & Social Auth', + url: '/guides/auth/quickstarts/with-expo-react-native-social-auth', + }, ], }, diff --git a/apps/docs/content/guides/auth/quickstarts/with-expo-react-native-social-auth.mdx b/apps/docs/content/guides/auth/quickstarts/with-expo-react-native-social-auth.mdx new file mode 100644 index 0000000000..539e338888 --- /dev/null +++ b/apps/docs/content/guides/auth/quickstarts/with-expo-react-native-social-auth.mdx @@ -0,0 +1,1384 @@ +--- +title: 'Build a Social Auth App with Expo React Native' +description: 'Learn how to implement social authentication in an app with Expo React Native and Supabase Database and Auth functionality.' +--- + +This tutorial demonstrates how to build a React Native app with [Expo](https://expo.dev) that implements social authentication. The app showcases a complete authentication flow with protected navigation using: + +- [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data with [Row Level Security](/docs/guides/auth#row-level-security) to ensure data is protected and users can only access their own information. +- [Supabase Auth](/docs/guides/auth) - enables users to log in through social authentication providers (Apple and Google). + +![Supabase Social Auth example](/docs/img/supabase-expo-social-auth-login.png) + + + +If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/auth/expo-social-auth). + + + +<$Partial path="project_setup.mdx" /> + +## Building the app + +Start by building the React Native app from scratch. + +### Initialize a React Native app + +Use [Expo](https://docs.expo.dev/get-started/create-a-project/) to initialize an app called `expo-social-auth` with the [standard template](https://docs.expo.dev/more/create-expo/#--template): + +```bash +npx create-expo-app@latest + +cd expo-social-auth +``` + +Install the additional dependencies: + +- [supabase-js](https://github.com/supabase/supabase-js) +- [@react-native-async-storage/async-storage](https://github.com/react-native-async-storage/async-storage) - A key-value store for React Native. +- [expo-secure-store](https://docs.expo.dev/versions/latest/sdk/securestore/) - Provides a way to securely store key-value pairs locally on the device. +- [expo-splash-screen](https://docs.expo.dev/versions/latest/sdk/splash-screen/) - Provides a way to programmatically manage the splash screen. + +```bash +npx expo install @supabase/supabase-js @react-native-async-storage/async-storage expo-secure-store expo-splash-screen +``` + +Now, create a helper file to initialize the Supabase client for both web and React Native platforms using platform-specific [storage adapters](https://docs.expo.dev/develop/user-interface/store-data/): [Expo SecureStore](https://docs.expo.dev/develop/user-interface/store-data/#secure-storage) for mobile and [AsyncStorage](https://docs.expo.dev/develop/user-interface/store-data/#async-storage) for web. + + + +{/* TODO: Future task to extract to repo and transclude */} + <$CodeTabs> + + ```ts name=lib/supabase.web.ts + import AsyncStorage from '@react-native-async-storage/async-storage'; + import { createClient } from '@supabase/supabase-js'; + import 'react-native-url-polyfill/auto'; + + const ExpoWebSecureStoreAdapter = { + getItem: (key: string) => { + console.debug("getItem", { key }) + return AsyncStorage.getItem(key) + }, + setItem: (key: string, value: string) => { + return AsyncStorage.setItem(key, value) + }, + removeItem: (key: string) => { + return AsyncStorage.removeItem(key) + }, + }; + + export const supabase = createClient( + process.env.EXPO_PUBLIC_SUPABASE_URL ?? '', + process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '', + { + auth: { + storage: ExpoWebSecureStoreAdapter, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + }, + }, + ); + ``` + + + + + + + If you want to encrypt the user's session information, use `aes-js` and store the encryption key in [Expo SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore). The [`aes-js` library](https://github.com/ricmoo/aes-js) is a reputable JavaScript-only implementation of the AES encryption algorithm in CTR mode. A new 256-bit encryption key is generated using the `react-native-get-random-values` library. This key is stored inside Expo's SecureStore, while the value is encrypted and placed inside AsyncStorage. + + Make sure that: + - You keep the `expo-secure-storage`, `aes-js` and `react-native-get-random-values` libraries up-to-date. + - Choose the correct [`SecureStoreOptions`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestoreoptions) for your app's needs. E.g. [`SecureStore.WHEN_UNLOCKED`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestorewhen_unlocked) regulates when the data can be accessed. + - Carefully consider optimizations or other modifications to the above example, as those can lead to introducing subtle security vulnerabilities. + + Implement a `ExpoSecureStoreAdapter` to pass in as Auth storage adapter for the `supabase-js` client: + + <$CodeTabs> + + ```ts name=lib/supabase.ts + import { createClient } from '@supabase/supabase-js'; + import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store'; + + const ExpoSecureStoreAdapter = { + getItem: (key: string) => { + console.debug("getItem", { key, getItemAsync }) + return getItemAsync(key) + }, + setItem: (key: string, value: string) => { + if (value.length > 2048) { + console.warn('Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.') + } + return setItemAsync(key, value) + }, + removeItem: (key: string) => { + return deleteItemAsync(key) + }, + }; + + export const supabase = createClient( + process.env.EXPO_PUBLIC_SUPABASE_URL ?? '', + process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '', + { + auth: { + storage: ExpoSecureStoreAdapter as any, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + }, + }, + ); + ``` + + + + + + +### Set up environment variables + +You need the API URL and the `anon` key copied [earlier](#get-the-api-keys). +These variables are safe to expose in your Expo app since Supabase has [Row Level Security](/docs/guides/database/postgres/row-level-security) enabled on your database. + +Create a `.env` file containing these variables: + +<$CodeTabs> + +```bash name=.env +EXPO_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL +EXPO_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY +``` + + + +### Set up protected navigation + +Next, you need to protect app navigation to prevent unauthenticated users from accessing protected routes. Use the [Expo `SplashScreen`](https://docs.expo.dev/versions/latest/sdk/splash-screen/) to display a loading screen while fetching the user profile and verifying authentication status. + +#### Create the `AuthContext` + +Create [a React context](https://react.dev/learn/passing-data-deeply-with-context) to manage the authentication session, making it accessible from any component: + +<$CodeTabs> + +```tsx name=hooks/use-auth-context.tsx +import { Session } from '@supabase/supabase-js' +import { createContext, useContext } from 'react' + +export type AuthData = { + session?: Session | null + profile?: any | null + isLoading: boolean + isLoggedIn: boolean +} + +export const AuthContext = createContext({ + session: undefined, + profile: undefined, + isLoading: true, + isLoggedIn: false, +}) + +export const useAuthContext = () => useContext(AuthContext) +``` + + + +#### Create the `AuthProvider` + +Next, create a provider component to manage the authentication session throughout the app: + +<$CodeTabs> + +```tsx name=providers/auth-provider.tsx +import { AuthContext } from '@/hooks/use-auth-context' +import { supabase } from '@/lib/supabase' +import type { Session } from '@supabase/supabase-js' +import { PropsWithChildren, useEffect, useState } from 'react' + +export default function AuthProvider({ children }: PropsWithChildren) { + const [session, setSession] = useState() + const [profile, setProfile] = useState() + const [isLoading, setIsLoading] = useState(true) + + // Fetch the session once, and subscribe to auth state changes + useEffect(() => { + const fetchSession = async () => { + setIsLoading(true) + + const { + data: { session }, + error, + } = await supabase.auth.getSession() + + if (error) { + console.error('Error fetching session:', error) + } + + setSession(session) + setIsLoading(false) + } + + fetchSession() + + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + console.log('Auth state changed:', { event: _event, session }) + setSession(session) + }) + + // Cleanup subscription on unmount + return () => { + subscription.unsubscribe() + } + }, []) + + // Fetch the profile when the session changes + useEffect(() => { + const fetchProfile = async () => { + setIsLoading(true) + + if (session) { + const { data } = await supabase + .from('profiles') + .select('*') + .eq('id', session.user.id) + .single() + + setProfile(data) + } else { + setProfile(null) + } + + setIsLoading(false) + } + + fetchProfile() + }, [session]) + + return ( + + {children} + + ) +} +``` + + + +#### Create the `SplashScreenController` + +Create a `SplashScreenController` component to display the [Expo `SplashScreen`](https://docs.expo.dev/versions/latest/sdk/splash-screen/) while the authentication session is loading: + +<$CodeTabs> + +```tsx name=components/splash-screen-controller.tsx +import { useAuthContext } from '@/hooks/use-auth-context' +import { SplashScreen } from 'expo-router' + +SplashScreen.preventAutoHideAsync() + +export function SplashScreenController() { + const { isLoading } = useAuthContext() + + if (!isLoading) { + SplashScreen.hideAsync() + } + + return null +} +``` + + + +### Create a logout component + +Create a logout button component to handle user sign-out: + +<$CodeTabs> + +```tsx name=components/social-auth-buttons/sign-out-button.tsx +import { supabase } from '@/lib/supabase' +import React from 'react' +import { Button } from 'react-native' + +async function onSignOutButtonPress() { + const { error } = await supabase.auth.signOut() + + if (error) { + console.error('Error signing out:', error) + } +} + +export default function SignOutButton() { + return