docs(react-native): cross-platform Apple social sign-in

This commit is contained in:
Massimo De Luisa committed 2025-08-21 18:47:22 +02:00
1 parent cbf62f469f
commit a73d092fa0
20 files changed
+1154 -110

No files matched your search

@@ -305,6 +305,14 @@ hideToc: true
icon: '/docs/img/icons/expo-icon',
hasLightIcon: true
},
{
title: 'Expo React Native Social Auth',
href: '/guides/getting-started/tutorials/with-expo-react-native-social-auth',
description:
'Learn how to implement social authentication in an app with Expo React Native and Supabase Database and Auth functionality.',
icon: '/docs/img/icons/expo-icon',
hasLightIcon: true
},
{
title: 'Android Kotlin',
href: '/guides/getting-started/tutorials/with-kotlin',
@@ -1,11 +1,14 @@
---
title: 'Build an Social Auth App with Expo React Native'
description: 'Learn how to use Supabase in your React Native App for Social Authentication.'
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.'
---
<$Partial path="quickstart_intro.mdx" />
This tutorial demonstrates how to build a product management app with social authentication. The app showcases user authentication, data management, and photo uploads using:
![Supabase Social Auth example](/docs/img/supabase-flutter-demo.png)
- [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.png)
<Admonition type="note">
@@ -17,12 +20,11 @@ If you get stuck while working through this guide, refer to the [full example on
## Building the app
Let's start building the React Native app from scratch.
Start building the React Native app from scratch.
### Initialize a React Native app
We can 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):
You can 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
@@ -30,13 +32,13 @@ npx create-expo-app@latest
cd expo-social-auth
```
Then let's install the additional dependencies: [supabase-js](https://github.com/supabase/supabase-js)
Then install the additional dependencies: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npx expo install @supabase/supabase-js @react-native-async-storage/async-storage expo-secure-store expo-splash-screen
```
Now let's create a helper file to initialize the Supabase client for web and React Native, by using a specific [storage adapter](https://docs.expo.dev/develop/user-interface/store-data/) for each platform. We will use the [Expo SecureStore](https://docs.expo.dev/develop/user-interface/store-data/#async-storage) for mobile and the [async storage](https://docs.expo.dev/develop/user-interface/store-data/#async-storage) for web.
Now create a helper file to initialize the Supabase client for both web and React Native platforms. You'll use 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.
<Tabs
scrollable
@@ -68,8 +70,8 @@ Now let's create a helper file to initialize the Supabase client for web and Rea
}
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY,
process.env.EXPO_PUBLIC_SUPABASE_URL ?? '',
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '',
{
auth: {
storage: ExpoWebSecureStoreAdapter,
@@ -79,7 +81,6 @@ Now let's create a helper file to initialize the Supabase client for web and Rea
},
},
);
```
</$CodeTabs>
@@ -108,6 +109,9 @@ Now let's create a helper file to initialize the Supabase client for web and Rea
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) => {
@@ -116,8 +120,8 @@ Now let's create a helper file to initialize the Supabase client for web and Rea
}
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY,
process.env.EXPO_PUBLIC_SUPABASE_URL ?? '',
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '',
{
auth: {
storage: ExpoSecureStoreAdapter as any,
@@ -134,12 +138,12 @@ Now let's create a helper file to initialize the Supabase client for web and Rea
</TabPanel>
</Tabs>
### Create a dotenv file containing the Supabase public URL and the anon key
### Set up environment variables
We need the API URL and the `anon` key that you 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.
You need the API URL and the `anon` key that you 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.
So let's create a dotenv file containing the two variables:
Create a `.env` file containing these variables:
<$CodeTabs>
@@ -150,13 +154,13 @@ EXPO_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
</$CodeTabs>
### Protect the navigation
### Set up protected navigation
Now we need to protect the navigation so that users can't access the protected routes without being logged in. We will use the [Expo SplashScreen](https://docs.expo.dev/versions/latest/sdk/splash-screen/) to show a loading screen while fetching the user profile and checking if the user is logged in.
Next, you need to protect your navigation to prevent unauthenticated users from accessing protected routes. You'll 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
Now let's create [a context](https://react.dev/learn/passing-data-deeply-with-context) to manage the authentication session, so that we can access it from any component:
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>
@@ -186,7 +190,7 @@ export const useAuthContext = () => useContext(AuthContext)
#### Create the AuthProvider
Now let's create a provider to manage the authentication session, so that we can access it from any component:
Next, create a provider component to manage the authentication session throughout your app:
<$CodeTabs>
@@ -201,8 +205,11 @@ export default function AuthProvider({ children }: PropsWithChildren) {
const [profile, setProfile] = useState<any>()
const [isLoading, setIsLoading] = useState<boolean>(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) {
@@ -210,6 +217,26 @@ export default function AuthProvider({ children }: PropsWithChildren) {
}
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
@@ -219,20 +246,18 @@ export default function AuthProvider({ children }: PropsWithChildren) {
.single()
setProfile(data)
} else {
setProfile(null)
}
setIsLoading(false)
}
fetchSession()
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
})
}, [])
fetchProfile()
}, [session])
return (
<AuthContext
<AuthContext.Provider
value={{
session,
isLoading,
@@ -241,7 +266,7 @@ export default function AuthProvider({ children }: PropsWithChildren) {
}}
>
{children}
</AuthContext>
</AuthContext.Provider>
)
}
```
@@ -251,7 +276,7 @@ export default function AuthProvider({ children }: PropsWithChildren) {
#### Create the SplashScreenController
Now let's create the `SplashScreenController` to use the Expo SplashScreen if the authentication session is loading, by using the `useAuthContext` hook.
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>
@@ -274,9 +299,106 @@ export function SplashScreenController() {
</$CodeTabs>
### Set up a login component
### Create a logout component
Let's set up a basic React Native component that we will use later to handle logins and sign ups.
Create a logout button component to handle user sign-out:
<$CodeTabs>
```tsx name=components/social-auth-buttons/SignOutButton.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}
/>
);
}
```
</$CodeTabs>
and add it to the `app/(tabs)/index.tsx` file, that will be used to display the user profile data and the logout button:
<$CodeTabs>
```tsx name=app/(tabs)/index.tsx
import { Image } from 'expo-image';
import { StyleSheet } from 'react-native';
import { HelloWave } from '@/components/HelloWave';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import SignOutButton from '@/components/social-auth-buttons/SignOutButton';
import { useAuthContext } from '@/hooks/useAuthContext';
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',
},
});
```
</$CodeTabs>
### Create a login screen
Next, create a basic login screen component:
<$CodeTabs>
@@ -317,11 +439,11 @@ const styles = StyleSheet.create({
</$CodeTabs>
#### Protect the navigation
#### Implement protected routes
Now let's wrap the navigation with the `AuthProvider` and the `SplashScreenController`.
Now wrap your navigation with the `AuthProvider` and `SplashScreenController`.
By leveraging the [protected routes](https://docs.expo.dev/router/advanced/authentication/#using-protected-routes) of the Expo router, we can protect our navigation:
Using [Expo Router's protected routes](https://docs.expo.dev/router/advanced/authentication/#using-protected-routes), you can secure your navigation:
<$CodeTabs>
@@ -332,15 +454,31 @@ import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';
import { SplashScreenController } from '../components/SplashScreenController';
import { SplashScreenController } from '@/components/SplashScreenController';
import { useAuthContext } from '@/hooks/useAuthContext';
import { useColorScheme } from '@/hooks/useColorScheme';
import AuthProvider from '@/providers/AuthProvider';
// 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();
const { isLoggedIn } = useAuthContext();
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
@@ -355,15 +493,7 @@ export default function RootLayout() {
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<AuthProvider>
<SplashScreenController />
<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>
<RootNavigator />
<StatusBar style="auto" />
</AuthProvider>
</ThemeProvider>
@@ -373,14 +503,14 @@ export default function RootLayout() {
</$CodeTabs>
We can now test the app by running:
You can now test the app by running:
```bash
npx expo prebuild
npx expo start --clear
```
and verify that the app is working as expected, that the splash screen is shown while fetching the user profile and that the login page is presented, even if we try to navigate to the home screen by using the `Link` button.
Verify that the app works as expected: the splash screen displays while fetching the user profile, and the login page appears even when attempting to navigate to the home screen using the `Link` button.
<Admonition type="note">
@@ -389,3 +519,503 @@ By default Supabase Auth requires email verification before a session is created
While testing, you can disable email confirmation in your [project's email auth provider settings](/dashboard/project/_/auth/providers).
</Admonition>
## Integrate social authentication
Now integrate social authentication with Supabase Auth.
### Apple authentication
First, add the button that you'll create inside the login screen you created previously:
<$CodeTabs>
```tsx name=app/login.tsx
...
import { AppleSignInButton } from '@/components/social-auth-buttons/apple/AppleSignInButton';
...
export default function LoginScreen() {
return (
<>
<Stack.Screen options={{ title: 'Login' }} />
<ThemedView style={styles.container}>
...
<AppleSignInButton />
...
</ThemedView>
</>
);
}
...
```
</$CodeTabs>
For Apple authentication, you can choose between two options:
- [Expo's AppleAuthentication library](https://docs.expo.dev/versions/latest/sdk/apple-authentication/) - iOS only
- [Invertase's React Native Apple Authentication library](https://github.com/invertase/react-native-apple-authentication) - supports iOS, Android, and Web (using the [react-apple-signin-auth](https://react-apple-signin-auth.ahmedtokyo.com/) web library)
For either option, you'll need to obtain a Service ID from the [Apple Developer Console](https://supabase.com/docs/guides/auth/social-login/auth-apple?queryGroups=framework&framework=nextjs&queryGroups=platform&platform=web#configuration-web).
<Admonition type="note">
To enable Apple sign-up on Android and Web, you'll also need to register the tunneled URL (e.g., `https://arnrer1-anonymous-8081.exp.direct`) obtained by running:
```bash
npx expo run:web --tunnel
```
and add it to the "Allowed Callback URLs" list in your Supabase dashboard Apple provider configuration.
For more information, follow the [Supabase Login with Apple](https://supabase.com/docs/guides/auth/social-login/auth-apple) guide.
</Admonition>
<Tabs
scrollable
size="large"
type="underlined"
defaultActiveId="invertase-react-native-apple-authentication"
queryGroup="apple-authentication"
>
<TabPanel id="invertase-react-native-apple-authentication" label="Invertase">
#### Prerequisites
Before proceeding, ensure you have followed the Invertase prerequisites documented in the [Invertase Initial Setup Guide](https://github.com/invertase/react-native-apple-authentication/blob/main/docs/INITIAL_SETUP.md) and the [Invertase Android Setup Guide](https://github.com/invertase/react-native-apple-authentication/blob/main/docs/ANDROID_EXTRA.md).
You'll also need to add two new environment variables to the `.env` file:
```bash
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:
```bash
npx expo install @invertase/react-native-apple-authentication
```
Then create the iOS specific button component `AppleSignInButton`:
<$CodeTabs>
```tsx name=components/social-auth-buttons/AppleSignInButton.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)/explore')
}
}
}
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()}
/>;
}
```
</$CodeTabs>
<Admonition type="note">
To test functionality on the simulator, remove the `getCredentialStateForUser` check:
<$CodeTabs>
```tsx name=components/social-auth-buttons/AppleSignInButton.ios.tsx
...
const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user);
...
```
</$CodeTabs>
</Admonition>
Enable the Apple authentication capability in iOS:
<$CodeTabs>
```json name=app.json
{
"expo": {
...
"ios": {
...
"usesAppleSignIn": true
...
},
...
}
}
```
</$CodeTabs>
and add the capabilities to the `Info.plist` file by following the [Expo documentation](https://docs.expo.dev/build-reference/ios-capabilities/#xcode).
<Admonition type="note">
Before testing the app, If you've already built the iOS app, clean the project artifacts before testing:
```bash
npx react-native-clean-project clean-project-auto
```
If issues persist, try completely cleaning the cache, as reported by many users in this [closed issue](https://github.com/invertase/react-native-apple-authentication/issues/23).
</Admonition>
Finally, update the iOS project by installing the Pod library and running the Expo prebuild command:
```bash
cd ios
pod install
cd ..
npx expo prebuild
```
Now test the application on a physical device:
```bash
npx expo run:ios --no-build-cache --device
```
You should see the login screen with the Apple authentication button.
<Admonition type="note">
If you get stuck while working through this guide, refer to the [full example guide on GitHub](https://github.com/invertase/react-native-apple-authentication?tab=readme-ov-file#react-native-apple-authentication).
</Admonition>
#### Android
Install the required libraries:
```bash
npx expo install @invertase/react-native-apple-authentication react-native-get-random-values uuid
```
Next, create the Android-specific `AppleSignInButton` component:
<$CodeTabs>
```tsx name=components/social-auth-buttons/AppleSignInButton.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()}
/>;
}
```
</$CodeTabs>
We should now be able to test the authentication by running it on a phisical device or simulator:
```bash
npx expo run:android --no-build-cache
```
#### Web
Install the required libraries:
```bash
npx expo install react-apple-signin-auth
```
Next, create the Web-specific `AppleSignInButton` component:
<$CodeTabs>
```tsx name=components/social-auth-buttons/AppleSignInButton.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,
}}
onSuccess={onAppleButtonSuccess}
/>;
}
```
</$CodeTabs>
Test the authentication in your browser using the tunneled HTTPS URL:
```bash
npx expo start --tunnel
```
</TabPanel>
<TabPanel id="expo-apple-authentication" label="Expo">
#### Prerequisites
Before proceeding, ensure you have followed the Expo prerequisites documented in the [Expo Setup Guide](https://docs.expo.dev/versions/latest/sdk/apple-authentication/).
#### iOS
Install the `expo-apple-authentication` library:
```bash
npx expo install expo-apple-authentication
```
Enable the Apple authentication capability in iOS and the plugin in `app.json`:
<$CodeTabs>
```json name=app.json
{
"expo": {
...
"ios": {
...
"usesAppleSignIn": true
...
},
"plugins": ["expo-apple-authentication"]
...
}
}
```
</$CodeTabs>
Then create the iOS specific button component `AppleSignInButton`:
<$CodeTabs>
```tsx name=components/social-auth-buttons/AppleSignInButton.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)/explore')
}
}
}
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()}
/>;
}
```
</$CodeTabs>
<Admonition type="note">
The Expo Apple Sign In button does not support the Simulator, so you will need to test it on a physical device.
</Admonition>
</TabPanel>
</Tabs>
Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

@@ -252,6 +252,20 @@ export const MOBILES: ConnectionType[] = [
},
],
},
{
key: 'exporeactnativesocialauth',
label: 'Expo React Native Social Auth',
icon: 'expo',
guideLink: 'https://supabase.com/docs/guides/getting-started/tutorials/with-expo-react-native-social-auth',
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'flutter',
label: 'Flutter',
+10
View File
@@ -1831,11 +1831,21 @@ module.exports = [
source: '/docs/guides/with-expo',
destination: '/docs/guides/getting-started/tutorials/with-expo-react-native',
},
{
permanent: true,
source: '/docs/guides/with-expo-social-auth',
destination: '/docs/guides/getting-started/tutorials/with-expo-react-native-social-auth',
},
{
permanent: true,
source: '/docs/guides/getting-started/tutorials/with-expo',
destination: '/docs/guides/getting-started/tutorials/with-expo-react-native',
},
{
permanent: true,
source: '/docs/guides/getting-started/tutorials/with-expo-social-auth',
destination: '/docs/guides/getting-started/tutorials/with-expo-react-native-social-auth',
},
{
permanent: true,
source: '/docs/guides/with-kotlin',
@@ -1,2 +1,4 @@
EXPO_PUBLIC_SUPABASE_URL=""
EXPO_PUBLIC_SUPABASE_ANON_KEY=""
EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID=""
EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI=""
+3 -1
View File
@@ -11,6 +11,7 @@
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.anonymous.exposocialauth"
"usesAppleSignIn": true,
},
"android": {
"adaptiveIcon": {
@@ -36,7 +37,8 @@
"backgroundColor": "#ffffff"
}
],
"expo-secure-store"
"expo-secure-store",
"expo-apple-authentication"
],
"experiments": {
"typedRoutes": true
@@ -1,12 +1,17 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { StyleSheet } from 'react-native';
import { HelloWave } from '@/components/HelloWave';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import SignOutButton from '@/components/social-auth-buttons/SignOutButton';
import { useAuthContext } from '@/hooks/useAuthContext';
export default function HomeScreen() {
const { profile } = useAuthContext();
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
@@ -21,36 +26,12 @@ export default function HomeScreen() {
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
<ThemedText>
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
Press{' '}
<ThemedText type="defaultSemiBold">
{Platform.select({
ios: 'cmd + d',
android: 'cmd + m',
web: 'F12',
})}
</ThemedText>{' '}
to open developer tools.
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
<ThemedText>
{`Tap the Explore tab to learn more about what's included in this starter app.`}
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
<ThemedText>
{`When you're ready, run `}
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
</ThemedText>
<ThemedText type="subtitle">Username</ThemedText>
<ThemedText>{profile?.username}</ThemedText>
<ThemedText type="subtitle">Full name</ThemedText>
<ThemedText>{profile?.full_name}</ThemedText>
</ThemedView>
<SignOutButton />
</ParallaxScrollView>
);
}
+19 -10
View File
@@ -10,9 +10,25 @@ import { useAuthContext } from '@/hooks/useAuthContext';
import { useColorScheme } from '@/hooks/useColorScheme';
import AuthProvider from '@/providers/AuthProvider';
// 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();
const { isLoggedIn } = useAuthContext();
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
@@ -27,17 +43,10 @@ export default function RootLayout() {
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<AuthProvider>
<SplashScreenController />
<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>
<RootNavigator />
<StatusBar style="auto" />
</AuthProvider>
</ThemeProvider>
);
}
+19 -2
View File
@@ -1,8 +1,10 @@
import { Link, Stack } from 'expo-router';
import { StyleSheet } from 'react-native';
import { Platform, StyleSheet } from 'react-native';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import AppleSignInButton from '@/components/social-auth-buttons/apple/AppleSignInButton';
import ExpoAppleSignInButton from '@/components/social-auth-buttons/apple/ExpoAppleSignInButton';
export default function LoginScreen() {
return (
@@ -10,9 +12,21 @@ export default function LoginScreen() {
<Stack.Screen options={{ title: 'Login' }} />
<ThemedView style={styles.container}>
<ThemedText type="title">Login</ThemedText>
<Link href="/" style={styles.link}>
<Link href="/(tabs)/explore" style={styles.link}>
<ThemedText type="link">Try to navigate to home screen!</ThemedText>
</Link>
<ThemedView style={styles.socialAuthButtonsContainer}>
{Platform.OS === 'ios' && (
<>
<ThemedText type="default">Invertase Apple Sign In</ThemedText>
<AppleSignInButton />
<ThemedText type="default">Expo Apple Sign In</ThemedText>
<ExpoAppleSignInButton />
</>
)}
{Platform.OS !== 'ios' && (<AppleSignInButton />)}
</ThemedView>
</ThemedView>
</>
);
@@ -29,4 +43,7 @@ const styles = StyleSheet.create({
marginTop: 15,
paddingVertical: 15,
},
socialAuthButtonsContainer: {
gap: 10,
},
});
@@ -0,0 +1,24 @@
import { useAuthContext } from '@/hooks/useAuthContext';
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() {
const { isLoggedIn } = useAuthContext();
return (
<Button
disabled={!isLoggedIn}
title="Sign out"
onPress={onSignOutButtonPress}
/>
);
}
@@ -0,0 +1,67 @@
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()}
/>;
}
@@ -0,0 +1,51 @@
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)/explore')
}
}
}
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()}
/>;
}
@@ -0,0 +1,66 @@
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,
}}
onSuccess={onAppleButtonSuccess}
/>;
}
@@ -0,0 +1,69 @@
import { supabase } from '@/lib/supabase';
import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js';
import * as AppleAuthentication from 'expo-apple-authentication';
import { router } from 'expo-router';
import { Platform, StyleSheet } from 'react-native';
async function onAppleButtonPress() {
// Performs login request
try {
const appleAuthRequestResponse = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
console.log('Apple sign in successful:', { appleAuthRequestResponse })
if (appleAuthRequestResponse.identityToken && appleAuthRequestResponse.authorizationCode) {
const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = {
provider: 'apple',
token: appleAuthRequestResponse.identityToken,
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)/explore')
}
}
} catch (e: any) {
if (e.code === 'ERR_REQUEST_CANCELED') {
console.error('Error signing in with Apple:', e)
} else {
console.error('Error signing in with Apple:', e)
}
}
}
export default function ExpoAppleSignInButton() {
if (Platform.OS !== 'ios') { return <></> }
return <AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={5}
style={styles.button}
onPress={() => onAppleButtonPress()}
/>
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
button: {
width: 160, height: 45
},
});
@@ -7,6 +7,9 @@ const ExpoSecureStoreAdapter = {
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) => {
@@ -15,8 +18,8 @@ const ExpoSecureStoreAdapter = {
}
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY,
process.env.EXPO_PUBLIC_SUPABASE_URL ?? '',
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '',
{
auth: {
storage: ExpoSecureStoreAdapter as any,
@@ -16,8 +16,8 @@ const ExpoWebSecureStoreAdapter = {
}
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY,
process.env.EXPO_PUBLIC_SUPABASE_URL ?? '',
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '',
{
auth: {
storage: ExpoWebSecureStoreAdapter,
+70 -5
View File
@@ -9,12 +9,14 @@
"version": "1.0.0",
"dependencies": {
"@expo/vector-icons": "^14.1.0",
"@invertase/react-native-apple-authentication": "^2.4.1",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
"@supabase/supabase-js": "^2.55.0",
"expo": "~53.0.20",
"expo-apple-authentication": "~7.2.4",
"expo-blur": "~14.1.5",
"expo-constants": "~17.1.7",
"expo-font": "~13.3.2",
@@ -29,15 +31,18 @@
"expo-system-ui": "~5.0.10",
"expo-web-browser": "~14.2.0",
"react": "19.0.0",
"react-apple-signin-auth": "^1.1.2",
"react-dom": "19.0.0",
"react-native": "0.79.5",
"react-native-gesture-handler": "~2.24.0",
"react-native-get-random-values": "~1.11.0",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5"
"react-native-webview": "13.13.5",
"uuid": "^11.1.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -2333,6 +2338,15 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@invertase/react-native-apple-authentication": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@invertase/react-native-apple-authentication/-/react-native-apple-authentication-2.4.1.tgz",
"integrity": "sha512-SzWXS/T6H62ScTX+jMLJFLAcBp2p8P30J5uQEimpLMUolqnv/iAljfpwTvZYaniVmMSUXjG0/N95ScUIcmsYRQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -6289,6 +6303,16 @@
}
}
},
"node_modules/expo-apple-authentication": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/expo-apple-authentication/-/expo-apple-authentication-7.2.4.tgz",
"integrity": "sha512-T2agaLLPT4Ax97FeXImB7BCCEzEJ0gB+ZwlFa/FXBtbp6WFKcGRlTVKiX2YPYLZzN5QjXcmQ9HHJ17jRthNHMg==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-asset": {
"version": "11.1.7",
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.1.7.tgz",
@@ -6567,6 +6591,12 @@
"integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==",
"license": "Apache-2.0"
},
"node_modules/fast-base64-decode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz",
"integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -10203,6 +10233,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-apple-signin-auth": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/react-apple-signin-auth/-/react-apple-signin-auth-1.1.2.tgz",
"integrity": "sha512-E5bPu4LtNR3IDsd08A/f1Y0HyuHfjqQpRNRCtQQ3JSVby2JK50FoixyK8EwUh6cbu8N4qrJStL77dEb51Ny5uA==",
"license": "MIT",
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
}
},
"node_modules/react-devtools-core": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz",
@@ -10354,6 +10394,18 @@
"react-native": "*"
}
},
"node_modules/react-native-get-random-values": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz",
"integrity": "sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==",
"license": "MIT",
"dependencies": {
"fast-base64-decode": "^1.0.0"
},
"peerDependencies": {
"react-native": ">=0.56"
}
},
"node_modules/react-native-is-edge-to-edge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
@@ -12495,12 +12547,16 @@
}
},
"node_modules/uuid": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
"integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/validate-npm-package-name": {
@@ -12874,6 +12930,15 @@
"node": ">=10.0.0"
}
},
"node_modules/xcode/node_modules/uuid": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
"integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/xml2js": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz",
+6 -1
View File
@@ -12,6 +12,7 @@
},
"dependencies": {
"@expo/vector-icons": "^14.1.0",
"@invertase/react-native-apple-authentication": "^2.4.1",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/elements": "^2.3.8",
@@ -32,15 +33,19 @@
"expo-system-ui": "~5.0.10",
"expo-web-browser": "~14.2.0",
"react": "19.0.0",
"react-apple-signin-auth": "^1.1.2",
"react-dom": "19.0.0",
"react-native": "0.79.5",
"react-native-gesture-handler": "~2.24.0",
"react-native-get-random-values": "~1.11.0",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5"
"react-native-webview": "13.13.5",
"uuid": "^11.1.0",
"expo-apple-authentication": "~7.2.4"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -8,8 +8,11 @@ export default function AuthProvider({ children }: PropsWithChildren) {
const [profile, setProfile] = useState<any>()
const [isLoading, setIsLoading] = useState<boolean>(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) {
@@ -17,6 +20,26 @@ export default function AuthProvider({ children }: PropsWithChildren) {
}
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
@@ -26,20 +49,18 @@ export default function AuthProvider({ children }: PropsWithChildren) {
.single()
setProfile(data)
} else {
setProfile(null)
}
setIsLoading(false)
}
fetchSession()
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
})
}, [])
fetchProfile()
}, [session])
return (
<AuthContext
<AuthContext.Provider
value={{
session,
isLoading,
@@ -48,6 +69,6 @@ export default function AuthProvider({ children }: PropsWithChildren) {
}}
>
{children}
</AuthContext>
</AuthContext.Provider>
)
}