import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueryClient } from '@tanstack/react-query'; import { useAuthStore } from '@/features/auth/store/authStore'; import { useUIStore } from '@/stores/ui'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; import { AstralBackground } from '@/components/ui/AstralBackground'; import { OfflineIndicator } from '@/components/OfflineIndicator'; import { AppRouter } from '@/router'; import { csrfService } from '@/services/csrf'; import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts'; import { KeyboardShortcutsPanel } from '@/components/ui/KeyboardShortcutsPanel'; import { useStateHydration } from '@/utils/stateHydration'; import { useQueryInvalidation } from '@/hooks/useQueryInvalidation'; import { usePatina } from '@/hooks/usePatina'; import { setupReactQuerySync } from '@/utils/reactQuerySync'; import { logger } from '@/utils/logger'; import { AudioProvider } from '@/context/AudioContext'; export function App() { const { t } = useTranslation(); const { refreshUser } = useAuthStore(); const { theme, setTheme, language, setLanguage } = useUIStore(); const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); // P1.2: Auth initialization state to prevent race condition const [isAuthReady, setIsAuthReady] = useState(false); const queryClient = useQueryClient(); // FE-COMP-022: Enable global keyboard shortcuts useGlobalKeyboardShortcuts({ enabled: true, onHelpOpen: () => setShowKeyboardHelp(true), }); // FE-STATE-003: Hydrate state from server on app load useStateHydration({ hydrateAuth: true, hydrateLibrary: false, // Can be enabled if needed hydrateChat: false, // Can be enabled if needed requireAuth: false, // Hydrate auth even if not authenticated (to check status) }); // FE-STATE-004: Listen for query invalidation events useQueryInvalidation(); // SUMI v3: Apply patina level based on user profile data usePatina(); // Action 2.3.1.2: Initialize React Query cache synchronization across tabs useEffect(() => { const cleanup = setupReactQuerySync(queryClient, { enabled: true, channelName: 'veza-react-query-sync', }); return cleanup; }, [queryClient]); // P1.2: Initialize auth state before rendering app // With httpOnly cookies we cannot read tokens in JS; always call refreshUser() // so getMe() is used to verify auth (cookies sent automatically). // CSRF token is fetched AFTER auth succeeds (no timing hack needed). useEffect(() => { const initAuth = async () => { try { await refreshUser(); // Fetch CSRF only after auth is confirmed const { isAuthenticated } = useAuthStore.getState(); if (isAuthenticated) { csrfService.refreshToken().catch((error) => { const msg = error instanceof Error ? error.message : String(error); if (!msg.includes('HTML page instead of JSON')) { logger.warn('Failed to fetch CSRF token on app init', { message: msg }); } }); } } catch (error) { logger.error('[App] Auth initialization failed', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); } finally { setIsAuthReady(true); } }; initAuth(); }, [refreshUser]); // Apply theme on load useEffect(() => { if (!theme || theme === 'system') { const root = document.documentElement; if ( !root.classList.contains('dark') && !root.classList.contains('light') ) { setTheme('dark'); } else { setTheme(theme); } } else { setTheme(theme); } // Sync language with i18n on load if (typeof window !== 'undefined' && window.i18n) { const currentLang = window.i18n.language || language; if (currentLang !== language) { window.i18n.changeLanguage(language); } else if (language !== currentLang) { setLanguage(currentLang as 'en' | 'fr' | 'es'); } } }, [setTheme, theme, language, setLanguage]); // Écouter les changements de préférence système pour le mode 'system' useEffect(() => { if (theme !== 'system') return; const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleChange = (e: MediaQueryListEvent) => { const root = document.documentElement; if (e.matches) { root.classList.add('dark'); } else { root.classList.remove('dark'); } }; // Écouter les changements if (mediaQuery.addEventListener) { mediaQuery.addEventListener('change', handleChange); } else { // Fallback pour les navigateurs plus anciens mediaQuery.addListener(handleChange); } return () => { if (mediaQuery.removeEventListener) { mediaQuery.removeEventListener('change', handleChange); } else { mediaQuery.removeListener(handleChange); } }; }, [theme]); // S5.1: Branded loading screen (Spotify-like splash) if (!isAuthReady) { return (