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(() => { console.log('[Auth] Initializing auth provider'); // 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', 'Has session:', !!session); // Extract email info early for cleanup const currentEmail = session?.user?.email; const newEmailPending = session?.user?.new_email; // CRITICAL: Always clear/update pending email state BEFORE any early returns setPendingEmail(newEmailPending ?? null); // Clear any error setSessionError(null); // Update session and user state based on event 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 with user, setting session'); setSession(session); setUser(session.user); sessionVerifiedRef.current = true; } else { console.log('[Auth] INITIAL_SESSION with no user - setting loading to false'); setSession(null); setUser(null); setProfile(null); sessionVerifiedRef.current = false; // CRITICAL: Set loading to false immediately when no session setLoading(false); return; // Exit early, no need to fetch profile } } else if (event === 'SIGNED_OUT') { console.log('[Auth] SIGNED_OUT detected, clearing all state'); setSession(null); setUser(null); setProfile(null); sessionVerifiedRef.current = false; setLoading(false); return; // Exit early, no need to fetch profile } else { setSession(session); setUser(session?.user ?? 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 () => { 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; } // Handle profile fetching for authenticated users if (session?.user) { // Clear any existing profile fetch timeout if (profileFetchTimeoutRef.current) { clearTimeout(profileFetchTimeoutRef.current); } // Only wait for profile on initial auth events const shouldWaitForProfile = (event === 'SIGNED_IN' || event === 'INITIAL_SESSION'); console.log('[Auth] Fetching profile, shouldWaitForProfile:', shouldWaitForProfile); profileFetchTimeoutRef.current = setTimeout(() => { fetchProfile(session.user.id, 0, () => { if (shouldWaitForProfile) { console.log('[Auth] Profile fetch complete, setting loading to false'); setLoading(false); } }); profileFetchTimeoutRef.current = null; }, 0); } else { // No session/user - clear profile and resolve loading setProfile(null); console.log('[Auth] No user, setting loading to false'); setLoading(false); } }); // THEN get initial session (this may trigger INITIAL_SESSION event) supabase.auth.getSession().then(({ data: { session }, error }) => { if (error) { console.error('[Auth] Initial session fetch error:', error); setSessionError(error.message); setLoading(false); return; } // Note: onAuthStateChange will handle the INITIAL_SESSION event // This is just a backup in case the event doesn't fire console.log('[Auth] getSession completed, session exists:', !!session); }); // Add a STRONG safety timeout to force loading to resolve loadingTimeoutRef.current = setTimeout(() => { if (loadingStateRef.current) { console.warn('[Auth] ⚠️ SAFETY TIMEOUT: Forcing loading to false after 3 seconds'); setLoading(false); } }, 3000); // Session verification fallback const verificationTimeout = setTimeout(() => { if (!sessionVerifiedRef.current) { console.log('[Auth] Session not verified after 2s, attempting manual verification'); verifySession(); } }, 2000); // Handle page visibility changes const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { console.log('[Auth] Tab became visible, verifying session'); verifySession(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { console.log('[Auth] Cleaning up auth provider'); 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; }