/** * @ai-summary Auth0 provider wrapper with API token injection */ import React from 'react'; import { Auth0Provider as BaseAuth0Provider, useAuth0 } from '@auth0/auth0-react'; import { useNavigate } from 'react-router-dom'; import { apiClient, setAuthReady } from '../api/client'; interface Auth0ProviderProps { children: React.ReactNode; } export const Auth0Provider: React.FC = ({ children }) => { const navigate = useNavigate(); const domain = import.meta.env.VITE_AUTH0_DOMAIN; const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID; const audience = import.meta.env.VITE_AUTH0_AUDIENCE; // Basic component loading debug console.log('[Auth0Provider] Component loaded', { domain, clientId, audience }); const onRedirectCallback = (appState?: { returnTo?: string }) => { console.log('[Auth0Provider] Redirect callback triggered', { appState, returnTo: appState?.returnTo }); navigate(appState?.returnTo || '/dashboard'); }; return ( {children} ); }; // Component to inject token into API client with mobile support const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { getAccessTokenSilently, isAuthenticated, isLoading, user } = useAuth0(); const [retryCount, setRetryCount] = React.useState(0); // Basic component loading debug console.log('[TokenInjector] Component loaded'); // Debug mobile authentication state React.useEffect(() => { const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); console.log(`[Auth Debug] Mobile: ${isMobile}, Loading: ${isLoading}, Authenticated: ${isAuthenticated}, User: ${user ? 'present' : 'null'}`); }, [isAuthenticated, isLoading, user]); // Helper function to get token with enhanced retry logic for mobile devices const getTokenWithRetry = async (maxRetries = 5, delayMs = 300): Promise => { for (let attempt = 0; attempt < maxRetries; attempt++) { try { // Enhanced progressive strategy for mobile compatibility let tokenOptions: any; if (attempt === 0) { // First attempt: try cache with shorter timeout tokenOptions = { timeoutInSeconds: 10, cacheMode: 'on' as const }; } else if (attempt === 1) { // Second attempt: cache with longer timeout tokenOptions = { timeoutInSeconds: 20, cacheMode: 'on' as const }; } else if (attempt === 2) { // Third attempt: force refresh with reasonable timeout tokenOptions = { timeoutInSeconds: 15, cacheMode: 'off' as const }; } else if (attempt === 3) { // Fourth attempt: force refresh with longer timeout tokenOptions = { timeoutInSeconds: 30, cacheMode: 'off' as const }; } else { // Final attempt: default behavior with maximum timeout tokenOptions = { timeoutInSeconds: 45 }; } const token = await getAccessTokenSilently(tokenOptions); console.log(`[Mobile Auth] Token acquired successfully on attempt ${attempt + 1}`, { cacheMode: tokenOptions.cacheMode, timeout: tokenOptions.timeoutInSeconds }); return token; } catch (error: any) { console.warn(`[Mobile Auth] Attempt ${attempt + 1}/${maxRetries} failed:`, { error: error.message || error, cacheMode: attempt <= 2 ? 'on' : 'off' }); // Mobile-specific: longer delays and more attempts if (attempt < maxRetries - 1) { const delay = delayMs * Math.pow(1.5, attempt); // Gentler exponential backoff console.log(`[Mobile Auth] Waiting ${Math.round(delay)}ms before retry...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } console.error('[Mobile Auth] All token acquisition attempts failed - authentication may be broken'); return null; }; // Force authentication check for devices when user seems logged in but isAuthenticated is false React.useEffect(() => { const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); // Debug current state console.log('[Auth Debug] State check:', { isMobile, isLoading, isAuthenticated, pathname: window.location.pathname, userAgent: navigator.userAgent.substring(0, 50) + '...' }); // Trigger for mobile devices OR any device on protected route without authentication if (!isLoading && !isAuthenticated && window.location.pathname !== '/') { console.log('[Auth Debug] User on protected route but not authenticated, forcing token check...'); // Aggressive token check const forceAuthCheck = async () => { try { // Try multiple approaches to get token const token = await getAccessTokenSilently({ cacheMode: 'off' as const, timeoutInSeconds: 10 }); console.log('[Auth Debug] Force auth successful, token acquired'); // Manually add to API client since isAuthenticated might still be false if (token) { console.log('[Auth Debug] Manually adding token to API client'); // Force add the token to subsequent requests apiClient.interceptors.request.use((config) => { if (!config.headers.Authorization) { config.headers.Authorization = `Bearer ${token}`; console.log('[Auth Debug] Token manually added to request'); } return config; }); setAuthReady(true); } } catch (error: any) { console.log('[Auth Debug] Force auth failed:', error.message); } }; forceAuthCheck(); } }, [isLoading, isAuthenticated, getAccessTokenSilently]); React.useEffect(() => { let interceptorId: number | undefined; if (isAuthenticated) { // Enhanced pre-warm token cache for mobile devices const initializeToken = async () => { // Give Auth0 more time to fully initialize on mobile devices const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const initDelay = isMobile ? 500 : 100; // Longer delay for mobile console.log(`[Mobile Auth] Initializing token cache (mobile: ${isMobile}, delay: ${initDelay}ms)`); await new Promise(resolve => setTimeout(resolve, initDelay)); try { const token = await getTokenWithRetry(); if (token) { console.log('[Mobile Auth] Token pre-warming successful'); setRetryCount(0); setAuthReady(true); } else { console.error('[Mobile Auth] Failed to acquire token after retries - will retry on API calls'); setRetryCount(prev => prev + 1); } } catch (error) { console.error('[Mobile Auth] Token initialization failed:', error); setRetryCount(prev => prev + 1); } }; initializeToken(); // Add token to all API requests with enhanced error handling interceptorId = apiClient.interceptors.request.use(async (config) => { try { const token = await getTokenWithRetry(); if (token) { config.headers.Authorization = `Bearer ${token}`; setAuthReady(true); } else { console.error('No token available for request to:', config.url); // Allow request to proceed - backend will return 401 if needed } } catch (error: any) { console.error('Failed to get access token for request:', error.message || error); // Allow request to proceed - backend will return 401 if needed } return config; }); } else { setRetryCount(0); setAuthReady(false); } // Cleanup function to remove interceptor return () => { if (interceptorId !== undefined) { apiClient.interceptors.request.eject(interceptorId); } }; }, [isAuthenticated, getAccessTokenSilently, retryCount]); return <>{children}; };