// src/providers/AuthProvider.tsx import React, { useState, useEffect, useCallback, ReactNode, useMemo } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { AuthContext, AuthContextType } from '../contexts/AuthContext'; import type { UserProfile } from '../types'; import * as apiClient from '../services/apiClient'; import { useAuthProfileQuery, AUTH_PROFILE_QUERY_KEY } from '../hooks/queries/useAuthProfileQuery'; import { getToken, setToken, removeToken } from '../services/tokenStorage'; import { logger } from '../services/logger.client'; /** * AuthProvider component that manages authentication state. * * Refactored to use TanStack Query (ADR-0005 Phase 7). */ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const queryClient = useQueryClient(); const [userProfile, setUserProfile] = useState(null); const [authStatus, setAuthStatus] = useState('Determining...'); const [isLoading, setIsLoading] = useState(true); // Use TanStack Query to fetch the authenticated user's profile const { data: fetchedProfile, isLoading: isQueryLoading, isError, isFetched, } = useAuthProfileQuery(); // Effect to sync query result with auth state useEffect(() => { // Only process once the query has completed at least once if (!isFetched && isQueryLoading) { return; } const token = getToken(); if (fetchedProfile) { logger.info('[AuthProvider] Profile received from query, setting state to AUTHENTICATED.'); setUserProfile(fetchedProfile); setAuthStatus('AUTHENTICATED'); } else if (token && isError) { logger.warn('[AuthProvider] Token was present but validation failed. Signing out.'); removeToken(); setUserProfile(null); setAuthStatus('SIGNED_OUT'); } else if (token && isFetched && !fetchedProfile) { // Token exists, query completed, but profile is null - sign out logger.warn('[AuthProvider] Token was present but profile is null. Signing out.'); removeToken(); setUserProfile(null); setAuthStatus('SIGNED_OUT'); } else if (!token) { logger.info('[AuthProvider] No auth token found. Setting state to SIGNED_OUT.'); setAuthStatus('SIGNED_OUT'); } setIsLoading(false); }, [fetchedProfile, isQueryLoading, isError, isFetched]); const logout = useCallback(() => { logger.info('[AuthProvider-Logout] Clearing user data and auth token.'); removeToken(); setUserProfile(null); setAuthStatus('SIGNED_OUT'); // Clear the auth profile cache on logout queryClient.removeQueries({ queryKey: AUTH_PROFILE_QUERY_KEY }); }, [queryClient]); 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'); // Update the query cache with the provided profile queryClient.setQueryData(AUTH_PROFILE_QUERY_KEY, profileData); 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 { // Directly fetch the profile (not using the query hook since we need immediate results) const response = await apiClient.getAuthenticatedUserProfile(); if (!response.ok) { const error = await response.json().catch(() => ({ message: `Request failed with status ${response.status}`, })); throw new Error(error.message || 'Failed to fetch profile'); } const fetchedProfileData: UserProfile = await response.json(); if (!fetchedProfileData) { throw new Error('Received null or undefined profile from API.'); } setUserProfile(fetchedProfileData); setAuthStatus('AUTHENTICATED'); // Update the query cache with the fetched profile queryClient.setQueryData(AUTH_PROFILE_QUERY_KEY, fetchedProfileData); 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}`); } } }, [logout, queryClient], ); const updateProfile = useCallback( (updatedProfileData: Partial) => { logger.info('[AuthProvider-UpdateProfile] Updating profile state.', { updatedProfileData }); setUserProfile((prevProfile) => { if (!prevProfile) return null; const newProfile = { ...prevProfile, ...updatedProfileData }; // Keep the query cache in sync queryClient.setQueryData(AUTH_PROFILE_QUERY_KEY, newProfile); return newProfile; }); }, [queryClient], ); const value = useMemo( () => ({ userProfile, authStatus, isLoading, login, logout, updateProfile, }), [userProfile, authStatus, isLoading, login, logout, updateProfile], ); return {children}; };