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; signOut: () => Promise; refreshProfile: () => 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); // Refs for lifecycle and cleanup management const isMountedRef = useRef(true); const profileFetchTimeoutRef = useRef(null); const novuUpdateTimeoutRef = useRef(null); const previousEmailRef = useRef(null); const fetchProfile = async (userId: string) => { try { const { data, error } = await supabase .from('profiles') .select(`*, location:locations(*)`) .eq('user_id', userId) .maybeSingle(); if (error && error.code !== 'PGRST116') { console.error('Error fetching profile:', error); // Show user-friendly error notification if (isMountedRef.current) { 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); } } catch (error) { console.error('Error fetching profile:', error); // Show user-friendly error notification if (isMountedRef.current) { toast({ title: "Profile Loading Error", description: "An unexpected error occurred while loading your profile.", variant: "destructive", }); } } }; const refreshProfile = async () => { if (user) { await fetchProfile(user.id); } }; useEffect(() => { // Get initial session supabase.auth.getSession().then(({ data: { session } }) => { if (!isMountedRef.current) return; setSession(session); setUser(session?.user ?? null); if (session?.user) { fetchProfile(session.user.id); } setLoading(false); }); // Listen for auth changes 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; // 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); setLoading(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); }); return () => { isMountedRef.current = false; subscription.unsubscribe(); // 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, signOut, refreshProfile, 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; }