import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { Session, User } from '@supabase/supabase-js'; import { supabase } from '../supabaseClient'; import { logger } from '../debugConfig'; import { CCUser, CCUserMetadata } from '../services/auth/authService'; import { UserPreferences } from '../services/auth/profileService'; import { storageService, StorageKeys } from '../services/auth/localStorageService'; export interface UserContextType { user: CCUser | null; loading: boolean; error: Error | null; profile: CCUser | null; preferences: UserPreferences; isMobile: boolean; isInitialized: boolean; updateProfile: (updates: Partial) => Promise; updatePreferences: (updates: Partial) => Promise; clearError: () => void; } export const UserContext = createContext({ user: null, loading: true, error: null, profile: null, preferences: {}, isMobile: false, isInitialized: false, updateProfile: async () => {}, updatePreferences: async () => {}, clearError: () => {} }); export const UserProvider = ({ children }: { children: React.ReactNode }) => { const [user] = useState(null); const [profile, setProfile] = useState(null); const [preferences, setPreferences] = useState({}); const [loading, setLoading] = useState(true); const [isInitialized, setIsInitialized] = useState(false); const [error, setError] = useState(null); const [isMobile] = useState(window.innerWidth <= 768); const mountedRef = React.useRef(true); // Use the main Supabase client for all operations to ensure proper session persistence // This avoids the "Multiple GoTrueClient instances" warning and ensures session restoration works const resolveProfile = useCallback(async (supabaseUser?: User | null, session?: Session | null) => { // Prevent duplicate work when we already have the same user resolved if (mountedRef.current && isInitialized) { const resolvedUserId = profile?.id; const incomingUserId = supabaseUser?.id ?? session?.user?.id ?? null; if (!incomingUserId && !supabaseUser) { logger.debug('user-context', '⚠️ Profile already initialized for guest session, skipping resolution'); return; } if (incomingUserId && resolvedUserId && resolvedUserId === incomingUserId) { logger.debug('user-context', '⚠️ Profile already initialized for current user, skipping resolution'); return; } } let userInfo: User | null = null; // Declare at function scope try { logger.debug('user-context', '🔄 Resolving user profile', { hasSupabaseUser: !!supabaseUser, isInitialized }); logger.debug('user-context', '🔧 Step 1: Starting profile resolution...'); // Don't set loading to true immediately - let the UI show progress naturally let authSession = session; userInfo = supabaseUser ?? null; logger.debug('user-context', '🔧 Step 2: Getting auth session...'); if (!authSession) { const { data } = await supabase.auth.getSession(); authSession = data.session; logger.debug('user-context', '🔧 Step 2a: Got session from supabase', { hasSession: !!authSession }); } logger.debug('user-context', '🔧 Step 3: Getting user info...'); if (!userInfo) { const { data } = await supabase.auth.getUser(); userInfo = data.user; logger.debug('user-context', '🔧 Step 3a: Got user from supabase', { hasUser: !!userInfo }); } if (!userInfo) { logger.debug('user-context', '⚠️ No user info available - clearing profile'); if (!mountedRef.current) { return; } setProfile(null); setPreferences({}); setError(null); setLoading(false); return; } logger.debug('user-context', '🔧 Step 4: User info available, proceeding...', { userId: userInfo.id, email: userInfo.email }); let profileRow: Record | null = null; // Fast-path: build profile from auth metadata immediately (no spinner on refresh). const fastMetadata = userInfo.user_metadata as CCUserMetadata; const fastProfile: CCUser = { id: userInfo.id, email: userInfo.email, user_type: fastMetadata?.user_type || '', username: fastMetadata?.username || userInfo.email?.split('@')[0] || 'user', display_name: String(fastMetadata?.display_name || ''), school_id: null, created_at: userInfo.created_at, updated_at: userInfo.updated_at }; if (mountedRef.current && !isInitialized) { setProfile(fastProfile); setLoading(false); setIsInitialized(true); logger.debug('user-context', '⚡ Fast-path: profile initialized from auth metadata'); } // Background: query Supabase profiles for authoritative data (user_db_name, school_db_name, display_name). // No setLoading toggles — spinner is already cleared above. const { data, error } = await supabase .from('profiles') .select('*') .eq('id', userInfo.id) .single(); if (error && error.code !== 'PGRST116') { logger.warn('user-context', '⚠️ Profiles query failed, using fast-path profile', { error: error.message, code: error.code }); profileRow = null; } else if (data) { profileRow = data; logger.debug('user-context', '✅ Supabase profile fetched', { userId: data.id, userDbName: data.user_db_name }); } else { profileRow = null; } logger.debug('user-context', '🔧 Step 6: Processing profile data...', { userId: userInfo.id, hasProfileRow: !!profileRow, hasUserDb: !!profileRow?.user_db_name, hasSchoolDb: !!profileRow?.school_db_name }); const metadata = userInfo.user_metadata as CCUserMetadata; const userProfile: CCUser = { id: userInfo.id, email: userInfo.email, user_type: metadata.user_type || '', username: metadata.username || '', display_name: String(metadata.display_name || ''), school_id: (profileRow?.school_id as string) ?? null, created_at: userInfo.created_at, updated_at: userInfo.updated_at }; if (!mountedRef.current) { logger.debug('user-context', '❌ Component unmounted during profile creation'); return; } logger.debug('user-context', '🔧 Setting profile and preferences...', { profileId: userProfile.id }); setProfile(userProfile); setPreferences({ theme: (profileRow?.theme && typeof profileRow.theme === 'string' && ['system', 'light', 'dark'].includes(profileRow.theme)) ? profileRow.theme as 'system' | 'light' | 'dark' : 'system', notifications: Boolean(profileRow?.notifications_enabled) }); logger.debug('user-context', '✅ User profile loaded', { userId: userProfile.id, userType: userProfile.user_type, username: userProfile.username }); setError(null); } catch (error) { logger.error('user-context', '❌ Failed to load user profile', { error }); if (!mountedRef.current) { return; } logger.error('user-context', '❌ Resolving user profile failed', { message: error instanceof Error ? error.message : String(error) }); // Create fallback profile even when errors occur logger.debug('user-context', '🔧 Creating fallback profile due to error...', { userId: userInfo?.id, email: userInfo?.email }); if (userInfo) { const metadata = userInfo.user_metadata as CCUserMetadata; const fallbackProfile: CCUser = { id: userInfo.id, email: userInfo.email, user_type: metadata?.user_type || 'email_teacher', username: metadata?.username || userInfo.email?.split('@')[0] || 'user', display_name: metadata?.display_name || userInfo.email?.split('@')[0] || 'User', school_id: null, created_at: userInfo.created_at, updated_at: userInfo.updated_at }; setProfile(fallbackProfile); logger.debug('user-context', '✅ Fallback profile created', { userId: fallbackProfile.id, userType: fallbackProfile.user_type }); } else { setProfile(null); } setPreferences({}); setError(error instanceof Error ? error : new Error('Failed to load user profile')); setLoading(false); // Ensure loading is cleared on error } finally { logger.debug('user-context', '🔧 Finalizing user context initialization...', { isMounted: mountedRef.current }); if (mountedRef.current) { // Loading state is already managed above, just log completion logger.debug('user-context', '✅ User context initialization complete'); } logger.debug('user-context', '🔧 Step 10: Setting isInitialized to true'); setIsInitialized(true); logger.debug('user-context', '✅ User context initialized flag set - initialization complete!', { isInitialized: true, profileId: profile?.id, userType: profile?.user_type }); } }, [profile?.id, profile?.user_type, isInitialized]); useEffect(() => { mountedRef.current = true; const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => { if (!mountedRef.current) { return; } logger.debug('user-context', '🔄 Auth state change', { event, hasSession: !!session, hasUser: !!session?.user }); switch (event) { case 'SIGNED_OUT': setLoading(false); setProfile(null); setPreferences({}); setIsInitialized(true); setError(null); break; case 'SIGNED_IN': case 'TOKEN_REFRESHED': case 'INITIAL_SESSION': await resolveProfile(session?.user ?? null, session ?? null); break; default: break; } }); return () => { mountedRef.current = false; subscription.unsubscribe(); }; }, [resolveProfile]); const updateProfile = async (updates: Partial) => { if (!user?.id || !profile) { return; } setLoading(true); try { const { error } = await supabase .from('profiles') .update({ ...updates, updated_at: new Date().toISOString() }) .eq('id', user.id); if (error) { throw error; } setProfile(prev => prev ? { ...prev, ...updates } : null); logger.info('user-context', '✅ Profile updated successfully'); } catch (error) { logger.error('user-context', '❌ Failed to update profile', { error }); setError(error instanceof Error ? error : new Error('Failed to update profile')); throw error; } finally { setLoading(false); } }; const updatePreferences = async (updates: Partial) => { if (!user?.id) { return; } setLoading(true); try { const newPreferences = { ...preferences, ...updates }; setPreferences(newPreferences); const { error } = await supabase .from('profiles') .update({ preferences: newPreferences, updated_at: new Date().toISOString() }) .eq('id', user.id); if (error) { throw error; } logger.info('user-context', '✅ Preferences updated successfully'); } catch (error) { logger.error('user-context', '❌ Failed to update preferences', { error }); setError(error instanceof Error ? error : new Error('Failed to update preferences')); throw error; } finally { setLoading(false); } }; // Store profile in localStorage whenever it changes useEffect(() => { if (profile) { storageService.set(StorageKeys.USER, profile); logger.debug('user-context', '💾 Stored user profile in localStorage', { userId: profile.id, userType: profile.user_type }); } else { storageService.remove(StorageKeys.USER); logger.debug('user-context', '🗑️ Removed user profile from localStorage'); } }, [profile]); return ( setError(null) }} > {children} ); } export const useUser = () => useContext(UserContext);