import React, { createContext, useContext, useEffect, useState, useRef } from 'react'; import type { User, Session } from '@supabase/supabase-js'; import { supabase } from '@/integrations/supabase/client'; import type { Profile } from '@/types/database'; import { toast } from '@/hooks/use-toast'; interface AuthContextType { user: User | null; session: Session | null; profile: Profile | null; loading: boolean; pendingEmail: string | null; sessionError: string | null; signOut: () => Promise; refreshProfile: () => Promise; verifySession: () => Promise; clearPendingEmail: () => void; } const AuthContext = createContext(undefined); function AuthProviderComponent({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null); const [session, setSession] = useState(null); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [pendingEmail, setPendingEmail] = useState(null); const [sessionError, setSessionError] = useState(null); // Refs for lifecycle and cleanup management const isMountedRef = useRef(true); const profileFetchTimeoutRef = useRef(null); const profileRetryCountRef = useRef(0); const sessionVerifiedRef = useRef(false); const novuUpdateTimeoutRef = useRef(null); const previousEmailRef = useRef(null); const loadingTimeoutRef = useRef(null); const loadingStateRef = useRef(loading); const fetchProfile = async (userId: string, retryCount = 0, onComplete?: () => void) => { try { const { data, error } = await supabase .from('profiles') .select(`*, location:locations(*)`) .eq('user_id', userId) .maybeSingle(); if (error && error.code !== 'PGRST116') { console.error('[Auth] Error fetching profile:', error); // Retry up to 3 times with exponential backoff if (retryCount < 3 && isMountedRef.current) { const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s console.log(`[Auth] Retrying profile fetch in ${delay}ms (attempt ${retryCount + 1}/3)`); setTimeout(() => { if (isMountedRef.current) { fetchProfile(userId, retryCount + 1, onComplete); } }, delay); return; } // All retries exhausted - complete anyway if (isMountedRef.current) { console.warn('[Auth] Profile fetch failed after 3 retries'); setProfile(null); onComplete?.(); } return; } // Success - update state and complete if (isMountedRef.current) { setProfile(data as Profile); profileRetryCountRef.current = 0; onComplete?.(); } } catch (error) { console.error('[Auth] Error fetching profile:', error); // Retry logic for network errors if (retryCount < 3 && isMountedRef.current) { const delay = Math.pow(2, retryCount) * 1000; setTimeout(() => { if (isMountedRef.current) { fetchProfile(userId, retryCount + 1, onComplete); } }, delay); } else { // All retries exhausted if (isMountedRef.current) { setProfile(null); onComplete?.(); } } } }; const refreshProfile = async () => { if (user) { await fetchProfile(user.id); } }; // Verify session is still valid const verifySession = async (updateLoadingState = false) => { if (updateLoadingState && isMountedRef.current) { setLoading(true); } try { const { data: { session }, error } = await supabase.auth.getSession(); if (error) { console.error('[Auth] Session verification failed:', error); setSessionError(error.message); if (updateLoadingState && isMountedRef.current) { setLoading(false); } return false; } if (!session) { console.log('[Auth] No active session found'); if (updateLoadingState && isMountedRef.current) { setLoading(false); } return false; } console.log('[Auth] Session verified:', session.user.email); sessionVerifiedRef.current = true; // Update state if session was found but not set if (!user && isMountedRef.current) { setSession(session); setUser(session.user); sessionVerifiedRef.current = true; } else if (updateLoadingState && isMountedRef.current) { setLoading(false); } return true; } catch (error) { console.error('[Auth] Session verification error:', error); if (updateLoadingState && isMountedRef.current) { setLoading(false); } return false; } }; // Keep loading state ref in sync useEffect(() => { loadingStateRef.current = loading; console.log('[Auth] Loading state changed:', loading); }, [loading]); useEffect(() => { // CRITICAL: Set up listener FIRST to catch all events const { data: { subscription }, } = supabase.auth.onAuthStateChange((event, session) => { console.log('[Auth] State change:', event, 'User:', session?.user?.email || 'none'); if (!isMountedRef.current) return; const currentEmail = session?.user?.email; const newEmailPending = session?.user?.new_email; // Clear any error setSessionError(null); // Explicitly handle SIGNED_IN and INITIAL_SESSION events if (event === 'SIGNED_IN' && session) { console.log('[Auth] SIGNED_IN detected, setting session and user'); setSession(session); setUser(session.user); sessionVerifiedRef.current = true; } else if (event === 'INITIAL_SESSION') { if (session?.user) { console.log('[Auth] INITIAL_SESSION detected with session, setting user'); setSession(session); setUser(session.user); sessionVerifiedRef.current = true; } else { console.log('[Auth] INITIAL_SESSION detected with NO session - user not logged in'); setSession(null); setUser(null); setProfile(null); sessionVerifiedRef.current = false; // CRITICAL: Set loading to false immediately when no session setLoading(false); } } else if (event === 'SIGNED_OUT') { console.log('[Auth] SIGNED_OUT detected, clearing session'); setSession(null); setUser(null); setProfile(null); sessionVerifiedRef.current = false; } else { setSession(session); setUser(session?.user ?? null); } // Track pending email changes setPendingEmail(newEmailPending ?? null); // Detect confirmed email change: email changed AND no longer pending if ( session?.user && previousEmailRef.current && currentEmail && currentEmail !== previousEmailRef.current && !newEmailPending ) { // Clear any existing Novu update timeout if (novuUpdateTimeoutRef.current) { clearTimeout(novuUpdateTimeoutRef.current); } // Defer Novu update and notifications to avoid blocking auth const oldEmail = previousEmailRef.current; novuUpdateTimeoutRef.current = setTimeout(async () => { if (!isMountedRef.current) return; try { // Update Novu subscriber with confirmed email const { notificationService } = await import('@/lib/notificationService'); if (notificationService.isEnabled()) { await notificationService.updateSubscriber({ subscriberId: session.user.id, email: currentEmail, }); } // Log the confirmed email change await supabase.from('admin_audit_log').insert({ admin_user_id: session.user.id, target_user_id: session.user.id, action: 'email_change_completed', details: { old_email: oldEmail, new_email: currentEmail, timestamp: new Date().toISOString(), }, }); // Send final security notification if (notificationService.isEnabled()) { await notificationService.trigger({ workflowId: 'email-changed', subscriberId: session.user.id, payload: { oldEmail: oldEmail, newEmail: currentEmail, timestamp: new Date().toISOString(), }, }); } } catch (error) { console.error('Error updating Novu after email confirmation:', error); } finally { novuUpdateTimeoutRef.current = null; } }, 0); } // Update tracked email if (currentEmail) { previousEmailRef.current = currentEmail; } if (session?.user) { // Clear any existing profile fetch timeout if (profileFetchTimeoutRef.current) { clearTimeout(profileFetchTimeoutRef.current); } // Defer profile fetch to avoid deadlock const shouldWaitForProfile = (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') && session !== null; console.log('[Auth] Profile fetch deferred, shouldWaitForProfile:', shouldWaitForProfile, 'event:', event); profileFetchTimeoutRef.current = setTimeout(() => { if (!isMountedRef.current) return; fetchProfile(session.user.id, 0, () => { if (isMountedRef.current && shouldWaitForProfile) { console.log('[Auth] Profile fetch complete, setting loading to false'); setLoading(false); } }); profileFetchTimeoutRef.current = null; }, 0); } else { if (isMountedRef.current) { setProfile(null); // CRITICAL: Always resolve loading when there's no session if (event === 'INITIAL_SESSION' || event === 'SIGNED_OUT') { console.log('[Auth] No session for event:', event, '- setting loading to false'); setLoading(false); } } } // CRITICAL: Always set loading false for non-session events if (event !== 'SIGNED_IN' && event !== 'INITIAL_SESSION') { console.log('[Auth] Setting loading to false immediately for event:', event); setLoading(false); } }); // THEN get initial session supabase.auth.getSession().then(({ data: { session }, error }) => { if (!isMountedRef.current) return; if (error) { console.error('[Auth] Initial session fetch error:', error); setSessionError(error.message); setLoading(false); return; } setSession(session); setUser(session?.user ?? null); if (session?.user) { sessionVerifiedRef.current = true; // Fetch profile with completion callback fetchProfile(session.user.id, 0, () => { if (isMountedRef.current) { setLoading(false); } }); } else { setLoading(false); } }); // Add a safety timeout to force loading to resolve loadingTimeoutRef.current = setTimeout(() => { if (isMountedRef.current && loadingStateRef.current) { console.warn('[Auth] Loading timeout reached after 5 seconds, forcing loading to false'); setLoading(false); } }, 5000); // Session verification fallback after 2 seconds const verificationTimeout = setTimeout(() => { if (!sessionVerifiedRef.current && isMountedRef.current) { console.log('[Auth] Session not verified, attempting manual verification'); verifySession(); } }, 2000); // Handle page visibility changes const handleVisibilityChange = () => { if (document.visibilityState === 'visible' && isMountedRef.current) { console.log('[Auth] Tab became visible, verifying session'); verifySession(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { isMountedRef.current = false; subscription.unsubscribe(); clearTimeout(verificationTimeout); if (loadingTimeoutRef.current) { clearTimeout(loadingTimeoutRef.current); } document.removeEventListener('visibilitychange', handleVisibilityChange); // Clear any pending timeouts if (profileFetchTimeoutRef.current) { clearTimeout(profileFetchTimeoutRef.current); profileFetchTimeoutRef.current = null; } if (novuUpdateTimeoutRef.current) { clearTimeout(novuUpdateTimeoutRef.current); novuUpdateTimeoutRef.current = null; } }; }, []); const signOut = async () => { const { error } = await supabase.auth.signOut(); if (error) { console.error('Error signing out:', error); throw error; } }; const clearPendingEmail = () => { setPendingEmail(null); }; const value = { user, session, profile, loading, pendingEmail, sessionError, signOut, refreshProfile, verifySession, clearPendingEmail, }; return {children}; } export const AuthProvider = AuthProviderComponent; export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }