From da59168d7b0bb81fe1377915aa6bcdc163e33cf1 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:20:34 -0600 Subject: [PATCH] fix: IndexedDB cache broken on page reload - root cause of mobile login failure (refs #190) 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 --- frontend/src/App.tsx | 13 ++++++++ frontend/src/core/utils/indexeddb-storage.ts | 31 ++++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3a161dd..64be162 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -496,6 +496,16 @@ function App() { const isAuthRoute = isSignupRoute || isVerifyEmailRoute || isOnboardingRoute; 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 const handleVehicleSelect = useCallback((vehicle: Vehicle) => { setSelectedVehicle(vehicle); @@ -572,6 +582,9 @@ function App() { ); } + if (callbackTimedOut) { + return ; + } if (mobileMode) { return ( diff --git a/frontend/src/core/utils/indexeddb-storage.ts b/frontend/src/core/utils/indexeddb-storage.ts index 24f0d60..2dc10d0 100644 --- a/frontend/src/core/utils/indexeddb-storage.ts +++ b/frontend/src/core/utils/indexeddb-storage.ts @@ -71,25 +71,27 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { return new Promise((resolve) => { const transaction = this.db!.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); - const request = store.getAll(); + const request = store.openCursor(); + this.memoryCache.clear(); request.onsuccess = () => { - const results = request.result; - this.memoryCache.clear(); - - for (const item of results) { - if (item.key && typeof item.value === 'string') { - this.memoryCache.set(item.key, item.value); + const cursor = request.result; + 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`); + resolve(); } - - console.log(`[IndexedDB] Loaded ${this.memoryCache.size} items into cache`); - resolve(); }; request.onerror = () => { 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 { 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 { await this.initPromise; - this.removeItem(key); + this.memoryCache.delete(key); + await this.persistToDB(key, null); } // Additional methods for enhanced functionality