mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 22:29:11 +03:00
docs(react-native): cross-platform Apple social sign-in
This commit is contained in:
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',
|
||||
|
||||
+679
-49
@@ -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 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).
|
||||
|
||||

|
||||
|
||||
<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',
|
||||
|
||||
@@ -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=""
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+67
@@ -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()}
|
||||
/>;
|
||||
}
|
||||
+51
@@ -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()}
|
||||
/>;
|
||||
}
|
||||
+66
@@ -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}
|
||||
/>;
|
||||
}
|
||||
+69
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user