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; signOut: () => Promise; 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(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(defaultAuthState); /** * Update auth state with partial updates */ const updateState = useCallback((updates: Partial) => { 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 => { 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( () => ({ ...state, signIn, signOut, clearError, }), [state, signIn, signOut, clearError] ); return {children}; } /** * 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 };