fix: IndexedDB cache broken on page reload - root cause of mobile login failure (refs #190)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m25s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 51s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

loadCacheFromDB used store.getAll() which returns raw values, not
key-value pairs. The item.key check always failed, so memoryCache
was empty after every page reload. Auth0 SDK state stored before
redirect was lost on mobile Safari (no bfcache).

Also fixed set()/remove() to await IDB persistence so Auth0 state
is fully written before loginWithRedirect() navigates away.

Added 10s timeout on callback loading state as safety net.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-14 22:20:34 -06:00
parent 38debaad5d
commit da59168d7b
2 changed files with 31 additions and 13 deletions

View File

@@ -496,6 +496,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);
@@ -572,6 +582,9 @@ function App() {
</ThemeProvider> </ThemeProvider>
); );
} }
if (callbackTimedOut) {
return <Navigate to="/" replace />;
}
if (mobileMode) { if (mobileMode) {
return ( return (
<ThemeProvider> <ThemeProvider>

View File

@@ -71,25 +71,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();
this.memoryCache.clear();
request.onsuccess = () => { request.onsuccess = () => {
const results = request.result; const cursor = request.result;
this.memoryCache.clear(); if (cursor) {
const key = cursor.key as string;
for (const item of results) { const value = cursor.value;
if (item.key && typeof item.value === 'string') { if (typeof key === 'string' && typeof value === 'string') {
this.memoryCache.set(item.key, item.value); this.memoryCache.set(key, value);
} }
cursor.continue();
} else {
console.log(`[IndexedDB] Loaded ${this.memoryCache.size} items into cache`);
resolve();
} }
console.log(`[IndexedDB] Loaded ${this.memoryCache.size} items into cache`);
resolve();
}; };
request.onerror = () => { request.onerror = () => {
console.warn('[IndexedDB] Failed to load cache from DB:', request.error); console.warn('[IndexedDB] Failed to load cache from DB:', request.error);
resolve(); // Don't fail initialization resolve();
}; };
}); });
} }
@@ -199,12 +201,15 @@ 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);
await this.persistToDB(key, stringValue);
} }
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);
await this.persistToDB(key, null);
} }
// Additional methods for enhanced functionality // Additional methods for enhanced functionality