// src/providers/AuthProvider.tsx import React, { useState, useEffect, useCallback, ReactNode, useMemo } from 'react'; import { AuthContext, AuthContextType } from '../contexts/AuthContext'; import type { UserProfile } from '../types'; import * as apiClient from '../services/apiClient'; import { useApi } from '../hooks/useApi'; import { getToken, setToken, removeToken } from '../services/tokenStorage'; import { logger } from '../services/logger.client'; export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [userProfile, setUserProfile] = useState(null); const [authStatus, setAuthStatus] = useState('Determining...'); const [isLoading, setIsLoading] = useState(true); // FIX: Stabilize the apiFunction passed to useApi. // By wrapping this in useCallback, we ensure the same function instance is passed to // useApi on every render. This prevents the `execute` function returned by `useApi` // from being recreated, which in turn breaks the infinite re-render loop in the useEffect below. const getProfileCallback = useCallback(() => apiClient.getAuthenticatedUserProfile(), []); const { execute: checkTokenApi } = useApi(getProfileCallback); const { execute: fetchProfileApi } = useApi(getProfileCallback); useEffect(() => { // This flag prevents state updates if the component unmounts or if another // auth operation (like login/logout) occurs before this initial check completes. let isMounted = true; logger.info('[AuthProvider-Effect] Starting initial authentication check.'); const checkAuthToken = async () => { const token = getToken(); if (token) { logger.info('[AuthProvider-Effect] Found auth token. Validating...'); try { const fetchedProfile = await checkTokenApi(); if (isMounted && fetchedProfile) { logger.info('[AuthProvider-Effect] Profile received, setting state to AUTHENTICATED.'); setUserProfile(fetchedProfile); setAuthStatus('AUTHENTICATED'); } else if (isMounted) { logger.warn( '[AuthProvider-Effect] Token was present but validation returned no profile. Signing out.', ); removeToken(); setUserProfile(null); setAuthStatus('SIGNED_OUT'); } } catch (e: unknown) { // This catch block is now primarily for unexpected errors, as useApi handles API errors. logger.warn('Auth token validation failed. Clearing token.', { error: e }); if (isMounted) { removeToken(); setUserProfile(null); setAuthStatus('SIGNED_OUT'); } } } else { logger.info('[AuthProvider-Effect] No auth token found. Setting state to SIGNED_OUT.'); if (isMounted) { setAuthStatus('SIGNED_OUT'); } } if (isMounted) { logger.info( '[AuthProvider-Effect] Initial auth check finished. Setting isLoading to false.', ); setIsLoading(false); } }; checkAuthToken(); return () => { logger.info('[AuthProvider-Effect] Component unmounting, cleaning up.'); isMounted = false; }; }, [checkTokenApi]); const logout = useCallback(() => { logger.info('[AuthProvider-Logout] Clearing user data and auth token.'); removeToken(); setUserProfile(null); setAuthStatus('SIGNED_OUT'); }, []); const login = useCallback( async (token: string, profileData?: UserProfile) => { logger.info(`[AuthProvider-Login] Attempting login.`); setToken(token); if (profileData) { // If profile is provided (e.g., from credential login), use it directly. logger.info('[AuthProvider-Login] Profile data received directly.'); setUserProfile(profileData); setAuthStatus('AUTHENTICATED'); logger.info('[AuthProvider-Login] Login successful. State set to AUTHENTICATED.', { user: profileData.user, }); } else { // If no profile is provided (e.g., from OAuth or token refresh), fetch it. logger.info('[AuthProvider-Login] Auth token set in storage. Fetching profile...'); try { const fetchedProfile = await fetchProfileApi(); if (!fetchedProfile) { throw new Error('Received null or undefined profile from API.'); } setUserProfile(fetchedProfile); setAuthStatus('AUTHENTICATED'); logger.info('[AuthProvider-Login] Profile fetch successful. State set to AUTHENTICATED.'); } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); logger.error('Failed to fetch user data after login. Rolling back auth state.', { error: errorMessage, }); logout(); // Log the user out to prevent an inconsistent state. // Re-throw the error so the calling component can handle it (e.g., show a notification) throw new Error(`Login succeeded, but failed to fetch your data: ${errorMessage}`); } } }, [fetchProfileApi, logout], ); const updateProfile = useCallback((updatedProfileData: Partial) => { logger.info('[AuthProvider-UpdateProfile] Updating profile state.', { updatedProfileData }); setUserProfile((prevProfile) => { if (!prevProfile) return null; return { ...prevProfile, ...updatedProfileData }; }); }, []); const value = useMemo( () => ({ userProfile, authStatus, isLoading, login, logout, updateProfile, }), [userProfile, authStatus, isLoading, login, logout, updateProfile], ); return {children}; };