import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import SplashPage from '../pages/SplashPage'; import type { User } from '@supabase/supabase-js'; import { supabase } from '../supabase'; import { buildCacheKey, readCache, writeCache, clearUserCache, } from '../utils/localCache'; export interface Profile { id: string; first_name: string; last_name: string; full_name: string; phone: string; country_of_residence: string; avatar_path: string | null; fcm_token: string | null; notification_push_enabled: boolean; notification_email_enabled: boolean; notification_sms_enabled: boolean; created_at: string; updated_at: string; } export type ProfileStatus = 'idle' | 'loading' | 'loaded' | 'missing' | 'error'; interface AuthContextType { user: User | null; profile: Profile | null; profileStatus: ProfileStatus; refreshProfile: () => Promise; setProfile: (profile: Profile | null) => void; signOut: () => Promise; } const BYPASS_AUTH = false; const AuthContext = createContext({ user: null, profile: null, profileStatus: BYPASS_AUTH ? 'idle' : 'loading', refreshProfile: async () => null, setProfile: () => {}, signOut: async () => {}, }); const withTimeout = async ( request: PromiseLike, timeoutMs: number, label: string ): Promise => { let timeoutId: ReturnType | undefined; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject( new Error( `${label} timed out. Please check your connection and try again.` ) ); }, timeoutMs); }); try { return await Promise.race([Promise.resolve(request), timeout]); } finally { if (timeoutId) clearTimeout(timeoutId); } }; export const useAuth = () => useContext(AuthContext); export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const [user, setUser] = useState(null); const [profile, setProfileState] = useState(null); const [profileStatus, setProfileStatus] = useState( BYPASS_AUTH ? 'idle' : 'loading' ); const [initializing, setInitializing] = useState(!BYPASS_AUTH); const activeProfileFetchRef = useRef(0); const mountedRef = useRef(false); const currentUserIdRef = useRef(null); const applyProfile = useCallback((nextProfile: Profile | null) => { if (!mountedRef.current) return; setProfileState(nextProfile); setProfileStatus(nextProfile ? 'loaded' : 'missing'); }, []); const setProfile = useCallback( (nextProfile: Profile | null) => { activeProfileFetchRef.current += 1; applyProfile(nextProfile); }, [applyProfile] ); const fetchProfile = useCallback( async ( userId: string, options?: { showLoading?: boolean } ): Promise => { const fetchId = activeProfileFetchRef.current + 1; activeProfileFetchRef.current = fetchId; if ((options?.showLoading ?? true) && mountedRef.current) { setProfileStatus('loading'); } const cacheKey = buildCacheKey(userId, 'profile'); try { const cachedProfile = await readCache(cacheKey); if ( cachedProfile && mountedRef.current && fetchId === activeProfileFetchRef.current && currentUserIdRef.current === userId ) { applyProfile(cachedProfile); } } catch (err) { console.error('[AuthProvider] Failed to read cached profile', err); } try { const { data, error } = await withTimeout( supabase.from('profiles').select('*').eq('id', userId).maybeSingle(), 8000, 'Profile loading' ); if ( !mountedRef.current || fetchId !== activeProfileFetchRef.current || currentUserIdRef.current !== userId ) { return data ? (data as Profile) : null; } if (error) { console.error('[AuthProvider] Failed to fetch profile', error); setProfileState(null); setProfileStatus('error'); return null; } const nextProfile = data ? (data as Profile) : null; applyProfile(nextProfile); if (nextProfile) { void writeCache(cacheKey, nextProfile); } return nextProfile; } catch (error) { if ( mountedRef.current && fetchId === activeProfileFetchRef.current && currentUserIdRef.current === userId ) { console.error('[AuthProvider] Profile fetch crashed', error); setProfileState(null); setProfileStatus('error'); } return null; } }, [applyProfile] ); useEffect(() => { mountedRef.current = true; if (BYPASS_AUTH) { setInitializing(false); setProfileStatus('idle'); return () => { mountedRef.current = false; activeProfileFetchRef.current += 1; }; } let cancelled = false; supabase.auth .getSession() .then(async ({ data }) => { if (cancelled || !mountedRef.current) return; const currentUser = data.session?.user ?? null; const keepSignedIn = localStorage.getItem('kumusha_keep_signed_in'); if (currentUser && keepSignedIn === 'false') { currentUserIdRef.current = null; setUser(null); setProfileState(null); setProfileStatus('idle'); await clearUserCache(currentUser.id); await supabase.auth.signOut(); if (!cancelled && mountedRef.current) { setInitializing(false); } return; } currentUserIdRef.current = currentUser?.id ?? null; setUser(currentUser); if (currentUser) { await fetchProfile(currentUser.id, { showLoading: false }); } else { setProfileState(null); setProfileStatus('idle'); } }) .catch((error) => { console.error('[AuthProvider] Failed to initialise session', error); if (!cancelled && mountedRef.current) { currentUserIdRef.current = null; setUser(null); setProfileState(null); setProfileStatus('missing'); } }) .finally(() => { if (!cancelled && mountedRef.current) { setInitializing(false); } }); const { data: { subscription }, } = supabase.auth.onAuthStateChange(async (_event, session) => { const nextUser = session?.user ?? null; if (!mountedRef.current) return; const previousUserId = currentUserIdRef.current; currentUserIdRef.current = nextUser?.id ?? null; if (nextUser) { if (nextUser.id !== previousUserId) { setProfileState(null); setProfileStatus('loading'); } setUser(nextUser); await fetchProfile(nextUser.id, { showLoading: false }); } else { activeProfileFetchRef.current += 1; setUser(null); setProfileState(null); setProfileStatus('idle'); if (previousUserId) { await clearUserCache(previousUserId); } } }); return () => { cancelled = true; mountedRef.current = false; activeProfileFetchRef.current += 1; subscription.unsubscribe(); }; }, [fetchProfile]); const signOut = useCallback(async () => { activeProfileFetchRef.current += 1; const currentUserId = currentUserIdRef.current; currentUserIdRef.current = null; setUser(null); setProfileState(null); setProfileStatus('idle'); if (currentUserId) { await clearUserCache(currentUserId); } localStorage.removeItem('kumusha_keep_signed_in'); await supabase.auth.signOut(); }, []); const refreshProfile = useCallback(() => { return user ? fetchProfile(user.id, { showLoading: false }) : Promise.resolve(null); }, [fetchProfile, user]); const contextValue = useMemo( () => ({ user, profile, profileStatus, refreshProfile, setProfile, signOut, }), [profile, profileStatus, refreshProfile, setProfile, signOut, user] ); if (initializing) { return ; } return ( {children} ); };