Fix mobile white screen when localStorage throws on access (#54).
Unit Tests / test (pull_request) Successful in 25s

Guard JWT token reads/writes so auth bootstrap no longer crashes React when mobile browsers block storage.
This commit is contained in:
2026-07-29 04:41:51 -05:00
parent 799d3d591f
commit 4d00131854
+43 -11
View File
@@ -9,6 +9,21 @@
export const ACCESS_TOKEN_KEY = "access_token"; export const ACCESS_TOKEN_KEY = "access_token";
export const REFRESH_TOKEN_KEY = "refresh_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} * @returns {{ get?: Function, set?: Function, remove?: Function } | undefined}
*/ */
@@ -24,10 +39,15 @@ function getPreferencesPlugin() {
* @returns {string|null} * @returns {string|null}
*/ */
export function getToken(key) { export function getToken(key) {
if (typeof localStorage === "undefined") { const storage = getLocalStorageSafe();
if (!storage) {
return null;
}
try {
return storage.getItem(key);
} catch {
return null; return null;
} }
return localStorage.getItem(key);
} }
/** /**
@@ -51,10 +71,15 @@ export function getRefreshToken() {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export async function setTokens(access, refresh) { export async function setTokens(access, refresh) {
if (typeof localStorage !== "undefined") { const storage = getLocalStorageSafe();
localStorage.setItem(ACCESS_TOKEN_KEY, access); if (storage) {
if (refresh != null) { try {
localStorage.setItem(REFRESH_TOKEN_KEY, refresh); 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<void>} * @returns {Promise<void>}
*/ */
export async function clearTokens() { export async function clearTokens() {
if (typeof localStorage !== "undefined") { const storage = getLocalStorageSafe();
localStorage.removeItem(ACCESS_TOKEN_KEY); if (storage) {
localStorage.removeItem(REFRESH_TOKEN_KEY); try {
storage.removeItem(ACCESS_TOKEN_KEY);
storage.removeItem(REFRESH_TOKEN_KEY);
} catch {
/* localStorage unavailable */
}
} }
const Preferences = getPreferencesPlugin(); const Preferences = getPreferencesPlugin();
@@ -110,7 +140,8 @@ export async function hydrateTokensFromNativeStorage() {
try { try {
const result = await Preferences.get({ key: ACCESS_TOKEN_KEY }); const result = await Preferences.get({ key: ACCESS_TOKEN_KEY });
if (result?.value) { if (result?.value) {
localStorage.setItem(ACCESS_TOKEN_KEY, result.value); const storage = getLocalStorageSafe();
storage?.setItem(ACCESS_TOKEN_KEY, result.value);
access = result.value; access = result.value;
} }
} catch { } catch {
@@ -122,7 +153,8 @@ export async function hydrateTokensFromNativeStorage() {
try { try {
const result = await Preferences.get({ key: REFRESH_TOKEN_KEY }); const result = await Preferences.get({ key: REFRESH_TOKEN_KEY });
if (result?.value) { if (result?.value) {
localStorage.setItem(REFRESH_TOKEN_KEY, result.value); const storage = getLocalStorageSafe();
storage?.setItem(REFRESH_TOKEN_KEY, result.value);
refresh = result.value; refresh = result.value;
} }
} catch { } catch {