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 fetchProfile = async (userId: string, retryCount = 0) => { 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); } }, delay); return; } // Show user-friendly error notification only after all retries if (isMountedRef.current && retryCount >= 3) { toast({ title: "Profile Loading Error", description: "Unable to load your profile. Please refresh the page or try again later.", variant: "destructive", }); } return; } // Only update state if component is still mounted if (isMountedRef.current) { setProfile(data as Profile); profileRetryCountRef.current = 0; // Reset retry count on success } } 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); } }, delay); } } }; const refreshProfile = async () => { if (user) { await fetchProfile(user.id); } }; // Verify session is still valid const verifySession = async () => { try { const { data: { session }, error } = await supabase.auth.getSession(); if (error) { console.error('[Auth] Session verification failed:', error); setSessionError(error.message); return false; } if (!session) { console.log('[Auth] No active session found'); 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); fetchProfile(session.user.id); } return true; } catch (error) { console.error('[Auth] Session verification error:', error); return false; } }; 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 event for iframe compatibility if (event === 'SIGNED_IN' && session) { console.log('[Auth] SIGNED_IN detected, setting session and user'); setSession(session); setUser(session.user); sessionVerifiedRef.current = true; 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 profileFetchTimeoutRef.current = setTimeout(() => { if (!isMountedRef.current) return; fetchProfile(session.user.id); profileFetchTimeoutRef.current = null; }, 0); } else { if (isMountedRef.current) { setProfile(null); } } 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); } setSession(session); setUser(session?.user ?? null); if (session?.user) { fetchProfile(session.user.id); sessionVerifiedRef.current = true; } setLoading(false); }); // 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); 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; }