'use client'; import { createContext, useContext, useEffect, useState, useCallback } from 'react'; import { User, Session } from '@supabase/supabase-js'; import { getSupabase } from '@/lib/supabase'; import { useRouter } from 'next/navigation'; import { api, setApiToken } from '@/lib/api'; interface BackendUser { id: string; email: string; name: string | null; avatarUrl: string | null; subscriptionTier: string; } interface AuthContextType { user: User | null; backendUser: BackendUser | null; session: Session | null; loading: boolean; signUp: (email: string, password: string, name: string) => Promise<{ error: Error | null }>; signIn: (email: string, password: string) => Promise<{ error: Error | null }>; signOut: () => Promise; getAccessToken: () => Promise; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null); const [backendUser, setBackendUser] = useState(null); const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); const router = useRouter(); const supabase = getSupabase(); useEffect(() => { // Get initial session supabase.auth.getSession().then(({ data: { session } }) => { setSession(session); setUser(session?.user ?? null); // Only set loading false when there's no session; otherwise wait for token exchange if (!session) { setApiToken(null); setLoading(false); } }); // Listen for auth changes const { data: { subscription }, } = supabase.auth.onAuthStateChange((_event, session) => { setSession(session); setUser(session?.user ?? null); if (!session) { setApiToken(null); setLoading(false); } }); return () => subscription.unsubscribe(); }, [supabase.auth]); const runExchange = useCallback(() => { if (!session?.access_token) { setApiToken(null); return; } let attempt = 0; const maxRetries = 3; const tryExchange = () => { api .exchangeToken(session.access_token) .then(({ accessToken, user: backendUserData }) => { setApiToken(accessToken); setBackendUser(backendUserData); setLoading(false); }) .catch(() => { attempt++; if (attempt < maxRetries) { // Retry with exponential backoff (500ms, 1500ms, 3500ms) setTimeout(tryExchange, 500 * Math.pow(2, attempt)); } else { setApiToken(null); setLoading(false); } }); }; tryExchange(); }, [session?.access_token]); // Exchange Supabase token for backend JWT; keep loading true until done so API calls wait for JWT useEffect(() => { runExchange(); }, [runExchange]); // Re-exchange on 401 (e.g. JWT expired) so next request gets a fresh token useEffect(() => { const handler = () => { if (session?.access_token) runExchange(); }; window.addEventListener('auth:unauthorized', handler); return () => window.removeEventListener('auth:unauthorized', handler); }, [session?.access_token, runExchange]); const signUp = useCallback( async (email: string, password: string, name: string) => { try { const { error } = await supabase.auth.signUp({ email, password, options: { data: { full_name: name, }, }, }); if (error) throw error; return { error: null }; } catch (error) { return { error: error as Error }; } }, [supabase.auth] ); const signIn = useCallback( async (email: string, password: string) => { try { const { error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) throw error; return { error: null }; } catch (error) { return { error: error as Error }; } }, [supabase.auth] ); const signOut = useCallback(async () => { await supabase.auth.signOut(); router.push('/'); }, [supabase.auth, router]); const getAccessToken = useCallback(async () => { const { data } = await supabase.auth.getSession(); return data.session?.access_token ?? null; }, [supabase.auth]); return ( {children} ); } export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }