Skip to content

Updated

Build a Social Sign-in App with Expo React Native

This tutorial demonstrates how to build a React Native app with Expo that implements social sign-in. The app showcases a complete authentication flow with protected navigation using:

  • Supabase Database - a Postgres database for storing your user data with Row Level Security to ensure data is protected and users can only access their own information.
  • Supabase Auth - enables users to sign in through social sign-in providers (Apple and Google).

The app's login screen in a web browser, an Android emulator, and an iPhone simulator. Each screen shows the Supabase logo, a Login heading, and buttons to sign in with Apple and Google.

Project setup

Before you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a "schema" inside the database.

Create a project

  1. Create a new project in the Supabase Dashboard.
  2. Enter your project details.
  3. Wait for the new database to launch.

Set up the database schema

Now set up the database schema. You can use the "User Management Starter" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.

  1. Go to the SQL Editor page in the Dashboard.
  2. Click User Management Starter under the Reference > Examples tab.
  3. Click Run.
supabase migration new user_management_starter
-- Create a table for public profiles
create table profiles (
id uuid references auth.users not null primary key,
updated_at timestamp with time zone,
username text unique,
full_name text,
avatar_url text,
website text,
constraint username_length check (char_length(username) >= 3)
);
-- Grant the privileges roles need
GRANT SELECT ON public.profiles TO anon;
GRANT SELECT, INSERT, UPDATE ON public.profiles TO authenticated;
-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/database/postgres/row-level-security for more details.
alter table profiles
enable row level security;
create policy "Public profiles are viewable by everyone." on profiles
for select using (true);
create policy "Users can insert their own profile." on profiles
for insert with check ((select auth.uid()) = id);
create policy "Users can update own profile." on profiles
for update using ((select auth.uid()) = id);
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger
set search_path = ''
as $$
begin
insert into public.profiles (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
-- Set up access controls for storage. Allows downloading object with public key
-- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.
create policy "Avatar images are publicly accessible." on storage.objects
for select using (bucket_id = 'avatars' and storage.allow_any_operation(array['object.get_authenticated_info', 'object.get_authenticated']));
create policy "Anyone can upload an avatar." on storage.objects
for insert with check (bucket_id = 'avatars');
create policy "Anyone can update their own avatar." on storage.objects
for update using ((select auth.uid()) = owner) with check (bucket_id = 'avatars');

Get API details

To interact with data in database tables, you use the client libraries that wrap the auto-generated Data API endpoints, authenticating using the Project URL and key from the project Connect dialog.

Building the app

Start by building the React Native app from scratch.

Initialize a React Native app

Use Expo to initialize an app called expo-social-auth with the standard template:

npx create-expo-app@latest
cd expo-social-auth

Install the additional dependencies:

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: Expo SecureStore for mobile and AsyncStorage for web.

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 isSSR = typeof window === 'undefined'
const ExpoWebSecureStoreAdapter = {
getItem: (key: string) => {
if (isSSR) return null
console.debug('getItem', { key })
return AsyncStorage.getItem(key)
},
setItem: (key: string, value: string) => {
if (isSSR) return
return AsyncStorage.setItem(key, value)
},
removeItem: (key: string) => {
if (isSSR) return
return AsyncStorage.removeItem(key)
},
}
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL ?? '',
process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? '',
{
auth: {
storage: ExpoWebSecureStoreAdapter,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
)
View source

If you want to encrypt the user's session information, use aes-js and store the encryption key in Expo SecureStore. The aes-js library 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 for your app's needs. E.g. SecureStore.WHEN_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:

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_PUBLISHABLE_KEY ?? '',
{
auth: {
storage: ExpoSecureStoreAdapter as any,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
)
View source

Set up environment variables

You need the API URL and the publishable key copied earlier. These variables are safe to expose in your Expo app since Supabase has Row Level Security enabled on your database.

Create a .env file containing these variables:

.env
EXPO_PUBLIC_SUPABASE_URL=""
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=""
EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID=""
EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI=""
EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID=""
View source

Set up protected navigation

Next, you need to protect app navigation to prevent unauthenticated users from accessing protected routes. Use the Expo SplashScreen to display a loading screen while fetching the user profile and verifying authentication status.

Create the AuthContext

Create a React context to manage the authentication session, making it accessible from any component:

hooks/use-auth-context.tsx
import { createContext, useContext } from 'react'
export type AuthData = {
claims?: Record<string, any> | null
profile?: any | null
isLoading: boolean
isLoggedIn: boolean
}
export const AuthContext = createContext<AuthData>({
claims: undefined,
profile: undefined,
isLoading: true,
isLoggedIn: false,
})
export const useAuthContext = () => useContext(AuthContext)
View source

Create the AuthProvider

Next, create a provider component to manage the authentication session throughout the app:

providers/auth-provider.tsx
import { AuthContext } from '@/hooks/use-auth-context'
import { supabase } from '@/lib/supabase'
import { PropsWithChildren, useEffect, useState } from 'react'
export default function AuthProvider({ children }: PropsWithChildren) {
const [claims, setClaims] = useState<Record<string, any> | undefined | null>()
const [profile, setProfile] = useState<any>()
const [isLoading, setIsLoading] = useState<boolean>(true)
// Fetch the claims once, and subscribe to auth state changes
useEffect(() => {
const fetchClaims = async () => {
setIsLoading(true)
const { data, error } = await supabase.auth.getClaims()
if (error) {
console.error('Error fetching claims:', error)
}
setClaims(data?.claims ?? null)
setIsLoading(false)
}
fetchClaims()
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(async (_event, _session) => {
console.log('Auth state changed:', { event: _event })
const { data } = await supabase.auth.getClaims()
setClaims(data?.claims ?? null)
})
// Cleanup subscription on unmount
return () => {
subscription.unsubscribe()
}
}, [])
// Fetch the profile when the claims change
useEffect(() => {
const fetchProfile = async () => {
setIsLoading(true)
if (claims) {
const { data } = await supabase.from('profiles').select('*').eq('id', claims.sub).single()
setProfile(data)
} else {
setProfile(null)
}
setIsLoading(false)
}
fetchProfile()
}, [claims])
return (
<AuthContext.Provider
value={{
claims,
isLoading,
profile,
isLoggedIn: claims != undefined,
}}
>
{children}
</AuthContext.Provider>
)
}
View source

Create the SplashScreenController

Create a SplashScreenController component to display the Expo SplashScreen while the authentication session is loading:

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
}
View source

Create a sign-out component

Create a sign-out button component to handle user sign-out:

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 <Button title="Sign out" onPress={onSignOutButtonPress} />
}
View source

And add it to the app/(tabs)/index.tsx file used to display the user profile data and the sign-out button:

app/(tabs)/index.tsx
import { Image } from 'expo-image'
import { StyleSheet } from 'react-native'
import { HelloWave } from '@/components/hello-wave'
import ParallaxScrollView from '@/components/parallax-scroll-view'
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import SignOutButton from '@/components/social-auth-buttons/sign-out-button'
import { useAuthContext } from '@/hooks/use-auth-context'
export default function HomeScreen() {
const { profile } = useAuthContext()
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
headerImage={
<Image
source={require('@/assets/images/partial-react-logo.png')}
style={styles.reactLogo}
/>
}
>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Welcome!</ThemedText>
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Username</ThemedText>
<ThemedText>{profile?.username}</ThemedText>
<ThemedText type="subtitle">Full name</ThemedText>
<ThemedText>{profile?.full_name}</ThemedText>
</ThemedView>
<SignOutButton />
</ParallaxScrollView>
)
}
const styles = StyleSheet.create({
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
stepContainer: {
gap: 8,
marginBottom: 8,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
position: 'absolute',
},
})
View source

Create a sign-in screen

Next, create a basic sign-in screen component:

app/login.tsx
import { Link, Stack } from 'expo-router'
import { StyleSheet } from 'react-native'
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
export default function LoginScreen() {
return (
<>
<Stack.Screen options={{ title: 'Login' }} />
<ThemedView style={styles.container}>
<ThemedText type="title">Login</ThemedText>
<Link href="/" style={styles.link}>
<ThemedText type="link">Try to navigate to home screen!</ThemedText>
</Link>
</ThemedView>
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
link: {
marginTop: 15,
paddingVertical: 15,
},
})
View source

Implement protected routes

Wrap the navigation with the AuthProvider and SplashScreenController.

Using Expo Router's protected routes, you can secure navigation:

app/\_layout.tsx
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import 'react-native-reanimated'
import { SplashScreenController } from '@/components/splash-screen-controller'
import { useAuthContext } from '@/hooks/use-auth-context'
import { useColorScheme } from '@/hooks/use-color-scheme'
import AuthProvider from '@/providers/auth-provider'
// Separate RootNavigator so we can access the AuthContext
function RootNavigator() {
const { isLoggedIn } = useAuthContext()
return (
<Stack>
<Stack.Protected guard={isLoggedIn}>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack.Protected>
<Stack.Protected guard={!isLoggedIn}>
<Stack.Screen name="login" options={{ headerShown: false }} />
</Stack.Protected>
<Stack.Screen name="+not-found" />
</Stack>
)
}
export default function RootLayout() {
const colorScheme = useColorScheme()
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<AuthProvider>
<SplashScreenController />
<RootNavigator />
<StatusBar style="auto" />
</AuthProvider>
</ThemeProvider>
)
}
View source

You can now test the app by running:

npx expo prebuild
npx expo start --clear

Verify that the app works as expected. The splash screen displays while fetching the user profile, and the sign-in page appears even when attempting to navigate to the home screen using the Link button.

Integrate social sign-in

Now integrate social sign-in with Supabase Auth, starting with Apple authentication. If you only need to implement Google authentication, you can skip to the Google authentication section.

Apple authentication

Start by adding the button inside the sign-in screen:

…
import AppleSignInButton from '@/components/social-auth-buttons/apple/apple-sign-in-button';
…
export default function LoginScreen() {
return (
<>
<Stack.Screen options={{ title: 'Login' }} />
<ThemedView style={styles.container}>
…
<AppleSignInButton />
…
</ThemedView>
</>
);
}
…

For Apple authentication, you can choose between:

For either option, you need to obtain a Service ID from the Apple Developer Console.

Prerequisites

Before proceeding, ensure you have followed the Invertase prerequisites documented in the Invertase Initial Setup Guide and the Invertase Android Setup Guide.

You need to add two new environment variables to the .env file:

EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID="YOUR_APPLE_AUTH_SERVICE_ID"
EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI="YOUR_APPLE_AUTH_REDIRECT_URI"

iOS

Install the @invertase/react-native-apple-authentication library:

npx expo install @invertase/react-native-apple-authentication

Then create the iOS specific button component AppleSignInButton:

components/social-auth-buttons/apple/apple-sign-in-button.ios.tsx
import { supabase } from '@/lib/supabase'
import { AppleButton, appleAuth } from '@invertase/react-native-apple-authentication'
import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js'
import { router } from 'expo-router'
import { Platform } from 'react-native'
async function onAppleButtonPress() {
// Performs login request
const appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: appleAuth.Operation.LOGIN,
// Note: it appears putting FULL_NAME first is important, see issue #293
requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL],
})
// Get the current authentication state for user
// Note: This method must be tested on a real device. On the iOS simulator it always throws an error.
const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user)
console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse })
if (
credentialState === appleAuth.State.AUTHORIZED &&
appleAuthRequestResponse.identityToken &&
appleAuthRequestResponse.authorizationCode
) {
const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = {
provider: 'apple',
token: appleAuthRequestResponse.identityToken,
nonce: appleAuthRequestResponse.nonce,
access_token: appleAuthRequestResponse.authorizationCode,
}
const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials)
if (error) {
console.error('Error signing in with Apple:', error)
}
if (data) {
console.log('Apple sign in successful:', data)
router.navigate('/(tabs)')
}
}
}
export default function AppleSignInButton() {
if (Platform.OS !== 'ios') {
return <></>
}
return (
<AppleButton
buttonStyle={AppleButton.Style.BLACK}
buttonType={AppleButton.Type.SIGN_IN}
style={{ width: 160, height: 45 }}
onPress={() => onAppleButtonPress()}
/>
)
}
View source

Enable the Apple authentication capability in iOS:

{
"expo": {
…
"ios": {
…
"usesAppleSignIn": true
…
},
…
}
}

Add the capabilities to the Info.plist file by following the Expo documentation.

Finally, update the iOS project by installing the Pod library and running the Expo prebuild command:

cd ios
pod install
cd ..
npx expo prebuild

Now test the application on a physical device:

npx expo run:ios --no-build-cache --device

You should see the sign-in screen with the Apple authentication button.

Android

Install the required libraries:

npx expo install @invertase/react-native-apple-authentication react-native-get-random-values uuid

Next, create the Android-specific AppleSignInButton component:

components/social-auth-buttons/apple/apple-sign-in-button.android.tsx
import { supabase } from '@/lib/supabase'
import { appleAuthAndroid, AppleButton } from '@invertase/react-native-apple-authentication'
import { SignInWithIdTokenCredentials } from '@supabase/supabase-js'
import { Platform } from 'react-native'
import 'react-native-get-random-values'
import { v4 as uuid } from 'uuid'
async function onAppleButtonPress() {
// Generate secure, random values for state and nonce
const rawNonce = uuid()
const state = uuid()
// Configure the request
appleAuthAndroid.configure({
// The Service ID you registered with Apple
clientId: process.env.EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID ?? '',
// Return URL added to your Apple dev console. We intercept this redirect, but it must still match
// the URL you provided to Apple. It can be an empty route on your backend as it's never called.
redirectUri: process.env.EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI ?? '',
// The type of response requested - code, id_token, or both.
responseType: appleAuthAndroid.ResponseType.ALL,
// The amount of user information requested from Apple.
scope: appleAuthAndroid.Scope.ALL,
// Random nonce value that will be SHA256 hashed before sending to Apple.
nonce: rawNonce,
// Unique state value used to prevent CSRF attacks. A UUID will be generated if nothing is provided.
state,
})
// Open the browser window for user sign in
const credentialState = await appleAuthAndroid.signIn()
console.log('Apple sign in successful:', credentialState)
if (credentialState.id_token && credentialState.code && credentialState.nonce) {
const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = {
provider: 'apple',
token: credentialState.id_token,
nonce: credentialState.nonce,
access_token: credentialState.code,
}
const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials)
if (error) {
console.error('Error signing in with Apple:', error)
}
if (data) {
console.log('Apple sign in successful:', data)
}
}
}
export default function AppleSignInButton() {
if (Platform.OS !== 'android' || appleAuthAndroid.isSupported !== true) {
return <></>
}
return (
<AppleButton
buttonStyle={AppleButton.Style.BLACK}
buttonType={AppleButton.Type.SIGN_IN}
onPress={() => onAppleButtonPress()}
/>
)
}
View source

You should now be able to test the authentication by running it on a physical device or simulator:

npx expo run:android --no-build-cache

Prerequisites

Before proceeding, as per the mobile options you need an Apple Service ID. To obtain it you can follow the Invertase Initial Setup Guide and the Invertase Android Setup Guide mentioned in the Invertase tab.

You also need to add two new environment variables to the .env file:

EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID="YOUR_APPLE_AUTH_SERVICE_ID"
EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI="YOUR_APPLE_AUTH_REDIRECT_URI"

Web

Install the required libraries:

npx expo install react-apple-signin-auth

Next, create the Web-specific AppleSignInButton component:

components/social-auth-buttons/apple/apple-sign-in-button.web.tsx
import { supabase } from '@/lib/supabase'
import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js'
import { useEffect, useState } from 'react'
import AppleSignin, { type AppleAuthResponse } from 'react-apple-signin-auth'
import { Platform } from 'react-native'
export default function AppleSignInButton() {
const [nonce, setNonce] = useState('')
const [sha256Nonce, setSha256Nonce] = useState('')
async function onAppleButtonSuccess(appleAuthRequestResponse: AppleAuthResponse) {
console.debug('Apple sign in successful:', { appleAuthRequestResponse })
if (
appleAuthRequestResponse.authorization &&
appleAuthRequestResponse.authorization.id_token &&
appleAuthRequestResponse.authorization.code
) {
const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = {
provider: 'apple',
token: appleAuthRequestResponse.authorization.id_token,
nonce,
access_token: appleAuthRequestResponse.authorization.code,
}
const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials)
if (error) {
console.error('Error signing in with Apple:', error)
}
if (data) {
console.log('Apple sign in successful:', data)
}
}
}
useEffect(() => {
function generateNonce(): string {
const array = new Uint32Array(1)
window.crypto.getRandomValues(array)
return array[0].toString()
}
async function generateSha256Nonce(nonce: string): Promise<string> {
const buffer = await window.crypto.subtle.digest('sha-256', new TextEncoder().encode(nonce))
const array = Array.from(new Uint8Array(buffer))
return array.map((b) => b.toString(16).padStart(2, '0')).join('')
}
let nonce = generateNonce()
setNonce(nonce)
generateSha256Nonce(nonce).then((sha256Nonce) => {
setSha256Nonce(sha256Nonce)
})
}, [])
if (Platform.OS !== 'web') {
return <></>
}
return (
<AppleSignin
authOptions={{
clientId: process.env.EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID ?? '',
redirectURI: process.env.EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI ?? '',
scope: 'email name',
state: 'state',
nonce: sha256Nonce,
usePopup: true,
}}
uiType="dark"
onSuccess={onAppleButtonSuccess}
onError={(error: any) => console.error('Apple sign in error:', error)}
/>
)
}
View source

Test the authentication in your browser using the tunneled HTTPS URL:

npx expo start --tunnel

Prerequisites

Before proceeding, ensure you have followed the Expo prerequisites documented in the Expo Setup Guide.

iOS

Install the expo-apple-authentication library:

npx expo install expo-apple-authentication

Enable the Apple authentication capability in iOS and the plugin in app.json:

{
"expo": {
…
"ios": {
…
"usesAppleSignIn": true
…
},
"plugins": ["expo-apple-authentication"]
…
}
}

Then create the iOS specific button component AppleSignInButton:

components/social-auth-buttons/apple/apple-sign-in-button.tsx
import { supabase } from '@/lib/supabase'
import { AppleButton, appleAuth } from '@invertase/react-native-apple-authentication'
import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js'
import { router } from 'expo-router'
import { Platform } from 'react-native'
async function onAppleButtonPress() {
// Performs login request
const appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: appleAuth.Operation.LOGIN,
// Note: it appears putting FULL_NAME first is important, see issue #293
requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL],
})
// Get the current authentication state for user
// Note: This method must be tested on a real device. On the iOS simulator it always throws an error.
const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user)
console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse })
if (
credentialState === appleAuth.State.AUTHORIZED &&
appleAuthRequestResponse.identityToken &&
appleAuthRequestResponse.authorizationCode
) {
const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = {
provider: 'apple',
token: appleAuthRequestResponse.identityToken,
nonce: appleAuthRequestResponse.nonce,
access_token: appleAuthRequestResponse.authorizationCode,
}
const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials)
if (error) {
console.error('Error signing in with Apple:', error)
}
if (data) {
console.log('Apple sign in successful:', data)
router.navigate('/(tabs)')
}
}
}
export default function AppleSignInButton() {
if (Platform.OS !== 'ios') {
return <></>
}
return (
<AppleButton
buttonStyle={AppleButton.Style.BLACK}
buttonType={AppleButton.Type.SIGN_IN}
style={{ width: 160, height: 45 }}
onPress={() => onAppleButtonPress()}
/>
)
}
View source

Google authentication

Start by adding the button to the sign-in screen:

…
import GoogleSignInButton from '@/components/social-auth-buttons/google/google-sign-in-button';
…
export default function LoginScreen() {
return (
<>
<Stack.Screen options={{ title: 'Login' }} />
<ThemedView style={styles.container}>
…
<GoogleSignInButton />
…
</ThemedView>
</>
);
}
…

For Google authentication, you can choose between the following options:

For either option, you need to obtain a Web Client ID from the Google Cloud Engine, as explained in the Google Sign In guide.

This guide only uses the @react-oauth/google@latest option for the Web, and the signInWithOAuth for the mobile platforms.

Before proceeding, add a new environment variable to the .env file:

EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID="YOUR_GOOGLE_AUTH_WEB_CLIENT_ID"

Create the mobile generic button component GoogleSignInButton:

components/social-auth-buttons/google/google-sign-in-button.tsx
import { supabase } from '@/lib/supabase'
import { useEffect } from 'react'
import { TouchableOpacity } from 'react-native'
import { expo } from '@/app.json'
import { Text } from '@react-navigation/elements'
import { Image } from 'expo-image'
import * as WebBrowser from 'expo-web-browser'
WebBrowser.maybeCompleteAuthSession()
export default function GoogleSignInButton() {
function extractParamsFromUrl(url: string) {
const parsedUrl = new URL(url)
const hash = parsedUrl.hash.substring(1) // Remove the leading '#'
const params = new URLSearchParams(hash)
return {
access_token: params.get('access_token'),
expires_in: parseInt(params.get('expires_in') || '0'),
refresh_token: params.get('refresh_token'),
token_type: params.get('token_type'),
provider_token: params.get('provider_token'),
code: params.get('code'),
}
}
async function onSignInButtonPress() {
console.debug('onSignInButtonPress - start')
const res = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${expo.scheme}://google-auth`,
queryParams: { prompt: 'consent' },
skipBrowserRedirect: true,
},
})
const googleOAuthUrl = res.data.url
if (!googleOAuthUrl) {
console.error('no oauth url found!')
return
}
const result = await WebBrowser.openAuthSessionAsync(
googleOAuthUrl,
`${expo.scheme}://google-auth`,
{ showInRecents: true }
).catch((err) => {
console.error('onSignInButtonPress - openAuthSessionAsync - error', { err })
console.log(err)
})
console.debug('onSignInButtonPress - openAuthSessionAsync - result', { result })
if (result && result.type === 'success') {
console.debug('onSignInButtonPress - openAuthSessionAsync - success')
const params = extractParamsFromUrl(result.url)
console.debug('onSignInButtonPress - openAuthSessionAsync - success', { params })
if (params.access_token && params.refresh_token) {
console.debug('onSignInButtonPress - setSession')
const { data, error } = await supabase.auth.setSession({
access_token: params.access_token,
refresh_token: params.refresh_token,
})
console.debug('onSignInButtonPress - setSession - success', { data, error })
return
} else {
console.error('onSignInButtonPress - setSession - failed')
// sign in/up failed
}
} else {
console.error('onSignInButtonPress - openAuthSessionAsync - failed')
}
}
// to warm up the browser
useEffect(() => {
WebBrowser.warmUpAsync()
return () => {
WebBrowser.coolDownAsync()
}
}, [])
return (
<TouchableOpacity
onPress={onSignInButtonPress}
style={{
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#ffffff',
borderWidth: 1,
borderColor: '#dbdbdb',
borderRadius: 4,
paddingVertical: 10,
paddingHorizontal: 15,
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2, // For Android shadow
}}
activeOpacity={0.8}
>
<Image
source={{ uri: 'https://developers.google.com/identity/images/g-logo.png' }}
style={{ width: 24, height: 24, marginRight: 10 }}
/>
<Text
style={{
fontSize: 16,
color: '#757575',
fontFamily: 'Roboto-Regular', // Assuming Roboto is available; install via expo-google-fonts or similar if needed
fontWeight: '500',
}}
>
Sign in with Google
</Text>
</TouchableOpacity>
)
}
View source

Finally, update the iOS and Android projects by running the Expo prebuild command:

npx expo prebuild --clean

Now test the application on both iOS and Android:

npx expo run:ios && npx expo run:android

You should see the sign-in screen with the Google authentication button.

The app's home screen after sign-in in a web browser, an Android emulator, and an iPhone simulator. Each screen shows a Welcome heading, the username and full name, a sign-out button, and Home and Explore tabs.

Install the @react-oauth/google library:

npx expo install @react-oauth/google

Enable the expo-web-browser plugin in app.json:

{
"expo": {
…
"plugins": [
…
[
"expo-web-browser",
{
"experimentalLauncherActivity": false
}
]
…
],
…
}
}

Then create the iOS specific button component GoogleSignInButton:

components/social-auth-buttons/google/google-sign-in-button.web.tsx
import { supabase } from '@/lib/supabase'
import { CredentialResponse, GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google'
import { SignInWithIdTokenCredentials } from '@supabase/supabase-js'
import { useEffect, useState } from 'react'
import 'react-native-get-random-values'
export default function GoogleSignInButton() {
// Generate secure, random values for state and nonce
const [nonce, setNonce] = useState('')
const [sha256Nonce, setSha256Nonce] = useState('')
async function onGoogleButtonSuccess(authRequestResponse: CredentialResponse) {
console.debug('Google sign in successful:', { authRequestResponse })
if (authRequestResponse.clientId && authRequestResponse.credential) {
const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = {
provider: 'google',
token: authRequestResponse.credential,
nonce: nonce,
}
const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials)
if (error) {
console.error('Error signing in with Google:', error)
}
if (data) {
console.log('Google sign in successful:', data)
}
}
}
function onGoogleButtonFailure() {
console.error('Error signing in with Google')
}
useEffect(() => {
function generateNonce(): string {
const array = new Uint32Array(1)
window.crypto.getRandomValues(array)
return array[0].toString()
}
async function generateSha256Nonce(nonce: string): Promise<string> {
const buffer = await window.crypto.subtle.digest('sha-256', new TextEncoder().encode(nonce))
const array = Array.from(new Uint8Array(buffer))
return array.map((b) => b.toString(16).padStart(2, '0')).join('')
}
let nonce = generateNonce()
setNonce(nonce)
generateSha256Nonce(nonce).then((sha256Nonce) => {
setSha256Nonce(sha256Nonce)
})
}, [])
return (
<GoogleOAuthProvider
clientId={process.env.EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID ?? ''}
nonce={sha256Nonce}
>
<GoogleLogin
nonce={sha256Nonce}
onSuccess={onGoogleButtonSuccess}
onError={onGoogleButtonFailure}
useOneTap={true}
auto_select={true}
/>
</GoogleOAuthProvider>
)
}
View source

Test the authentication in your browser using the tunnelled HTTPS URL:

npx expo start --tunnel