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 index 2f6e0a9206fc3..daa535e479a28 100644 --- 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 @@ -53,42 +53,11 @@ Now, create a helper file to initialize the Supabase client for both web and Rea queryGroup="auth-store" > -{/* 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, - }, - }, - ); - ``` - - + <$CodeSample + path="/auth/expo-social-auth/lib/supabase.web.ts" + lines={[[1, -1]]} + meta="name=lib/supabase.web.ts" +/> @@ -102,43 +71,12 @@ Now, create a helper file to initialize the Supabase client for both web and Rea Implement a `ExpoSecureStoreAdapter` to pass in as Auth storage adapter for the `supabase-js` client: - <$CodeTabs> + <$CodeSample + path="/auth/expo-social-auth/lib/supabase.ts" + lines={[[1, -1]]} + meta="name=lib/supabase.ts" - ```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, - }, - }, - ); - ``` - - +/> @@ -150,14 +88,11 @@ These variables are safe to expose in your Expo app since Supabase has [Row Leve 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 -``` - - +<$CodeSample +path="/auth/expo-social-auth/.env" +lines={[[1, -1]]} +meta="name=.env" +/> ### Set up protected navigation @@ -167,279 +102,59 @@ Next, you need to protect app navigation to prevent unauthenticated users from a 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) -``` - - +<$CodeSample +path="/auth/expo-social-auth/hooks/use-auth-context.tsx" +lines={[[1, -1]]} +meta="name=hooks/use-auth-context.tsx" +/> #### 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} - - ) -} -``` - - +<$CodeSample +path="/auth/expo-social-auth/providers/auth-provider.tsx" +lines={[[1, -1]]} +meta="name=providers/auth-provider.tsx" +/> #### 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 -} -``` - - +<$CodeSample +path="/auth/expo-social-auth/components/splash-screen-controller.tsx" +lines={[[1, -1]]} +meta="name=components/splash-screen-controller.tsx" +/> ### 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 - + @@ -284,6 +458,7 @@ export const EditorPanel = () => { language="pgsql" value={currentValue} onChange={handleChange} + onMount={(_, m) => setMonaco(m)} aiEndpoint={`${BASE_PATH}/api/ai/code/complete`} aiMetadata={{ projectRef: project?.ref, @@ -379,10 +554,89 @@ export const EditorPanel = () => { )} -
+
+ {(isUpserting || saveStatus !== 'idle') && ( +
+ {isUpserting && } + {saveStatus === 'success' && } + {saveStatus === 'error' && } + + {isUpserting + ? 'Saving...' + : saveStatus === 'success' + ? 'Snippet updated' + : 'Failed to save snippet'} + +
+ )} +
+ { + if (!ref || !profile || !project) return + const snippet = createSqlSnippetSkeletonV2({ + name, + sql: currentValue, + owner_id: profile.id, + project_id: project.id, + }) + sqlEditorSnap.addSnippet({ projectRef: ref, snippet }) + sqlEditorSnap.addNeedsSaving(snippet.id) + setActiveSnippet(snippet as unknown as Extract) + originalSnippetRef.current = { sql: currentValue, name } + showSaveSuccess() + }} + /> ) } diff --git a/apps/studio/components/ui/EditorPanel/EditorPanel.utils.ts b/apps/studio/components/ui/EditorPanel/EditorPanel.utils.ts new file mode 100644 index 0000000000000..3a6ffa836c701 --- /dev/null +++ b/apps/studio/components/ui/EditorPanel/EditorPanel.utils.ts @@ -0,0 +1,9 @@ +import type { SqlError } from 'state/editor-panel-state' + +export function formatSqlError(error: SqlError): { header: string | undefined; lines: string[] } { + if (error.formattedError) { + const lines = error.formattedError.split('\n').filter((l) => l.length > 0) + return { header: lines[0], lines: lines.slice(1) } + } + return { header: undefined, lines: [error.message ?? ''] } +} diff --git a/apps/studio/components/ui/EditorPanel/SaveSnippetDialog.tsx b/apps/studio/components/ui/EditorPanel/SaveSnippetDialog.tsx new file mode 100644 index 0000000000000..4412fbf4604b4 --- /dev/null +++ b/apps/studio/components/ui/EditorPanel/SaveSnippetDialog.tsx @@ -0,0 +1,123 @@ +import { useParams } from 'common' +import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils' +import { generateSnippetTitle } from 'components/interfaces/SQLEditor/SQLEditor.constants' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useCheckOpenAIKeyQuery } from 'data/ai/check-api-key-query' +import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation' +import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' +import { + AiIconAnimation, + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + Input_Shadcn_, + Label_Shadcn_, +} from 'ui' + +interface SaveSnippetDialogProps { + open: boolean + sql: string + onOpenChange: (open: boolean) => void + onSave: (name: string) => void +} + +export const SaveSnippetDialog = ({ open, sql, onOpenChange, onSave }: SaveSnippetDialogProps) => { + const { ref } = useParams() + const { data: organization } = useSelectedOrganizationQuery() + const { data: subscription } = useOrgSubscriptionQuery( + { orgSlug: organization?.slug }, + { enabled: open } + ) + const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: ref }) + const { data: check } = useCheckOpenAIKeyQuery() + + const [name, setName] = useState(generateSnippetTitle()) + + const isApiKeySet = !!check?.hasKey + const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive + + const { mutate: generateTitle, isPending: isGenerating } = useSqlTitleGenerateMutation({ + onSuccess: ({ title }) => setName(title), + onError: (error) => toast.error(`Failed to generate title: ${error.message}`), + }) + + // Reset the name each time the dialog opens + useEffect(() => { + if (open) setName(generateSnippetTitle()) + }, [open]) + + const handleSave = () => { + const trimmed = name.trim() + if (!trimmed) return + onSave(trimmed) + onOpenChange(false) + } + + return ( + + + + Save snippet + + + +
+ Name + setName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSave() + }} + /> +
+ {!hasHipaaAddon && ( +
+ generateTitle({ sql })} + tooltip={{ + content: { + side: 'bottom', + text: isApiKeySet + ? undefined + : 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.', + }, + }} + > +
+
+ +
+ Generate with AI +
+
+
+ )} +
+ + + + + +
+
+ ) +} diff --git a/apps/studio/components/ui/QueryBlock/EditQueryButton.tsx b/apps/studio/components/ui/QueryBlock/EditQueryButton.tsx index 5e514a60979a6..2664e35f972c3 100644 --- a/apps/studio/components/ui/QueryBlock/EditQueryButton.tsx +++ b/apps/studio/components/ui/QueryBlock/EditQueryButton.tsx @@ -1,15 +1,14 @@ -import { Edit } from 'lucide-react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { ComponentProps } from 'react' - import { useParams } from 'common' import { useIsInlineEditorEnabled } from 'components/interfaces/Account/Preferences/InlineEditorSettings' -import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types' import useNewQuery from 'components/interfaces/SQLEditor/hooks' +import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types' import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { Edit } from 'lucide-react' +import { useRouter } from 'next/router' +import { ComponentProps } from 'react' +import { editorPanelState } from 'state/editor-panel-state' import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { @@ -20,6 +19,7 @@ import { DropdownMenuTrigger, TooltipContent, } from 'ui' + import { ButtonTooltip } from '../ButtonTooltip' interface EditQueryButtonProps { @@ -42,7 +42,7 @@ export const EditQueryButton = ({ const { newQuery } = useNewQuery() const sqlEditorSnap = useSqlEditorV2StateSnapshot() - const { closeSidebar } = useSidebarManagerSnapshot() + const { closeSidebar, openSidebar } = useSidebarManagerSnapshot() const isInSQLEditor = router.pathname.includes('/sql') const isInNewSnippet = router.pathname.endsWith('/sql') @@ -57,15 +57,16 @@ export const EditQueryButton = ({ if (id !== undefined) { return ( } tooltip={tooltip} - > - - + onClick={() => { + editorPanelState.setActiveSnippetId(id) + openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) + }} + /> ) } diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index 8332f6929cc13..12e726730963e 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -395,10 +395,13 @@ export const useTableRowsQuery = ( ) => { const queryClient = useQueryClient() + // [Joshen] Exclude preflightCheck from query key + const { preflightCheck, ...othersArgs } = args + return useQuery({ queryKey: tableRowKeys.tableRows(projectRef, { table: { id: tableId }, - ...args, + ...othersArgs, }), queryFn: ({ signal }) => getTableRows({ queryClient, projectRef, connectionString, tableId, ...args }, signal), diff --git a/apps/studio/state/editor-panel-state.tsx b/apps/studio/state/editor-panel-state.tsx index 167e33ef3b5ad..bff9268b6e81b 100644 --- a/apps/studio/state/editor-panel-state.tsx +++ b/apps/studio/state/editor-panel-state.tsx @@ -6,13 +6,21 @@ type Template = { content: string } +export type SqlError = { + error?: string + formattedError?: string + message?: string +} + type EditorPanelState = { value: string templates: Template[] - results: any[] | undefined - error: any + results: Record[] | undefined + error: SqlError | undefined initialPrompt: string onChange: ((value: string) => void) | undefined + activeSnippetId: string | null + pendingReset: boolean } const initialState: EditorPanelState = { @@ -22,6 +30,8 @@ const initialState: EditorPanelState = { error: undefined, initialPrompt: '', onChange: undefined, + activeSnippetId: null, + pendingReset: false, } export const editorPanelState = proxy({ @@ -35,15 +45,24 @@ export const editorPanelState = proxy({ setTemplates(templates: Template[]) { editorPanelState.templates = templates }, - setResults(results: any[] | undefined) { + setResults(results: Record[] | undefined) { editorPanelState.results = results }, - setError(error: any) { + setError(error: SqlError | undefined) { editorPanelState.error = error }, setInitialPrompt(initialPrompt: string) { editorPanelState.initialPrompt = initialPrompt }, + setActiveSnippetId(id: string | null) { + editorPanelState.activeSnippetId = id + }, + openAsNew() { + editorPanelState.value = '' + editorPanelState.results = undefined + editorPanelState.error = undefined + editorPanelState.pendingReset = true + }, reset() { Object.assign(editorPanelState, initialState) }, diff --git a/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx b/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx index 83f44aa13ccfc..34b7acd92bd2c 100644 --- a/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx +++ b/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx @@ -1,11 +1,6 @@ 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 { Colors } from '@/constants/theme'; import { useColorScheme } from '@/hooks/use-color-scheme'; export default function TabLayout() { @@ -16,28 +11,11 @@ export default function TabLayout() { screenOptions={{ tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, headerShown: false, - tabBarButton: HapticTab, - tabBarBackground: TabBarBackground, - tabBarStyle: Platform.select({ - ios: { - // Use a transparent background on iOS to show the blur effect - position: 'absolute', - }, - default: {}, - }), }}> , - }} - /> - , }} /> diff --git a/examples/auth/expo-social-auth/app/(tabs)/explore.tsx b/examples/auth/expo-social-auth/app/(tabs)/explore.tsx deleted file mode 100644 index cc171bb1e5998..0000000000000 --- a/examples/auth/expo-social-auth/app/(tabs)/explore.tsx +++ /dev/null @@ -1,110 +0,0 @@ -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 index 3eda3779baa12..e9ad4f762af44 100644 --- a/examples/auth/expo-social-auth/app/(tabs)/index.tsx +++ b/examples/auth/expo-social-auth/app/(tabs)/index.tsx @@ -1,16 +1,15 @@ -import { Image } from 'expo-image'; -import { StyleSheet } from 'react-native'; +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'; +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(); + const { profile } = useAuthContext() return ( - }> + } + > Welcome! @@ -33,7 +33,7 @@ export default function HomeScreen() { - ); + ) } const styles = StyleSheet.create({ @@ -53,4 +53,4 @@ const styles = StyleSheet.create({ 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 index 5171f3fd6ca92..ca79ddc8ee5f1 100644 --- a/examples/auth/expo-social-auth/app/+not-found.tsx +++ b/examples/auth/expo-social-auth/app/+not-found.tsx @@ -9,7 +9,7 @@ export default function NotFoundScreen() { <> - This screen does not exist. + This screen doesn't exist. Go to home screen! diff --git a/examples/auth/expo-social-auth/app/_layout.tsx b/examples/auth/expo-social-auth/app/_layout.tsx index c49ad43d58c65..b0cf3e6ce2b6a 100644 --- a/examples/auth/expo-social-auth/app/_layout.tsx +++ b/examples/auth/expo-social-auth/app/_layout.tsx @@ -1,18 +1,17 @@ -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 { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native' +import { Stack } from 'expo-router' +import { StatusBar } from 'expo-status-bar' +import 'react-native-reanimated' -import { SplashScreenController } from '@/components/splash-screen-controller'; +import { 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'; +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(); + const { isLoggedIn } = useAuthContext() return ( @@ -24,20 +23,11 @@ function RootNavigator() { - ); + ) } 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; - } + const colorScheme = useColorScheme() return ( @@ -47,6 +37,5 @@ export default function RootLayout() { - ); + ) } - \ 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 index 7472c985ab52e..910dc83bfa8aa 100644 --- a/examples/auth/expo-social-auth/app/login.tsx +++ b/examples/auth/expo-social-auth/app/login.tsx @@ -1,40 +1,21 @@ -import { Link, Stack } from 'expo-router'; -import { Platform, StyleSheet } from 'react-native'; +import { Link, Stack } from 'expo-router' +import { 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'; +import { ThemedText } from '@/components/themed-text' +import { ThemedView } from '@/components/themed-view' 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({ @@ -43,14 +24,9 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', padding: 20, - gap: 20, - }, - socialAuthButtonsContainer: { - display: 'flex', - gap: 10, }, - image: { - width: 100, - height: 100, + link: { + marginTop: 15, + paddingVertical: 15, }, -}); +}) diff --git a/examples/auth/expo-social-auth/assets/fonts/SpaceMono-Regular.ttf b/examples/auth/expo-social-auth/assets/fonts/SpaceMono-Regular.ttf old mode 100755 new mode 100644 index 28d7ff717769d..1cfa3653dc8dd Binary files a/examples/auth/expo-social-auth/assets/fonts/SpaceMono-Regular.ttf and b/examples/auth/expo-social-auth/assets/fonts/SpaceMono-Regular.ttf differ diff --git a/examples/auth/expo-social-auth/assets/images/android-icon-background.png b/examples/auth/expo-social-auth/assets/images/android-icon-background.png new file mode 100644 index 0000000000000..5ffefc5bb57a3 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/android-icon-background.png differ diff --git a/examples/auth/expo-social-auth/assets/images/android-icon-foreground.png b/examples/auth/expo-social-auth/assets/images/android-icon-foreground.png new file mode 100644 index 0000000000000..3a9e5016dca48 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/android-icon-foreground.png differ diff --git a/examples/auth/expo-social-auth/assets/images/android-icon-monochrome.png b/examples/auth/expo-social-auth/assets/images/android-icon-monochrome.png new file mode 100644 index 0000000000000..77484ebdbca25 Binary files /dev/null and b/examples/auth/expo-social-auth/assets/images/android-icon-monochrome.png differ diff --git a/examples/auth/expo-social-auth/assets/images/favicon.png b/examples/auth/expo-social-auth/assets/images/favicon.png index e75f697b18018..408bd74661578 100644 Binary files a/examples/auth/expo-social-auth/assets/images/favicon.png 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 index a0b1526fc7b78..7165a53c7475d 100644 Binary files a/examples/auth/expo-social-auth/assets/images/icon.png and b/examples/auth/expo-social-auth/assets/images/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 deleted file mode 100644 index ad802ac16ae9e..0000000000000 --- a/examples/auth/expo-social-auth/assets/supabase-logo-icon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/examples/auth/expo-social-auth/components/collapsible.tsx b/examples/auth/expo-social-auth/components/collapsible.tsx deleted file mode 100644 index e32747ac02694..0000000000000 --- a/examples/auth/expo-social-auth/components/collapsible.tsx +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index dfbd23ea265fc..0000000000000 --- a/examples/auth/expo-social-auth/components/external-link.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 7f3981cb94065..0000000000000 --- a/examples/auth/expo-social-auth/components/haptic-tab.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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 index f9592011c2d4a..abfc61edf2e4c 100644 --- a/examples/auth/expo-social-auth/components/hello-wave.tsx +++ b/examples/auth/expo-social-auth/components/hello-wave.tsx @@ -1,11 +1,11 @@ import { useEffect } from 'react'; import { StyleSheet } from 'react-native'; import Animated, { - useAnimatedStyle, useSharedValue, + useAnimatedStyle, + withTiming, withRepeat, withSequence, - withTiming, } from 'react-native-reanimated'; import { ThemedText } from '@/components/themed-text'; @@ -16,7 +16,7 @@ export function HelloWave() { useEffect(() => { rotationAnimation.value = withRepeat( withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })), - 4 // Run the animation 4 times + 4 ); }, [rotationAnimation]); diff --git a/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx b/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx index b9e5a18e76a74..93f376484a4c6 100644 --- a/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx +++ b/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx @@ -8,7 +8,6 @@ import Animated, { } 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; @@ -26,7 +25,7 @@ export default function ParallaxScrollView({ const colorScheme = useColorScheme() ?? 'light'; const scrollRef = useAnimatedRef(); const scrollOffset = useScrollViewOffset(scrollRef); - const bottom = useBottomTabOverflow(); + const headerAnimatedStyle = useAnimatedStyle(() => { return { transform: [ @@ -46,11 +45,7 @@ export default function ParallaxScrollView({ 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 index 4175ac49f070b..599cf00a062b1 100644 --- 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 @@ -16,7 +16,7 @@ async function onAppleButtonPress() { // 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 }) + console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse }); if (credentialState === appleAuth.State.AUTHORIZED && appleAuthRequestResponse.identityToken && appleAuthRequestResponse.authorizationCode) { const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { @@ -24,28 +24,28 @@ async function onAppleButtonPress() { token: appleAuthRequestResponse.identityToken, nonce: appleAuthRequestResponse.nonce, access_token: appleAuthRequestResponse.authorizationCode, - } + }; - const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) + const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials); if (error) { - console.error('Error signing in with Apple:', error) + console.error('Error signing in with Apple:', error); } if (data) { - console.log('Apple sign in successful:', data) - router.navigate('/(tabs)/explore') + console.log('Apple sign in successful:', data); + router.navigate('/(tabs)'); } } } export default function AppleSignInButton() { - if (Platform.OS !== 'ios') { return <> } - + 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 index 66099bef7384f..599cf00a062b1 100644 --- 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 @@ -1,69 +1,51 @@ import { supabase } from '@/lib/supabase'; +import { AppleButton, appleAuth } from '@invertase/react-native-apple-authentication'; import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js'; -import { useEffect, useState } from 'react'; -import AppleSignin, { type AppleAuthResponse } from 'react-apple-signin-auth'; +import { router } from 'expo-router'; 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, - } +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], + }); - const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) + // 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); - if (error) { - console.error('Error signing in with Apple:', error) - } + console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse }); - 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(); + if (credentialState === appleAuth.State.AUTHORIZED && appleAuthRequestResponse.identityToken && appleAuthRequestResponse.authorizationCode) { + const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { + provider: 'apple', + token: appleAuthRequestResponse.identityToken, + nonce: appleAuthRequestResponse.nonce, + access_token: appleAuthRequestResponse.authorizationCode, }; - 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(''); + const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials); + + if (error) { + console.error('Error signing in with Apple:', error); } - - let nonce = generateNonce(); - setNonce(nonce); - - generateSha256Nonce(nonce) - .then((sha256Nonce) => { setSha256Nonce(sha256Nonce) }); - }, []); - if (Platform.OS !== 'web') { return <> } - - 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.web.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.web.tsx new file mode 100644 index 0000000000000..fa1befa6121f2 --- /dev/null +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.web.tsx @@ -0,0 +1,68 @@ +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 { + 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 console.error('Apple sign in error:', error)} + />; +} 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 deleted file mode 100644 index 2eb5a8eba9da0..0000000000000 --- a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/expo-apple-sign-in-button.tsx +++ /dev/null @@ -1,69 +0,0 @@ -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 index f502a75e641e8..6962d83c38e32 100644 --- 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 @@ -15,7 +15,7 @@ export default function GoogleSignInButton() { 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"), @@ -25,7 +25,7 @@ export default function GoogleSignInButton() { code: params.get("code"), }; }; - + async function onSignInButtonPress() { console.debug('onSignInButtonPress - start'); const res = await supabase.auth.signInWithOAuth({ @@ -39,7 +39,7 @@ export default function GoogleSignInButton() { const googleOAuthUrl = res.data.url; - if (!googleOAuthUrl) { + if (!googleOAuthUrl) { console.error("no oauth url found!"); return; } @@ -49,7 +49,7 @@ export default function GoogleSignInButton() { `${expo.scheme}://google-auth`, { showInRecents: true }, ).catch((err) => { - console.error('onSignInButtonPress - openAuthSessionAsync - error', { err }) + console.error('onSignInButtonPress - openAuthSessionAsync - error', { err }); console.log(err); }); 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 index 840df98ab8be0..36d60d9be2caa 100644 --- 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 @@ -12,28 +12,28 @@ export default function GoogleSignInButton() { const [sha256Nonce, setSha256Nonce] = useState(''); async function onGoogleButtonSuccess(authRequestResponse: CredentialResponse) { - console.debug('Google sign in successful:', { authRequestResponse }) + 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) + const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials); if (error) { - console.error('Error signing in with Google:', error) + console.error('Error signing in with Google:', error); } if (data) { - console.log('Google sign in successful:', data) + console.log('Google sign in successful:', data); } } } function onGoogleButtonFailure() { - console.error('Error signing in with Google') + console.error('Error signing in with Google'); } useEffect(() => { @@ -41,7 +41,7 @@ export default function GoogleSignInButton() { 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)); @@ -70,4 +70,4 @@ export default function GoogleSignInButton() { /> ); -} \ 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 index a4215387df983..6416938e1f039 100644 --- 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 @@ -1,24 +1,15 @@ -import { useAuthContext } from '@/hooks/use-auth-context'; -import { supabase } from '@/lib/supabase'; -import React from 'react'; -import { Button } from 'react-native'; +import { supabase } from '@/lib/supabase' +import React from 'react' +import { Button } from 'react-native' async function onSignOutButtonPress() { - const { error } = await supabase.auth.signOut() + const { error } = await supabase.auth.signOut() - if (error) { - console.error('Error signing out:', error) - } + if (error) { + console.error('Error signing out:', error) + } } export default function SignOutButton() { - const { isLoggedIn } = useAuthContext(); - - return ( -