diff --git a/.github/workflows/studio-unit-tests.yml b/.github/workflows/studio-unit-tests.yml index ab6d4d0c31538..08ffc95673f7c 100644 --- a/.github/workflows/studio-unit-tests.yml +++ b/.github/workflows/studio-unit-tests.yml @@ -64,6 +64,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: ./apps/studio/coverage/lcov.info base-path: './apps/studio' + fail-on-error: false finish: needs: test diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 611b514922ac1..2a15b0d4bc9c0 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -620,6 +620,10 @@ export const auth: NavMenuConstant = { name: 'React Native', url: '/guides/auth/quickstarts/react-native' as `/${string}`, }, + { + name: 'React Native with Expo & Social Auth', + url: '/guides/auth/quickstarts/with-expo-react-native-social-auth', + }, ], }, diff --git a/apps/docs/content/guides/auth/quickstarts/with-expo-react-native-social-auth.mdx b/apps/docs/content/guides/auth/quickstarts/with-expo-react-native-social-auth.mdx new file mode 100644 index 0000000000000..539e338888095 --- /dev/null +++ b/apps/docs/content/guides/auth/quickstarts/with-expo-react-native-social-auth.mdx @@ -0,0 +1,1384 @@ +--- +title: 'Build a Social Auth App with Expo React Native' +description: 'Learn how to implement social authentication in an app with Expo React Native and Supabase Database and Auth functionality.' +--- + +This tutorial demonstrates how to build a React Native app with [Expo](https://expo.dev) that implements social authentication. The app showcases a complete authentication flow with protected navigation using: + +- [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data with [Row Level Security](/docs/guides/auth#row-level-security) to ensure data is protected and users can only access their own information. +- [Supabase Auth](/docs/guides/auth) - enables users to log in through social authentication providers (Apple and Google). + +![Supabase Social Auth example](/docs/img/supabase-expo-social-auth-login.png) + + + +If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/auth/expo-social-auth). + + + +<$Partial path="project_setup.mdx" /> + +## Building the app + +Start by building the React Native app from scratch. + +### Initialize a React Native app + +Use [Expo](https://docs.expo.dev/get-started/create-a-project/) to initialize an app called `expo-social-auth` with the [standard template](https://docs.expo.dev/more/create-expo/#--template): + +```bash +npx create-expo-app@latest + +cd expo-social-auth +``` + +Install the additional dependencies: + +- [supabase-js](https://github.com/supabase/supabase-js) +- [@react-native-async-storage/async-storage](https://github.com/react-native-async-storage/async-storage) - A key-value store for React Native. +- [expo-secure-store](https://docs.expo.dev/versions/latest/sdk/securestore/) - Provides a way to securely store key-value pairs locally on the device. +- [expo-splash-screen](https://docs.expo.dev/versions/latest/sdk/splash-screen/) - Provides a way to programmatically manage the splash screen. + +```bash +npx expo install @supabase/supabase-js @react-native-async-storage/async-storage expo-secure-store expo-splash-screen +``` + +Now, create a helper file to initialize the Supabase client for both web and React Native platforms using platform-specific [storage adapters](https://docs.expo.dev/develop/user-interface/store-data/): [Expo SecureStore](https://docs.expo.dev/develop/user-interface/store-data/#secure-storage) for mobile and [AsyncStorage](https://docs.expo.dev/develop/user-interface/store-data/#async-storage) for web. + + + +{/* TODO: Future task to extract to repo and transclude */} + <$CodeTabs> + + ```ts name=lib/supabase.web.ts + import AsyncStorage from '@react-native-async-storage/async-storage'; + import { createClient } from '@supabase/supabase-js'; + import 'react-native-url-polyfill/auto'; + + const ExpoWebSecureStoreAdapter = { + getItem: (key: string) => { + console.debug("getItem", { key }) + return AsyncStorage.getItem(key) + }, + setItem: (key: string, value: string) => { + return AsyncStorage.setItem(key, value) + }, + removeItem: (key: string) => { + return AsyncStorage.removeItem(key) + }, + }; + + export const supabase = createClient( + process.env.EXPO_PUBLIC_SUPABASE_URL ?? '', + process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '', + { + auth: { + storage: ExpoWebSecureStoreAdapter, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + }, + }, + ); + ``` + + + + + + + If you want to encrypt the user's session information, use `aes-js` and store the encryption key in [Expo SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore). The [`aes-js` library](https://github.com/ricmoo/aes-js) is a reputable JavaScript-only implementation of the AES encryption algorithm in CTR mode. A new 256-bit encryption key is generated using the `react-native-get-random-values` library. This key is stored inside Expo's SecureStore, while the value is encrypted and placed inside AsyncStorage. + + Make sure that: + - You keep the `expo-secure-storage`, `aes-js` and `react-native-get-random-values` libraries up-to-date. + - Choose the correct [`SecureStoreOptions`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestoreoptions) for your app's needs. E.g. [`SecureStore.WHEN_UNLOCKED`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestorewhen_unlocked) regulates when the data can be accessed. + - Carefully consider optimizations or other modifications to the above example, as those can lead to introducing subtle security vulnerabilities. + + Implement a `ExpoSecureStoreAdapter` to pass in as Auth storage adapter for the `supabase-js` client: + + <$CodeTabs> + + ```ts name=lib/supabase.ts + import { createClient } from '@supabase/supabase-js'; + import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store'; + + const ExpoSecureStoreAdapter = { + getItem: (key: string) => { + console.debug("getItem", { key, getItemAsync }) + return getItemAsync(key) + }, + setItem: (key: string, value: string) => { + if (value.length > 2048) { + console.warn('Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.') + } + return setItemAsync(key, value) + }, + removeItem: (key: string) => { + return deleteItemAsync(key) + }, + }; + + export const supabase = createClient( + process.env.EXPO_PUBLIC_SUPABASE_URL ?? '', + process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '', + { + auth: { + storage: ExpoSecureStoreAdapter as any, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + }, + }, + ); + ``` + + + + + + +### Set up environment variables + +You need the API URL and the `anon` key copied [earlier](#get-the-api-keys). +These variables are safe to expose in your Expo app since Supabase has [Row Level Security](/docs/guides/database/postgres/row-level-security) enabled on your database. + +Create a `.env` file containing these variables: + +<$CodeTabs> + +```bash name=.env +EXPO_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL +EXPO_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY +``` + + + +### Set up protected navigation + +Next, you need to protect app navigation to prevent unauthenticated users from accessing protected routes. Use the [Expo `SplashScreen`](https://docs.expo.dev/versions/latest/sdk/splash-screen/) to display a loading screen while fetching the user profile and verifying authentication status. + +#### Create the `AuthContext` + +Create [a React context](https://react.dev/learn/passing-data-deeply-with-context) to manage the authentication session, making it accessible from any component: + +<$CodeTabs> + +```tsx name=hooks/use-auth-context.tsx +import { Session } from '@supabase/supabase-js' +import { createContext, useContext } from 'react' + +export type AuthData = { + session?: Session | null + profile?: any | null + isLoading: boolean + isLoggedIn: boolean +} + +export const AuthContext = createContext({ + session: undefined, + profile: undefined, + isLoading: true, + isLoggedIn: false, +}) + +export const useAuthContext = () => useContext(AuthContext) +``` + + + +#### Create the `AuthProvider` + +Next, create a provider component to manage the authentication session throughout the app: + +<$CodeTabs> + +```tsx name=providers/auth-provider.tsx +import { AuthContext } from '@/hooks/use-auth-context' +import { supabase } from '@/lib/supabase' +import type { Session } from '@supabase/supabase-js' +import { PropsWithChildren, useEffect, useState } from 'react' + +export default function AuthProvider({ children }: PropsWithChildren) { + const [session, setSession] = useState() + const [profile, setProfile] = useState() + const [isLoading, setIsLoading] = useState(true) + + // Fetch the session once, and subscribe to auth state changes + useEffect(() => { + const fetchSession = async () => { + setIsLoading(true) + + const { + data: { session }, + error, + } = await supabase.auth.getSession() + + if (error) { + console.error('Error fetching session:', error) + } + + setSession(session) + setIsLoading(false) + } + + fetchSession() + + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + console.log('Auth state changed:', { event: _event, session }) + setSession(session) + }) + + // Cleanup subscription on unmount + return () => { + subscription.unsubscribe() + } + }, []) + + // Fetch the profile when the session changes + useEffect(() => { + const fetchProfile = async () => { + setIsLoading(true) + + if (session) { + const { data } = await supabase + .from('profiles') + .select('*') + .eq('id', session.user.id) + .single() + + setProfile(data) + } else { + setProfile(null) + } + + setIsLoading(false) + } + + fetchProfile() + }, [session]) + + return ( + + {children} + + ) +} +``` + + + +#### Create the `SplashScreenController` + +Create a `SplashScreenController` component to display the [Expo `SplashScreen`](https://docs.expo.dev/versions/latest/sdk/splash-screen/) while the authentication session is loading: + +<$CodeTabs> + +```tsx name=components/splash-screen-controller.tsx +import { useAuthContext } from '@/hooks/use-auth-context' +import { SplashScreen } from 'expo-router' + +SplashScreen.preventAutoHideAsync() + +export function SplashScreenController() { + const { isLoading } = useAuthContext() + + if (!isLoading) { + SplashScreen.hideAsync() + } + + return null +} +``` + + + +### Create a logout component + +Create a logout button component to handle user sign-out: + +<$CodeTabs> + +```tsx name=components/social-auth-buttons/sign-out-button.tsx +import { supabase } from '@/lib/supabase' +import React from 'react' +import { Button } from 'react-native' + +async function onSignOutButtonPress() { + const { error } = await supabase.auth.signOut() + + if (error) { + console.error('Error signing out:', error) + } +} + +export default function SignOutButton() { + return + + + + + + ) +} + +export { SupabaseSelectPromo as default } diff --git a/apps/www/components/SupabaseSelectPromo/styles.css b/apps/www/components/SupabaseSelectPromo/styles.css new file mode 100644 index 0000000000000..cd65e478f3830 --- /dev/null +++ b/apps/www/components/SupabaseSelectPromo/styles.css @@ -0,0 +1,8 @@ +/* Font face definitions also used in Launch Week 15 */ +@font-face { + font-family: 'SuisseIntl-Book'; + src: url('/fonts/launchweek/15/SuisseIntl-Book.otf') format('opentype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} diff --git a/apps/www/components/SurveyResults/StateOfStartupsHeader.tsx b/apps/www/components/SurveyResults/StateOfStartupsHeader.tsx index 197802c447dc5..e2ee1cf1a5a0a 100644 --- a/apps/www/components/SurveyResults/StateOfStartupsHeader.tsx +++ b/apps/www/components/SurveyResults/StateOfStartupsHeader.tsx @@ -59,7 +59,6 @@ const TextBlock = ({
{text} diff --git a/apps/www/internals/generate-sitemap.mjs b/apps/www/internals/generate-sitemap.mjs index 3ae050c7fe33e..d884aa295fc39 100644 --- a/apps/www/internals/generate-sitemap.mjs +++ b/apps/www/internals/generate-sitemap.mjs @@ -10,7 +10,7 @@ import prettier from 'prettier' // Constants for CMS integration const CMS_SITE_ORIGIN = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' - ? 'https://supabase.com' + ? 'https://cms.supabase.com' : process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL && typeof process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL === 'string' ? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL?.replace('zone-www-dot-com-git-', 'cms-git-')}` diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index b508e1c7968ec..985b4b3ce98d0 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -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', diff --git a/apps/www/lib/remotePatterns.js b/apps/www/lib/remotePatterns.js index 85300ef1a1afa..8a60102b56ac0 100644 --- a/apps/www/lib/remotePatterns.js +++ b/apps/www/lib/remotePatterns.js @@ -1,6 +1,6 @@ const CMS_SITE_ORIGIN = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' - ? 'https://supabase.com' + ? 'https://cms.supabase.com' : process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL && typeof process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL === 'string' ? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL?.replace('zone-www-dot-com-git-', 'cms-git-')}` @@ -147,6 +147,12 @@ module.exports = [ port: '', pathname: '/dms/image/**', }, + { + protocol: 'https', + hostname: 'cms.supabase.com', + port: '', + pathname: '**', + }, // Dynamically generated CMS patterns based on CMS_SITE_ORIGIN ...generateCMSRemotePatterns(), ] diff --git a/apps/www/pages/state-of-startups.tsx b/apps/www/pages/state-of-startups.tsx index 384616d748322..a4b62f3f7eaf1 100644 --- a/apps/www/pages/state-of-startups.tsx +++ b/apps/www/pages/state-of-startups.tsx @@ -9,11 +9,11 @@ import { Button, cn } from 'ui' import { useFlag } from 'common' import DefaultLayout from '~/components/Layouts/Default' -import { DecorativeProgressBar } from '~/components/SurveyResults/DecorativeProgressBar' import { SurveyChapter } from '~/components/SurveyResults/SurveyChapter' import { SurveyChapterSection } from '~/components/SurveyResults/SurveyChapterSection' import { SurveySectionBreak } from '~/components/SurveyResults/SurveySectionBreak' import { StateOfStartupsHeader } from '~/components/SurveyResults/StateOfStartupsHeader' +import SupabaseSelectPromo from '~/components/SupabaseSelectPromo' import { useSendTelemetryEvent } from '~/lib/telemetry' @@ -262,6 +262,7 @@ function StateOfStartupsPage() { ))} + ) diff --git a/apps/www/public/images/select/supabase-select.svg b/apps/www/public/images/supabase-select/logo.svg similarity index 100% rename from apps/www/public/images/select/supabase-select.svg rename to apps/www/public/images/supabase-select/logo.svg diff --git a/apps/www/public/images/supabase-select/speakers/dylan-field.jpg b/apps/www/public/images/supabase-select/speakers/dylan-field.jpg new file mode 100644 index 0000000000000..a38ebb70b5cfb Binary files /dev/null and b/apps/www/public/images/supabase-select/speakers/dylan-field.jpg differ diff --git a/apps/www/scripts/generateStaticContent.mjs b/apps/www/scripts/generateStaticContent.mjs index 6c0e71fbab4f1..f66940eb17c62 100644 --- a/apps/www/scripts/generateStaticContent.mjs +++ b/apps/www/scripts/generateStaticContent.mjs @@ -8,7 +8,7 @@ import matter from 'gray-matter' const FILENAME_SUBSTRING = 11 // based on YYYY-MM-DD format const CMS_SITE_ORIGIN = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' - ? 'https://supabase.com' + ? 'https://cms.supabase.com' : process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL && typeof process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL === 'string' ? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL?.replace('zone-www-dot-com-git-', 'cms-git-')}` diff --git a/examples/auth/expo-social-auth/.env.template b/examples/auth/expo-social-auth/.env.template new file mode 100644 index 0000000000000..fd2c50b3e0621 --- /dev/null +++ b/examples/auth/expo-social-auth/.env.template @@ -0,0 +1,5 @@ +EXPO_PUBLIC_SUPABASE_URL="" +EXPO_PUBLIC_SUPABASE_ANON_KEY="" +EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID="" +EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI="" +EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID="" diff --git a/examples/auth/expo-social-auth/.gitignore b/examples/auth/expo-social-auth/.gitignore new file mode 100644 index 0000000000000..f610ec0d642fd --- /dev/null +++ b/examples/auth/expo-social-auth/.gitignore @@ -0,0 +1,39 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +app-example diff --git a/examples/auth/expo-social-auth/README.md b/examples/auth/expo-social-auth/README.md new file mode 100644 index 0000000000000..f88db0ccae7f4 --- /dev/null +++ b/examples/auth/expo-social-auth/README.md @@ -0,0 +1,52 @@ +# Welcome to your Expo app šŸ‘‹ + +This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). + +## Get started + +1. Install dependencies + + ```bash + npm install + ``` + +2. Update the `.env` file with your Supabase project's URL and Anon Key. + +3. Start the app + + ```bash + npx expo start + ``` + +In the output, you'll find options to open the app in a + +- [development build](https://docs.expo.dev/develop/development-builds/introduction/) +- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) +- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) +- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo + +You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). + +## Get a fresh project + +When you're ready, run: + +```bash +npm run reset-project +``` + +This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. + +## Learn more + +To learn more about developing your project with Expo, look at the following resources: + +- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). +- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. + +## Join the community + +Join our community of developers creating universal apps. + +- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. +- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. diff --git a/examples/auth/expo-social-auth/app.json b/examples/auth/expo-social-auth/app.json new file mode 100644 index 0000000000000..53e6a620c34cc --- /dev/null +++ b/examples/auth/expo-social-auth/app.json @@ -0,0 +1,54 @@ +{ + "expo": { + "name": "expo-social-auth", + "slug": "expo-social-auth", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "exposocialauth", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "bundleIdentifier": "com.anonymous.exposocialauth", + "supportsTablet": true, + "usesAppleSignIn": true + }, + "android": { + "adaptiveIcon": { + "backgroundColor": "#ffffff", + "foregroundImage": "./assets/images/adaptive-icon.png" + }, + "edgeToEdgeEnabled": true, + "package": "com.anonymous.exposocialauth" + }, + "web": { + "bundler": "metro", + "favicon": "./assets/images/favicon.png", + "output": "single" + }, + "plugins": [ + "expo-router", + [ + "expo-splash-screen", + { + "backgroundColor": "#ffffff", + "image": "./assets/images/splash-icon.png", + "imageWidth": 200, + "resizeMode": "contain" + } + ], + "expo-secure-store", + "expo-apple-authentication", + [ + "expo-web-browser", + { + "experimentalLauncherActivity": false + } + ] + ], + "experiments": { + "typedRoutes": true, + "reactCompiler": true + } + } +} \ No newline at end of file diff --git a/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx b/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx new file mode 100644 index 0000000000000..83f44aa13ccfc --- /dev/null +++ b/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx @@ -0,0 +1,45 @@ +import { Tabs } from 'expo-router'; +import React from 'react'; +import { Platform } from 'react-native'; + +import { HapticTab } from '@/components/haptic-tab'; +import { IconSymbol } from '@/components/ui/icon-symbol'; +import TabBarBackground from '@/components/ui/tab-bar-background'; +import { Colors } from '@/constants/colors'; +import { useColorScheme } from '@/hooks/use-color-scheme'; + +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + , + }} + /> + , + }} + /> + + ); +} diff --git a/examples/auth/expo-social-auth/app/(tabs)/explore.tsx b/examples/auth/expo-social-auth/app/(tabs)/explore.tsx new file mode 100644 index 0000000000000..cc171bb1e5998 --- /dev/null +++ b/examples/auth/expo-social-auth/app/(tabs)/explore.tsx @@ -0,0 +1,110 @@ +import { Image } from 'expo-image'; +import { Platform, StyleSheet } from 'react-native'; + +import { Collapsible } from '@/components/collapsible'; +import { ExternalLink } from '@/components/external-link'; +import ParallaxScrollView from '@/components/parallax-scroll-view'; +import { ThemedText } from '@/components/themed-text'; +import { ThemedView } from '@/components/themed-view'; +import { IconSymbol } from '@/components/ui/icon-symbol'; + +export default function TabTwoScreen() { + return ( + + }> + + Explore + + This app includes example code to help you get started. + + + This app has two screens:{' '} + app/(tabs)/index.tsx and{' '} + app/(tabs)/explore.tsx + + + The layout file in app/(tabs)/_layout.tsx{' '} + sets up the tab navigator. + + + Learn more + + + + + You can open this project on Android, iOS, and the web. To open the web version, press{' '} + w in the terminal running this project. + + + + + For static images, you can use the @2x and{' '} + @3x suffixes to provide files for + different screen densities + + + + Learn more + + + + + Open app/_layout.tsx to see how to load{' '} + + custom fonts such as this one. + + + + Learn more + + + + + This template has light and dark mode support. The{' '} + useColorScheme() hook lets you inspect + what the user's current color scheme is, and so you can adjust UI colors accordingly. + + + Learn more + + + + + This template includes an example of an animated component. The{' '} + components/HelloWave.tsx component uses + the powerful react-native-reanimated{' '} + library to create a waving hand animation. + + {Platform.select({ + ios: ( + + The components/ParallaxScrollView.tsx{' '} + component provides a parallax effect for the header image. + + ), + })} + + + ); +} + +const styles = StyleSheet.create({ + headerImage: { + color: '#808080', + bottom: -90, + left: -35, + position: 'absolute', + }, + titleContainer: { + flexDirection: 'row', + gap: 8, + }, +}); diff --git a/examples/auth/expo-social-auth/app/(tabs)/index.tsx b/examples/auth/expo-social-auth/app/(tabs)/index.tsx new file mode 100644 index 0000000000000..3eda3779baa12 --- /dev/null +++ b/examples/auth/expo-social-auth/app/(tabs)/index.tsx @@ -0,0 +1,56 @@ +import { Image } from 'expo-image'; +import { StyleSheet } from 'react-native'; + +import { HelloWave } from '@/components/hello-wave'; +import ParallaxScrollView from '@/components/parallax-scroll-view'; +import { ThemedText } from '@/components/themed-text'; +import { ThemedView } from '@/components/themed-view'; +import SignOutButton from '@/components/social-auth-buttons/sign-out-button'; +import { useAuthContext } from '@/hooks/use-auth-context'; + +export default function HomeScreen() { + + const { profile } = useAuthContext(); + + return ( + + }> + + Welcome! + + + + Username + {profile?.username} + Full name + {profile?.full_name} + + + + ); +} + +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', + }, +}); diff --git a/examples/auth/expo-social-auth/app/+not-found.tsx b/examples/auth/expo-social-auth/app/+not-found.tsx new file mode 100644 index 0000000000000..5171f3fd6ca92 --- /dev/null +++ b/examples/auth/expo-social-auth/app/+not-found.tsx @@ -0,0 +1,32 @@ +import { Link, Stack } from 'expo-router'; +import { StyleSheet } from 'react-native'; + +import { ThemedText } from '@/components/themed-text'; +import { ThemedView } from '@/components/themed-view'; + +export default function NotFoundScreen() { + return ( + <> + + + This screen does not exist. + + Go to home screen! + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + link: { + marginTop: 15, + paddingVertical: 15, + }, +}); diff --git a/examples/auth/expo-social-auth/app/_layout.tsx b/examples/auth/expo-social-auth/app/_layout.tsx new file mode 100644 index 0000000000000..c49ad43d58c65 --- /dev/null +++ b/examples/auth/expo-social-auth/app/_layout.tsx @@ -0,0 +1,52 @@ +import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; +import { useFonts } from 'expo-font'; +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import 'react-native-reanimated'; + +import { SplashScreenController } from '@/components/splash-screen-controller'; + +import { useAuthContext } from '@/hooks/use-auth-context'; +import { useColorScheme } from '@/hooks/use-color-scheme'; +import AuthProvider from '@/providers/auth-provider'; + +// Separate RootNavigator so we can access the AuthContext +function RootNavigator() { + const { isLoggedIn } = useAuthContext(); + + return ( + + + + + + + + + + ); +} + +export default function RootLayout() { + const colorScheme = useColorScheme(); + + const [loaded] = useFonts({ + SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), + }); + + if (!loaded) { + // Async font loading only occurs in development. + return null; + } + + return ( + + + + + + + + ); +} + \ No newline at end of file diff --git a/examples/auth/expo-social-auth/app/login.tsx b/examples/auth/expo-social-auth/app/login.tsx new file mode 100644 index 0000000000000..7472c985ab52e --- /dev/null +++ b/examples/auth/expo-social-auth/app/login.tsx @@ -0,0 +1,56 @@ +import { Link, Stack } from 'expo-router'; +import { Platform, StyleSheet } from 'react-native'; + +import { ThemedText } from '@/components/themed-text'; +import { ThemedView } from '@/components/themed-view'; + +import AppleSignInButton from '@/components/social-auth-buttons/apple/apple-sign-in-button'; +import ExpoAppleSignInButton from '@/components/social-auth-buttons/apple/expo-apple-sign-in-button'; +import GoogleSignInButton from '@/components/social-auth-buttons/google/google-sign-in-button'; +import { Image } from 'expo-image'; + +export default function LoginScreen() { + return ( + <> + + + + Login + + Try to navigate to home screen! + + + + {Platform.OS === 'ios' && ( + <> + Invertase Apple Sign In + + Expo Apple Sign In + + + )} + {Platform.OS !== 'ios' && ()} + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 20, + gap: 20, + }, + socialAuthButtonsContainer: { + display: 'flex', + gap: 10, + }, + image: { + width: 100, + height: 100, + }, +}); diff --git a/examples/auth/expo-social-auth/assets/fonts/SpaceMono-Regular.ttf b/examples/auth/expo-social-auth/assets/fonts/SpaceMono-Regular.ttf new file mode 100755 index 0000000000000..28d7ff717769d Binary files /dev/null and b/examples/auth/expo-social-auth/assets/fonts/SpaceMono-Regular.ttf differ diff --git a/examples/auth/expo-social-auth/assets/images/adaptive-icon.png b/examples/auth/expo-social-auth/assets/images/adaptive-icon.png new file mode 100644 index 0000000000000..03d6f6b6c6727 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/adaptive-icon.png differ diff --git a/examples/auth/expo-social-auth/assets/images/favicon.png b/examples/auth/expo-social-auth/assets/images/favicon.png new file mode 100644 index 0000000000000..e75f697b18018 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/favicon.png differ diff --git a/examples/auth/expo-social-auth/assets/images/icon.png b/examples/auth/expo-social-auth/assets/images/icon.png new file mode 100644 index 0000000000000..a0b1526fc7b78 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/icon.png differ diff --git a/examples/auth/expo-social-auth/assets/images/partial-react-logo.png b/examples/auth/expo-social-auth/assets/images/partial-react-logo.png new file mode 100644 index 0000000000000..66fd9570e4fac Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/partial-react-logo.png differ diff --git a/examples/auth/expo-social-auth/assets/images/react-logo.png b/examples/auth/expo-social-auth/assets/images/react-logo.png new file mode 100644 index 0000000000000..9d72a9ffcbb39 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/react-logo.png differ diff --git a/examples/auth/expo-social-auth/assets/images/react-logo@2x.png b/examples/auth/expo-social-auth/assets/images/react-logo@2x.png new file mode 100644 index 0000000000000..2229b130ad5b7 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/react-logo@2x.png differ diff --git a/examples/auth/expo-social-auth/assets/images/react-logo@3x.png b/examples/auth/expo-social-auth/assets/images/react-logo@3x.png new file mode 100644 index 0000000000000..a99b2032221d5 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/react-logo@3x.png differ diff --git a/examples/auth/expo-social-auth/assets/images/splash-icon.png b/examples/auth/expo-social-auth/assets/images/splash-icon.png new file mode 100644 index 0000000000000..03d6f6b6c6727 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/splash-icon.png differ diff --git a/examples/auth/expo-social-auth/assets/supabase-logo-icon.svg b/examples/auth/expo-social-auth/assets/supabase-logo-icon.svg new file mode 100644 index 0000000000000..ad802ac16ae9e --- /dev/null +++ b/examples/auth/expo-social-auth/assets/supabase-logo-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/examples/auth/expo-social-auth/components/collapsible.tsx b/examples/auth/expo-social-auth/components/collapsible.tsx new file mode 100644 index 0000000000000..e32747ac02694 --- /dev/null +++ b/examples/auth/expo-social-auth/components/collapsible.tsx @@ -0,0 +1,45 @@ +import { PropsWithChildren, useState } from 'react'; +import { StyleSheet, TouchableOpacity } from 'react-native'; + +import { ThemedText } from '@/components/themed-text'; +import { ThemedView } from '@/components/themed-view'; +import { IconSymbol } from '@/components/ui/icon-symbol'; +import { Colors } from '@/constants/colors'; +import { useColorScheme } from '@/hooks/use-color-scheme'; + +export function Collapsible({ children, title }: PropsWithChildren & { title: string }) { + const [isOpen, setIsOpen] = useState(false); + const theme = useColorScheme() ?? 'light'; + + return ( + + setIsOpen((value) => !value)} + activeOpacity={0.8}> + + + {title} + + {isOpen && {children}} + + ); +} + +const styles = StyleSheet.create({ + heading: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + content: { + marginTop: 6, + marginLeft: 24, + }, +}); diff --git a/examples/auth/expo-social-auth/components/external-link.tsx b/examples/auth/expo-social-auth/components/external-link.tsx new file mode 100644 index 0000000000000..dfbd23ea265fc --- /dev/null +++ b/examples/auth/expo-social-auth/components/external-link.tsx @@ -0,0 +1,24 @@ +import { Href, Link } from 'expo-router'; +import { openBrowserAsync } from 'expo-web-browser'; +import { type ComponentProps } from 'react'; +import { Platform } from 'react-native'; + +type Props = Omit, 'href'> & { href: Href & string }; + +export function ExternalLink({ href, ...rest }: Props) { + return ( + { + if (Platform.OS !== 'web') { + // Prevent the default behavior of linking to the default browser on native. + event.preventDefault(); + // Open the link in an in-app browser. + await openBrowserAsync(href); + } + }} + /> + ); +} diff --git a/examples/auth/expo-social-auth/components/haptic-tab.tsx b/examples/auth/expo-social-auth/components/haptic-tab.tsx new file mode 100644 index 0000000000000..7f3981cb94065 --- /dev/null +++ b/examples/auth/expo-social-auth/components/haptic-tab.tsx @@ -0,0 +1,18 @@ +import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'; +import { PlatformPressable } from '@react-navigation/elements'; +import * as Haptics from 'expo-haptics'; + +export function HapticTab(props: BottomTabBarButtonProps) { + return ( + { + if (process.env.EXPO_OS === 'ios') { + // Add a soft haptic feedback when pressing down on the tabs. + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + props.onPressIn?.(ev); + }} + /> + ); +} diff --git a/examples/auth/expo-social-auth/components/hello-wave.tsx b/examples/auth/expo-social-auth/components/hello-wave.tsx new file mode 100644 index 0000000000000..f9592011c2d4a --- /dev/null +++ b/examples/auth/expo-social-auth/components/hello-wave.tsx @@ -0,0 +1,40 @@ +import { useEffect } from 'react'; +import { StyleSheet } from 'react-native'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withRepeat, + withSequence, + withTiming, +} from 'react-native-reanimated'; + +import { ThemedText } from '@/components/themed-text'; + +export function HelloWave() { + const rotationAnimation = useSharedValue(0); + + useEffect(() => { + rotationAnimation.value = withRepeat( + withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })), + 4 // Run the animation 4 times + ); + }, [rotationAnimation]); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${rotationAnimation.value}deg` }], + })); + + return ( + + šŸ‘‹ + + ); +} + +const styles = StyleSheet.create({ + text: { + fontSize: 28, + lineHeight: 32, + marginTop: -6, + }, +}); diff --git a/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx b/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx new file mode 100644 index 0000000000000..b9e5a18e76a74 --- /dev/null +++ b/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx @@ -0,0 +1,82 @@ +import type { PropsWithChildren, ReactElement } from 'react'; +import { StyleSheet } from 'react-native'; +import Animated, { + interpolate, + useAnimatedRef, + useAnimatedStyle, + useScrollViewOffset, +} from 'react-native-reanimated'; + +import { ThemedView } from '@/components/themed-view'; +import { useBottomTabOverflow } from '@/components/ui/tab-bar-background'; +import { useColorScheme } from '@/hooks/use-color-scheme'; + +const HEADER_HEIGHT = 250; + +type Props = PropsWithChildren<{ + headerImage: ReactElement; + headerBackgroundColor: { dark: string; light: string }; +}>; + +export default function ParallaxScrollView({ + children, + headerImage, + headerBackgroundColor, +}: Props) { + const colorScheme = useColorScheme() ?? 'light'; + const scrollRef = useAnimatedRef(); + const scrollOffset = useScrollViewOffset(scrollRef); + const bottom = useBottomTabOverflow(); + const headerAnimatedStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateY: interpolate( + scrollOffset.value, + [-HEADER_HEIGHT, 0, HEADER_HEIGHT], + [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] + ), + }, + { + scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]), + }, + ], + }; + }); + + return ( + + + + {headerImage} + + {children} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + height: HEADER_HEIGHT, + overflow: 'hidden', + }, + content: { + flex: 1, + padding: 32, + gap: 16, + overflow: 'hidden', + }, +}); diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.android.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.android.tsx new file mode 100644 index 0000000000000..da82cbcc72e6d --- /dev/null +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.android.tsx @@ -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 onAppleButtonPress()} + />; +} \ No newline at end of file diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.ios.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.ios.tsx new file mode 100644 index 0000000000000..4175ac49f070b --- /dev/null +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.ios.tsx @@ -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 onAppleButtonPress()} + />; +} \ No newline at end of file diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.tsx new file mode 100644 index 0000000000000..66099bef7384f --- /dev/null +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.tsx @@ -0,0 +1,69 @@ +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'; + +/** + * This is the Apple sign in button for the web. + */ +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 { + 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 ; +} \ No newline at end of file diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/expo-apple-sign-in-button.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/expo-apple-sign-in-button.tsx new file mode 100644 index 0000000000000..2eb5a8eba9da0 --- /dev/null +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/expo-apple-sign-in-button.tsx @@ -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 onAppleButtonPress()} + /> +} + + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + button: { + width: 160, height: 45 + }, +}); diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.tsx new file mode 100644 index 0000000000000..f502a75e641e8 --- /dev/null +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.tsx @@ -0,0 +1,126 @@ +import { supabase } from '@/lib/supabase'; +import { useEffect } from 'react'; +import { TouchableOpacity } from 'react-native'; + +import { expo } from '@/app.json'; +import { Text } from '@react-navigation/elements'; +import { Image } from 'expo-image'; +import * as WebBrowser from "expo-web-browser"; + +WebBrowser.maybeCompleteAuthSession(); + +export default function GoogleSignInButton() { + + function extractParamsFromUrl(url: string) { + const parsedUrl = new URL(url); + const hash = parsedUrl.hash.substring(1); // Remove the leading '#' + const params = new URLSearchParams(hash); + + return { + access_token: params.get("access_token"), + expires_in: parseInt(params.get("expires_in") || "0"), + refresh_token: params.get("refresh_token"), + token_type: params.get("token_type"), + provider_token: params.get("provider_token"), + code: params.get("code"), + }; + }; + + async function onSignInButtonPress() { + console.debug('onSignInButtonPress - start'); + const res = await supabase.auth.signInWithOAuth({ + provider: "google", + options: { + redirectTo: `${expo.scheme}://google-auth`, + queryParams: { prompt: "consent" }, + skipBrowserRedirect: true, + }, + }); + + const googleOAuthUrl = res.data.url; + + if (!googleOAuthUrl) { + console.error("no oauth url found!"); + return; + } + + const result = await WebBrowser.openAuthSessionAsync( + googleOAuthUrl, + `${expo.scheme}://google-auth`, + { showInRecents: true }, + ).catch((err) => { + console.error('onSignInButtonPress - openAuthSessionAsync - error', { err }) + console.log(err); + }); + + console.debug('onSignInButtonPress - openAuthSessionAsync - result', { result }); + + if (result && result.type === "success") { + console.debug('onSignInButtonPress - openAuthSessionAsync - success'); + const params = extractParamsFromUrl(result.url); + console.debug('onSignInButtonPress - openAuthSessionAsync - success', { params }); + + if (params.access_token && params.refresh_token) { + console.debug('onSignInButtonPress - setSession'); + const { data, error } = await supabase.auth.setSession({ + access_token: params.access_token, + refresh_token: params.refresh_token, + }); + console.debug('onSignInButtonPress - setSession - success', { data, error }); + return; + } else { + console.error('onSignInButtonPress - setSession - failed'); + // sign in/up failed + } + } else { + console.error('onSignInButtonPress - openAuthSessionAsync - failed'); + } + } + + // to warm up the browser + useEffect(() => { + WebBrowser.warmUpAsync(); + + return () => { + WebBrowser.coolDownAsync(); + }; + }, []); + + return ( + + + + Sign in with Google + + + ); +} diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.web.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.web.tsx new file mode 100644 index 0000000000000..840df98ab8be0 --- /dev/null +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.web.tsx @@ -0,0 +1,73 @@ +import { supabase } from '@/lib/supabase'; +import { CredentialResponse, GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google'; +import { SignInWithIdTokenCredentials } from '@supabase/supabase-js'; +import { useEffect, useState } from 'react'; + +import 'react-native-get-random-values'; + +export default function GoogleSignInButton() { + + // Generate secure, random values for state and nonce + const [nonce, setNonce] = useState(''); + const [sha256Nonce, setSha256Nonce] = useState(''); + + async function onGoogleButtonSuccess(authRequestResponse: CredentialResponse) { + console.debug('Google sign in successful:', { authRequestResponse }) + if (authRequestResponse.clientId && authRequestResponse.credential) { + const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { + provider: 'google', + token: authRequestResponse.credential, + nonce: nonce, + } + + const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) + + if (error) { + console.error('Error signing in with Google:', error) + } + + if (data) { + console.log('Google sign in successful:', data) + } + } + } + + function onGoogleButtonFailure() { + console.error('Error signing in with Google') + } + + useEffect(() => { + function generateNonce(): string { + const array = new Uint32Array(1); + window.crypto.getRandomValues(array); + return array[0].toString(); + }; + + async function generateSha256Nonce(nonce: string): Promise { + const buffer = await window.crypto.subtle.digest('sha-256', new TextEncoder().encode(nonce)); + const array = Array.from(new Uint8Array(buffer)); + return array.map(b => b.toString(16).padStart(2, '0')).join(''); + } + + let nonce = generateNonce(); + setNonce(nonce); + + generateSha256Nonce(nonce) + .then((sha256Nonce) => { setSha256Nonce(sha256Nonce) }); + }, []); + + return ( + + + + ); +} \ No newline at end of file diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/sign-out-button.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/sign-out-button.tsx new file mode 100644 index 0000000000000..a4215387df983 --- /dev/null +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/sign-out-button.tsx @@ -0,0 +1,24 @@ +import { useAuthContext } from '@/hooks/use-auth-context'; +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 ( +