diff options
Diffstat (limited to 'apps/mobile/contexts/AuthContext.tsx')
| -rw-r--r-- | apps/mobile/contexts/AuthContext.tsx | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/apps/mobile/contexts/AuthContext.tsx b/apps/mobile/contexts/AuthContext.tsx new file mode 100644 index 0000000..6102433 --- /dev/null +++ b/apps/mobile/contexts/AuthContext.tsx @@ -0,0 +1,298 @@ +import React, { + createContext, + useContext, + useEffect, + useState, + useCallback, + useMemo, + type ReactNode, +} from 'react'; +import { AppState, type AppStateStatus } from 'react-native'; +import type { Session, User, AuthChangeEvent } from '@supabase/supabase-js'; +import { supabase } from '../lib/supabase'; +import { signIn as authSignIn, signOut as authSignOut, getSession } from '../lib/auth'; + +/** + * Auth context state interface + */ +interface AuthState { + user: User | null; + session: Session | null; + isLoading: boolean; + isInitialized: boolean; + error: string | null; +} + +/** + * Auth context actions interface + */ +interface AuthActions { + signIn: (email: string, password: string) => Promise<boolean>; + signOut: () => Promise<void>; + clearError: () => void; +} + +/** + * Combined auth context type + */ +type AuthContextType = AuthState & AuthActions; + +/** + * Default auth state + */ +const defaultAuthState: AuthState = { + user: null, + session: null, + isLoading: true, + isInitialized: false, + error: null, +}; + +/** + * Auth context with default values + */ +const AuthContext = createContext<AuthContextType | undefined>(undefined); + +/** + * Props for AuthProvider component + */ +interface AuthProviderProps { + children: ReactNode; +} + +/** + * AuthProvider component that manages authentication state + * - Initializes auth state on mount + * - Listens to auth state changes + * - Auto-refreshes session when app comes to foreground + * - Provides auth actions via context + */ +export function AuthProvider({ children }: AuthProviderProps) { + const [state, setState] = useState<AuthState>(defaultAuthState); + + /** + * Update auth state with partial updates + */ + const updateState = useCallback((updates: Partial<AuthState>) => { + setState((prev) => ({ ...prev, ...updates })); + }, []); + + /** + * Initialize auth state by checking for existing session + */ + const initialize = useCallback(async () => { + try { + const { session, error } = await getSession(); + + if (error) { + console.warn('Auth initialization warning:', error); + } + + updateState({ + user: session?.user ?? null, + session, + isLoading: false, + isInitialized: true, + error: null, + }); + } catch (error) { + console.error('Auth initialization error:', error); + updateState({ + user: null, + session: null, + isLoading: false, + isInitialized: true, + error: error instanceof Error ? error.message : 'Failed to initialize auth', + }); + } + }, [updateState]); + + /** + * Sign in with email and password + */ + const signIn = useCallback( + async (email: string, password: string): Promise<boolean> => { + updateState({ isLoading: true, error: null }); + + const result = await authSignIn(email, password); + + if (result.success && result.user && result.session) { + updateState({ + user: result.user, + session: result.session, + isLoading: false, + error: null, + }); + return true; + } + + updateState({ + isLoading: false, + error: result.error || 'Sign in failed', + }); + return false; + }, + [updateState] + ); + + /** + * Sign out the current user + */ + const signOut = useCallback(async () => { + updateState({ isLoading: true, error: null }); + + const result = await authSignOut(); + + if (result.success) { + updateState({ + user: null, + session: null, + isLoading: false, + error: null, + }); + } else { + updateState({ + isLoading: false, + error: result.error || 'Sign out failed', + }); + } + }, [updateState]); + + /** + * Clear any auth errors + */ + const clearError = useCallback(() => { + updateState({ error: null }); + }, [updateState]); + + /** + * Handle app state changes (foreground/background) + * Refresh session when app comes to foreground + */ + useEffect(() => { + const handleAppStateChange = async (nextAppState: AppStateStatus) => { + if (nextAppState === 'active' && state.session) { + // App came to foreground, refresh the session + try { + const { data, error } = await supabase.auth.refreshSession(); + if (error) { + console.warn('Session refresh warning:', error.message); + } else if (data.session) { + updateState({ + session: data.session, + user: data.session.user, + }); + } + } catch (error) { + console.error('Session refresh error:', error); + } + } + }; + + const subscription = AppState.addEventListener('change', handleAppStateChange); + + return () => { + subscription.remove(); + }; + }, [state.session, updateState]); + + /** + * Listen to Supabase auth state changes + */ + useEffect(() => { + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange( + (event: AuthChangeEvent, session: Session | null) => { + console.log('Auth state changed:', event); + + switch (event) { + case 'SIGNED_IN': + updateState({ + user: session?.user ?? null, + session, + isLoading: false, + error: null, + }); + break; + case 'SIGNED_OUT': + updateState({ + user: null, + session: null, + isLoading: false, + error: null, + }); + break; + case 'TOKEN_REFRESHED': + updateState({ + session, + user: session?.user ?? null, + }); + break; + case 'USER_UPDATED': + updateState({ + user: session?.user ?? null, + }); + break; + case 'PASSWORD_RECOVERY': + case 'MFA_CHALLENGE_VERIFIED': + // Handle other events as needed + break; + default: + // INITIAL_SESSION and other events + break; + } + } + ); + + return () => { + subscription.unsubscribe(); + }; + }, [updateState]); + + /** + * Initialize auth on mount + */ + useEffect(() => { + initialize(); + }, [initialize]); + + /** + * Memoized context value + */ + const value = useMemo<AuthContextType>( + () => ({ + ...state, + signIn, + signOut, + clearError, + }), + [state, signIn, signOut, clearError] + ); + + return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; +} + +/** + * Hook to access auth context + * Must be used within an AuthProvider + */ +export function useAuth(): AuthContextType { + const context = useContext(AuthContext); + + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + + return context; +} + +/** + * Hook to check if user is authenticated + */ +export function useIsAuthenticated(): boolean { + const { session } = useAuth(); + return session !== null; +} + +export type { AuthContextType, AuthState, AuthActions }; |
