fix: Mobile login redirects to homepage without showing Auth0 login page (#188) #193

Merged
egullickson merged 9 commits from issue-188-fix-mobile-login-redirect into main 2026-02-15 15:36:38 +00:00
4 changed files with 199 additions and 119 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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

View File

@@ -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>