fix: Mobile login redirects to homepage without showing Auth0 login page (#188) #193
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useTransition, useCallback, lazy } from 'react';
|
import React, { useState, useEffect, useTransition, useCallback, lazy } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
import { useIsAuthInitialized } from './core/auth/auth-gate';
|
import { useIsAuthInitialized } from './core/auth/auth-gate';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
@@ -310,11 +310,11 @@ const EditVehicleScreen: React.FC<EditVehicleScreenProps> = ({ vehicle, onBack,
|
|||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { isLoading, isAuthenticated, user } = useAuth0();
|
const { isLoading, isAuthenticated, user, error: authError } = useAuth0();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const isAuthGateReady = useIsAuthInitialized();
|
const isAuthGateReady = useIsAuthInitialized();
|
||||||
const [_isPending, startTransition] = useTransition();
|
const [_isPending, startTransition] = useTransition();
|
||||||
console.log('[DEBUG App] Render check - isLoading:', isLoading, 'isAuthenticated:', isAuthenticated, 'isAuthGateReady:', isAuthGateReady);
|
|
||||||
|
|
||||||
// Initialize data synchronization
|
// Initialize data synchronization
|
||||||
const { prefetchForNavigation } = useDataSync();
|
const { prefetchForNavigation } = useDataSync();
|
||||||
@@ -365,17 +365,24 @@ function App() {
|
|||||||
const [showAddVehicle, setShowAddVehicle] = useState(false);
|
const [showAddVehicle, setShowAddVehicle] = useState(false);
|
||||||
|
|
||||||
// Sync browser URL to Zustand screen state on mount (enables direct URL navigation on mobile)
|
// Sync browser URL to Zustand screen state on mount (enables direct URL navigation on mobile)
|
||||||
|
// Skip on auth routes -- their query params must survive until Auth0 SDK processes them
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const screen = routeToScreen[window.location.pathname];
|
const path = window.location.pathname;
|
||||||
|
if (path === '/callback' || path === '/signup' || path === '/verify-email') return;
|
||||||
|
const screen = routeToScreen[path];
|
||||||
if (screen && screen !== activeScreen) {
|
if (screen && screen !== activeScreen) {
|
||||||
navigateToScreen(screen, { source: 'url-sync' });
|
navigateToScreen(screen, { source: 'url-sync' });
|
||||||
}
|
}
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- intentionally runs once on mount
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- intentionally runs once on mount
|
||||||
|
|
||||||
// Sync Zustand screen changes back to browser URL (enables bookmarks and URL sharing)
|
// Sync Zustand screen changes back to browser URL (enables bookmarks and URL sharing)
|
||||||
|
// Skip on auth routes -- replaceState would strip ?code= and &state= params that
|
||||||
|
// Auth0 SDK needs for handleRedirectCallback (child effects fire before parent effects)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
if (path === '/callback' || path === '/signup' || path === '/verify-email') return;
|
||||||
const targetPath = screenToRoute[activeScreen];
|
const targetPath = screenToRoute[activeScreen];
|
||||||
if (targetPath && window.location.pathname !== targetPath) {
|
if (targetPath && path !== targetPath) {
|
||||||
window.history.replaceState(null, '', targetPath);
|
window.history.replaceState(null, '', targetPath);
|
||||||
}
|
}
|
||||||
}, [activeScreen]);
|
}, [activeScreen]);
|
||||||
@@ -486,7 +493,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [navigateToScreen, navigateToVehicleSubScreen]);
|
}, [navigateToScreen, navigateToVehicleSubScreen]);
|
||||||
|
|
||||||
console.log('MotoVaultPro status:', { isLoading, isAuthenticated, mobileMode, activeScreen, vehicleSubScreen, userAgent: navigator.userAgent });
|
|
||||||
|
|
||||||
const isGarageRoute = location.pathname === '/garage' || location.pathname.startsWith('/garage/');
|
const isGarageRoute = location.pathname === '/garage' || location.pathname.startsWith('/garage/');
|
||||||
const isCallbackRoute = location.pathname === '/callback';
|
const isCallbackRoute = location.pathname === '/callback';
|
||||||
@@ -496,6 +502,16 @@ function App() {
|
|||||||
const isAuthRoute = isSignupRoute || isVerifyEmailRoute || isOnboardingRoute;
|
const isAuthRoute = isSignupRoute || isVerifyEmailRoute || isOnboardingRoute;
|
||||||
const shouldShowHomePage = !isGarageRoute && !isCallbackRoute && !isAuthRoute;
|
const shouldShowHomePage = !isGarageRoute && !isCallbackRoute && !isAuthRoute;
|
||||||
|
|
||||||
|
const [callbackTimedOut, setCallbackTimedOut] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCallbackRoute && !isAuthenticated && !isLoading) {
|
||||||
|
const timer = setTimeout(() => setCallbackTimedOut(true), 10000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
setCallbackTimedOut(false);
|
||||||
|
return undefined;
|
||||||
|
}, [isCallbackRoute, isAuthenticated, isLoading]);
|
||||||
|
|
||||||
// Enhanced navigation handlers for mobile
|
// Enhanced navigation handlers for mobile
|
||||||
const handleVehicleSelect = useCallback((vehicle: Vehicle) => {
|
const handleVehicleSelect = useCallback((vehicle: Vehicle) => {
|
||||||
setSelectedVehicle(vehicle);
|
setSelectedVehicle(vehicle);
|
||||||
@@ -557,8 +573,23 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback route requires authentication - handled by CallbackPage component
|
if (isCallbackRoute) {
|
||||||
if (isCallbackRoute && isAuthenticated) {
|
if (authError) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen gap-4 px-4">
|
||||||
|
<div className="text-lg text-red-600 text-center">Login failed: {authError.message}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Return to Homepage
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<React.Suspense fallback={
|
<React.Suspense fallback={
|
||||||
@@ -572,6 +603,28 @@ function App() {
|
|||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (callbackTimedOut) {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
if (mobileMode) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<Layout mobileMode={true}>
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-slate-500">Processing login...</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-lg">Processing login...</div>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldShowHomePage) {
|
if (shouldShowHomePage) {
|
||||||
return (
|
return (
|
||||||
@@ -637,7 +690,6 @@ function App() {
|
|||||||
// Wait for auth gate to be ready before rendering protected routes
|
// Wait for auth gate to be ready before rendering protected routes
|
||||||
// This prevents a race condition where the page renders before the auth token is ready
|
// This prevents a race condition where the page renders before the auth token is ready
|
||||||
if (!isAuthGateReady) {
|
if (!isAuthGateReady) {
|
||||||
console.log('[DEBUG App] Auth gate not ready yet, showing loading state');
|
|
||||||
if (mobileMode) {
|
if (mobileMode) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { apiClient, setAuthReady } from '../api/client';
|
import { apiClient, setAuthReady } from '../api/client';
|
||||||
import { createIndexedDBAdapter } from '../utils/indexeddb-storage';
|
import { createIndexedDBAdapter } from '../utils/indexeddb-storage';
|
||||||
import { setAuthInitialized } from './auth-gate';
|
import { setAuthInitialized } from './auth-gate';
|
||||||
|
import logger from '../../utils/logger';
|
||||||
|
|
||||||
interface Auth0ProviderProps {
|
interface Auth0ProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -20,12 +21,8 @@ export const Auth0Provider: React.FC<Auth0ProviderProps> = ({ children }) => {
|
|||||||
const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
|
const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
|
||||||
const audience = import.meta.env.VITE_AUTH0_AUDIENCE;
|
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 }) => {
|
const onRedirectCallback = (appState?: { returnTo?: string }) => {
|
||||||
console.log('[Auth0Provider] Redirect callback triggered', { appState, returnTo: appState?.returnTo });
|
logger.debug('Auth0 redirect callback triggered', { returnTo: appState?.returnTo });
|
||||||
// Route to callback page which will check user status and redirect appropriately
|
// Route to callback page which will check user status and redirect appropriately
|
||||||
// Pass the intended destination as state for after status check
|
// Pass the intended destination as state for after status check
|
||||||
navigate('/callback', {
|
navigate('/callback', {
|
||||||
@@ -57,17 +54,9 @@ export const Auth0Provider: React.FC<Auth0ProviderProps> = ({ children }) => {
|
|||||||
|
|
||||||
// Component to inject token into API client with mobile support
|
// Component to inject token into API client with mobile support
|
||||||
const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const { getAccessTokenSilently, isAuthenticated, isLoading, user } = useAuth0();
|
const { getAccessTokenSilently, isAuthenticated, isLoading, logout } = useAuth0();
|
||||||
const [retryCount, setRetryCount] = React.useState(0);
|
const [retryCount, setRetryCount] = React.useState(0);
|
||||||
|
const validatingRef = React.useRef(false);
|
||||||
// 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
|
// Helper function to get token with enhanced retry logic for mobile devices
|
||||||
const getTokenWithRetry = async (maxRetries = 5, delayMs = 300): Promise<any> => {
|
const getTokenWithRetry = async (maxRetries = 5, delayMs = 300): Promise<any> => {
|
||||||
@@ -93,77 +82,49 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const token = await getAccessTokenSilently(tokenOptions);
|
const token = await getAccessTokenSilently(tokenOptions);
|
||||||
console.log(`[Mobile Auth] Token acquired successfully on attempt ${attempt + 1}`, {
|
logger.debug(`Token acquired on attempt ${attempt + 1}`);
|
||||||
cacheMode: tokenOptions.cacheMode,
|
|
||||||
timeout: tokenOptions.timeoutInSeconds
|
|
||||||
});
|
|
||||||
return token;
|
return token;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.warn(`[Mobile Auth] Attempt ${attempt + 1}/${maxRetries} failed:`, {
|
logger.warn(`Token attempt ${attempt + 1}/${maxRetries} failed`, {
|
||||||
error: error.message || error,
|
error: error.message || String(error),
|
||||||
cacheMode: attempt <= 2 ? 'on' : 'off'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mobile-specific: longer delays and more attempts
|
// Mobile-specific: longer delays and more attempts
|
||||||
if (attempt < maxRetries - 1) {
|
if (attempt < maxRetries - 1) {
|
||||||
const delay = delayMs * Math.pow(1.5, attempt); // Gentler exponential backoff
|
const delay = delayMs * Math.pow(1.5, attempt);
|
||||||
console.log(`[Mobile Auth] Waiting ${Math.round(delay)}ms before retry...`);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.error('[Mobile Auth] All token acquisition attempts failed - authentication may be broken');
|
logger.error('All token acquisition attempts failed');
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Force authentication check for devices when user seems logged in but isAuthenticated is false
|
// Prevent stale session state when cached token is no longer valid
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
if (!isAuthenticated || isLoading || validatingRef.current) return;
|
||||||
|
if (window.location.pathname === '/callback') return;
|
||||||
|
|
||||||
// Debug current state
|
const validateToken = async () => {
|
||||||
console.log('[Auth Debug] State check:', {
|
validatingRef.current = true;
|
||||||
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 {
|
||||||
// Try multiple approaches to get token
|
await getAccessTokenSilently({ cacheMode: 'off', timeoutInSeconds: 10 });
|
||||||
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) {
|
} catch (error: any) {
|
||||||
console.log('[Auth Debug] Force auth failed:', error.message);
|
const errorType = error?.error || error?.message || '';
|
||||||
|
if (errorType.includes('login_required') || errorType.includes('consent_required') ||
|
||||||
|
errorType.includes('invalid_grant')) {
|
||||||
|
logger.warn('Stale token detected, clearing auth state');
|
||||||
|
const { indexedDBStorage } = await import('../utils/indexeddb-storage');
|
||||||
|
await indexedDBStorage.clearAll();
|
||||||
|
logout({ openUrl: false });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
validatingRef.current = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
forceAuthCheck();
|
validateToken();
|
||||||
}
|
}, [isAuthenticated, isLoading, getAccessTokenSilently, logout]);
|
||||||
}, [isLoading, isAuthenticated, getAccessTokenSilently]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let interceptorId: number | undefined;
|
let interceptorId: number | undefined;
|
||||||
@@ -175,34 +136,30 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
|||||||
try {
|
try {
|
||||||
const { indexedDBStorage } = await import('../utils/indexeddb-storage');
|
const { indexedDBStorage } = await import('../utils/indexeddb-storage');
|
||||||
await indexedDBStorage.waitForReady();
|
await indexedDBStorage.waitForReady();
|
||||||
console.log('[Auth] IndexedDB storage is ready');
|
logger.debug('IndexedDB storage is ready');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Auth] IndexedDB not ready, proceeding anyway:', error);
|
logger.warn('IndexedDB not ready, proceeding anyway', { error: String(error) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimal delay only for mobile devices (desktop needs no delay since IndexedDB is already ready)
|
// Minimal delay only for mobile devices (desktop needs no delay since IndexedDB is already ready)
|
||||||
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
// Small delay for mobile browsers to settle after IndexedDB init
|
|
||||||
console.log('[Mobile Auth] Initializing token cache (mobile: true, delay: 50ms)');
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
} else {
|
|
||||||
console.log('[Auth] Initializing token cache (desktop, no delay)');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = await getTokenWithRetry();
|
const token = await getTokenWithRetry();
|
||||||
if (token) {
|
if (token) {
|
||||||
console.log('[Mobile Auth] Token pre-warming successful');
|
logger.debug('Token pre-warming successful');
|
||||||
setRetryCount(0);
|
setRetryCount(0);
|
||||||
setAuthReady(true);
|
setAuthReady(true);
|
||||||
setAuthInitialized(true); // Signal that auth is fully ready
|
setAuthInitialized(true); // Signal that auth is fully ready
|
||||||
} else {
|
} else {
|
||||||
console.error('[Mobile Auth] Failed to acquire token after retries - will retry on API calls');
|
logger.error('Failed to acquire token after retries');
|
||||||
setRetryCount(prev => prev + 1);
|
setRetryCount(prev => prev + 1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Mobile Auth] Token initialization failed:', error);
|
logger.error('Token initialization failed', { error: String(error) });
|
||||||
setRetryCount(prev => prev + 1);
|
setRetryCount(prev => prev + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -221,11 +178,11 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
|||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
setAuthReady(true);
|
setAuthReady(true);
|
||||||
} else {
|
} else {
|
||||||
console.error('No token available for request to:', config.url);
|
logger.error('No token available for request', { url: config.url });
|
||||||
// Allow request to proceed - backend will return 401 if needed
|
// Allow request to proceed - backend will return 401 if needed
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to get access token for request:', error.message || error);
|
logger.error('Failed to get access token for request', { error: error.message || String(error) });
|
||||||
// Allow request to proceed - backend will return 401 if needed
|
// Allow request to proceed - backend will return 401 if needed
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* @ai-context Replaces localStorage with IndexedDB for mobile browser compatibility
|
* @ai-context Replaces localStorage with IndexedDB for mobile browser compatibility
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import logger from '../../utils/logger';
|
||||||
|
|
||||||
interface StorageAdapter {
|
interface StorageAdapter {
|
||||||
getItem(key: string): string | null;
|
getItem(key: string): string | null;
|
||||||
setItem(key: string, value: string): void;
|
setItem(key: string, value: string): void;
|
||||||
@@ -36,9 +38,9 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
|
|||||||
this.db = await this.openDatabase();
|
this.db = await this.openDatabase();
|
||||||
await this.loadCacheFromDB();
|
await this.loadCacheFromDB();
|
||||||
this.isReady = true;
|
this.isReady = true;
|
||||||
console.log('[IndexedDB] Storage initialized successfully');
|
logger.debug('IndexedDB storage initialized successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[IndexedDB] Initialization failed, using memory only:', error);
|
logger.error('IndexedDB initialization failed, using memory only', { error: String(error) });
|
||||||
this.isReady = false;
|
this.isReady = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +50,7 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
|
|||||||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
console.error(`IndexedDB open failed: ${request.error?.message}`);
|
logger.error(`IndexedDB open failed: ${request.error?.message}`);
|
||||||
resolve(null as any); // Fallback to memory-only mode
|
resolve(null as any); // Fallback to memory-only mode
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,25 +73,27 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const transaction = this.db!.transaction([this.storeName], 'readonly');
|
const transaction = this.db!.transaction([this.storeName], 'readonly');
|
||||||
const store = transaction.objectStore(this.storeName);
|
const store = transaction.objectStore(this.storeName);
|
||||||
const request = store.getAll();
|
const request = store.openCursor();
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const results = request.result;
|
|
||||||
this.memoryCache.clear();
|
this.memoryCache.clear();
|
||||||
|
|
||||||
for (const item of results) {
|
request.onsuccess = () => {
|
||||||
if (item.key && typeof item.value === 'string') {
|
const cursor = request.result;
|
||||||
this.memoryCache.set(item.key, item.value);
|
if (cursor) {
|
||||||
|
const key = cursor.key as string;
|
||||||
|
const value = cursor.value;
|
||||||
|
if (typeof key === 'string' && typeof value === 'string') {
|
||||||
|
this.memoryCache.set(key, value);
|
||||||
}
|
}
|
||||||
}
|
cursor.continue();
|
||||||
|
} else {
|
||||||
console.log(`[IndexedDB] Loaded ${this.memoryCache.size} items into cache`);
|
logger.debug(`IndexedDB loaded ${this.memoryCache.size} items into cache`);
|
||||||
resolve();
|
resolve();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
console.warn('[IndexedDB] Failed to load cache from DB:', request.error);
|
logger.warn('IndexedDB failed to load cache from DB', { error: String(request.error) });
|
||||||
resolve(); // Don't fail initialization
|
resolve();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -105,14 +109,14 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
|
|||||||
const request = store.delete(key);
|
const request = store.delete(key);
|
||||||
request.onsuccess = () => resolve();
|
request.onsuccess = () => resolve();
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
console.warn(`[IndexedDB] Failed to delete ${key}:`, request.error);
|
logger.warn(`IndexedDB failed to delete ${key}`, { error: String(request.error) });
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const request = store.put(value, key);
|
const request = store.put(value, key);
|
||||||
request.onsuccess = () => resolve();
|
request.onsuccess = () => resolve();
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
console.warn(`[IndexedDB] Failed to persist ${key}:`, request.error);
|
logger.warn(`IndexedDB failed to persist ${key}`, { error: String(request.error) });
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -130,7 +134,7 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
|
|||||||
// Async persist to IndexedDB (non-blocking)
|
// Async persist to IndexedDB (non-blocking)
|
||||||
if (this.isReady) {
|
if (this.isReady) {
|
||||||
this.persistToDB(key, value).catch(error => {
|
this.persistToDB(key, value).catch(error => {
|
||||||
console.warn(`[IndexedDB] Background persist failed for ${key}:`, error);
|
logger.warn(`IndexedDB background persist failed for ${key}`, { error: String(error) });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,7 +145,7 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
|
|||||||
// Async remove from IndexedDB (non-blocking)
|
// Async remove from IndexedDB (non-blocking)
|
||||||
if (this.isReady) {
|
if (this.isReady) {
|
||||||
this.persistToDB(key, null).catch(error => {
|
this.persistToDB(key, null).catch(error => {
|
||||||
console.warn(`[IndexedDB] Background removal failed for ${key}:`, error);
|
logger.warn(`IndexedDB background removal failed for ${key}`, { error: String(error) });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,6 +161,30 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clearAll(): Promise<void> {
|
||||||
|
await this.initPromise;
|
||||||
|
if (!this.db) {
|
||||||
|
this.memoryCache.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tx = this.db.transaction(this.storeName, 'readwrite');
|
||||||
|
tx.objectStore(this.storeName).clear();
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
tx.oncomplete = () => {
|
||||||
|
this.memoryCache.clear();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
tx.onerror = () => {
|
||||||
|
this.memoryCache.clear();
|
||||||
|
reject(tx.error);
|
||||||
|
};
|
||||||
|
tx.onabort = () => {
|
||||||
|
this.memoryCache.clear();
|
||||||
|
reject(new Error('Transaction aborted'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
key(index: number): string | null {
|
key(index: number): string | null {
|
||||||
const keys = Array.from(this.memoryCache.keys());
|
const keys = Array.from(this.memoryCache.keys());
|
||||||
return keys[index] || null;
|
return keys[index] || null;
|
||||||
@@ -167,6 +195,11 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auth0 Cache interface implementation
|
// Auth0 Cache interface implementation
|
||||||
|
// allKeys() eliminates Auth0 SDK's CacheKeyManifest fallback (auth0-spa-js line 2319)
|
||||||
|
allKeys(): string[] {
|
||||||
|
return Array.from(this.memoryCache.keys());
|
||||||
|
}
|
||||||
|
|
||||||
async get(key: string): Promise<any> {
|
async get(key: string): Promise<any> {
|
||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
const value = this.getItem(key);
|
const value = this.getItem(key);
|
||||||
@@ -175,12 +208,21 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
|
|||||||
|
|
||||||
async set(key: string, value: any): Promise<void> {
|
async set(key: string, value: any): Promise<void> {
|
||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
this.setItem(key, JSON.stringify(value));
|
const stringValue = JSON.stringify(value);
|
||||||
|
this.memoryCache.set(key, stringValue);
|
||||||
|
// Fire-and-forget: persist to IndexedDB for page reload survival
|
||||||
|
this.persistToDB(key, stringValue).catch(error => {
|
||||||
|
logger.warn(`IndexedDB background persist failed for ${key}`, { error: String(error) });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(key: string): Promise<void> {
|
async remove(key: string): Promise<void> {
|
||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
this.removeItem(key);
|
this.memoryCache.delete(key);
|
||||||
|
// Fire-and-forget: remove from IndexedDB
|
||||||
|
this.persistToDB(key, null).catch(error => {
|
||||||
|
logger.warn(`IndexedDB background removal failed for ${key}`, { error: String(error) });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional methods for enhanced functionality
|
// Additional methods for enhanced functionality
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import { FeaturesGrid } from './HomePage/FeaturesGrid';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const { loginWithRedirect, isAuthenticated } = useAuth0();
|
const { loginWithRedirect, isAuthenticated, logout } = useAuth0();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
const [sessionCleared, setSessionCleared] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,6 +42,22 @@ export const HomePage = () => {
|
|||||||
navigate('/signup');
|
navigate('/signup');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearSession = async () => {
|
||||||
|
try {
|
||||||
|
const { indexedDBStorage } = await import('../core/utils/indexeddb-storage');
|
||||||
|
await indexedDBStorage.clearAll();
|
||||||
|
Object.keys(localStorage).forEach(key => {
|
||||||
|
if (key.startsWith('@@auth0')) localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
logout({ openUrl: false });
|
||||||
|
setSessionCleared(true);
|
||||||
|
setTimeout(() => setSessionCleared(false), 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[HomePage] Failed to clear session:', error);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-nero text-avus">
|
<div className="min-h-screen bg-nero text-avus">
|
||||||
{/* Navigation Bar */}
|
{/* Navigation Bar */}
|
||||||
@@ -84,6 +101,12 @@ export const HomePage = () => {
|
|||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleClearSession}
|
||||||
|
className="text-white/40 hover:text-white/70 text-xs transition-colors min-h-[44px] min-w-[44px] flex items-center"
|
||||||
|
>
|
||||||
|
{sessionCleared ? 'Session cleared' : 'Trouble logging in?'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
@@ -149,6 +172,12 @@ export const HomePage = () => {
|
|||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleClearSession}
|
||||||
|
className="w-full text-white/40 hover:text-white/70 text-xs py-2 min-h-[44px] transition-colors"
|
||||||
|
>
|
||||||
|
{sessionCleared ? 'Session cleared' : 'Trouble logging in?'}
|
||||||
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user