From 103ca2c5e4246dd2ae8edaaacb5c25a9c9ffb1b6 Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Wed, 29 Jul 2026 03:00:04 -0700 Subject: [PATCH] Fix mobile white screen when localStorage throws on access (#54) (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Closes #54 - Add `getLocalStorageSafe()` helper in `tokenStorage.js` to handle browsers that expose `localStorage` but throw `SecurityError` on access - Wrap all JWT token get/set/remove calls in try/catch so auth bootstrap no longer crashes React on mobile ## Problem On mobile browsers, hesychia.ai flashes the UI briefly after login then goes white. Auth startup reads tokens from `localStorage` without guarding against storage access errors (common in private mode or blocked storage). ## Test plan - [ ] Open hesychia.ai on mobile Safari/Chrome (normal mode) and sign in — dashboard should render - [ ] Repeat in private/incognito mode — app should not white-screen (may require re-login each session if storage blocked) - [ ] Verify existing web login/logout still works on desktop - [ ] Run `npm test -- tokenStorage` in `llm-fe`Reviewed-on: https://git.aimloperations.com/ai_ml_operations/chat_web_app/pulls/55 --- llm-fe/src/llm-fe/auth/tokenStorage.js | 54 ++++++++++++++++++++------ 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/llm-fe/src/llm-fe/auth/tokenStorage.js b/llm-fe/src/llm-fe/auth/tokenStorage.js index a4b1fd9..170279e 100644 --- a/llm-fe/src/llm-fe/auth/tokenStorage.js +++ b/llm-fe/src/llm-fe/auth/tokenStorage.js @@ -9,6 +9,21 @@ export const ACCESS_TOKEN_KEY = "access_token"; export const REFRESH_TOKEN_KEY = "refresh_token"; +/** + * Browsers can expose localStorage but throw on access (privacy mode / blocked storage). + * @returns {Storage|null} + */ +function getLocalStorageSafe() { + try { + if (typeof window === "undefined" || !window.localStorage) { + return null; + } + return window.localStorage; + } catch { + return null; + } +} + /** * @returns {{ get?: Function, set?: Function, remove?: Function } | undefined} */ @@ -24,10 +39,15 @@ function getPreferencesPlugin() { * @returns {string|null} */ export function getToken(key) { - if (typeof localStorage === "undefined") { + const storage = getLocalStorageSafe(); + if (!storage) { + return null; + } + try { + return storage.getItem(key); + } catch { return null; } - return localStorage.getItem(key); } /** @@ -51,10 +71,15 @@ export function getRefreshToken() { * @returns {Promise} */ export async function setTokens(access, refresh) { - if (typeof localStorage !== "undefined") { - localStorage.setItem(ACCESS_TOKEN_KEY, access); - if (refresh != null) { - localStorage.setItem(REFRESH_TOKEN_KEY, refresh); + const storage = getLocalStorageSafe(); + if (storage) { + try { + storage.setItem(ACCESS_TOKEN_KEY, access); + if (refresh != null) { + storage.setItem(REFRESH_TOKEN_KEY, refresh); + } + } catch { + /* localStorage unavailable */ } } @@ -75,9 +100,14 @@ export async function setTokens(access, refresh) { * @returns {Promise} */ export async function clearTokens() { - if (typeof localStorage !== "undefined") { - localStorage.removeItem(ACCESS_TOKEN_KEY); - localStorage.removeItem(REFRESH_TOKEN_KEY); + const storage = getLocalStorageSafe(); + if (storage) { + try { + storage.removeItem(ACCESS_TOKEN_KEY); + storage.removeItem(REFRESH_TOKEN_KEY); + } catch { + /* localStorage unavailable */ + } } const Preferences = getPreferencesPlugin(); @@ -110,7 +140,8 @@ export async function hydrateTokensFromNativeStorage() { try { const result = await Preferences.get({ key: ACCESS_TOKEN_KEY }); if (result?.value) { - localStorage.setItem(ACCESS_TOKEN_KEY, result.value); + const storage = getLocalStorageSafe(); + storage?.setItem(ACCESS_TOKEN_KEY, result.value); access = result.value; } } catch { @@ -122,7 +153,8 @@ export async function hydrateTokensFromNativeStorage() { try { const result = await Preferences.get({ key: REFRESH_TOKEN_KEY }); if (result?.value) { - localStorage.setItem(REFRESH_TOKEN_KEY, result.value); + const storage = getLocalStorageSafe(); + storage?.setItem(REFRESH_TOKEN_KEY, result.value); refresh = result.value; } } catch {