diff --git a/.changeset/popular-feet-bake.md b/.changeset/popular-feet-bake.md new file mode 100644 index 000000000..f3bf95e9c --- /dev/null +++ b/.changeset/popular-feet-bake.md @@ -0,0 +1,7 @@ +--- +'gitbook': patch +--- + +Implement a safer way to interact with localStorage. + +If it's disabled on the browser it should not throw error. diff --git a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx index 506a78f1a..b209cd61e 100644 --- a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx +++ b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useMemo } from 'react'; import { useHash, useIsMounted } from '@/components/hooks'; +import * as storage from '@/lib/local-storage'; import { ClassValue, tcls } from '@/lib/tailwind'; interface TabsState { @@ -12,15 +13,12 @@ interface TabsState { activeTitles: string[]; } -let globalTabsState: TabsState = (() => { - if (typeof localStorage === 'undefined') { - return { activeIds: {}, activeTitles: [] }; - } - - const stored = localStorage.getItem('@gitbook/tabsState'); - return stored ? (JSON.parse(stored) as TabsState) : { activeIds: {}, activeTitles: [] }; -})(); +const defaultTabsState: TabsState = { + activeIds: {}, + activeTitles: [], +}; +let globalTabsState = storage.getItem('@gitbook/tabsState', defaultTabsState); const listeners = new Set<() => void>(); function useTabsState() { @@ -33,9 +31,7 @@ function useTabsState() { const setTabsState = useCallback((updater: (previous: TabsState) => TabsState) => { globalTabsState = updater(globalTabsState); - if (typeof localStorage !== 'undefined') { - localStorage.setItem('@gitbook/tabsState', JSON.stringify(globalTabsState)); - } + storage.setItem('@gitbook/tabsState', globalTabsState); listeners.forEach((listener) => listener()); }, []); const state = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot); diff --git a/packages/gitbook/src/components/Insights/sessions.ts b/packages/gitbook/src/components/Insights/sessions.ts index 061894dc2..48a12e962 100644 --- a/packages/gitbook/src/components/Insights/sessions.ts +++ b/packages/gitbook/src/components/Insights/sessions.ts @@ -1,5 +1,7 @@ 'use client'; +import * as storage from '@/lib/local-storage'; + import { generateRandomId } from './utils'; const SESSION_TTL = 1000 * 60 * 30; // 30 minutes @@ -22,22 +24,20 @@ export function getSession(): Session { } try { - const rawSession = - typeof localStorage !== 'undefined' ? localStorage.getItem(SESSION_KEY) : null; + const session = storage.getItem(SESSION_KEY, null); - if (rawSession) { - const storedSession = JSON.parse(rawSession); - - if ( - typeof storedSession === 'object' && - typeof storedSession.lastActiveAt === 'number' && - typeof storedSession.id === 'string' && - storedSession.lastActiveAt + SESSION_TTL > Date.now() - ) { - currentSession = storedSession as Session; - touchSession(); - return currentSession; - } + if ( + session && + typeof session === 'object' && + 'lastActiveAt' in session && + typeof session.lastActiveAt === 'number' && + 'id' in session && + typeof session.id === 'string' && + session.lastActiveAt + SESSION_TTL > Date.now() + ) { + currentSession = session as Session; + touchSession(); + return currentSession; } } catch (error) { console.error('Error parsing session', error); @@ -65,7 +65,7 @@ export function touchSession() { * Save the session to the local storage. */ export function saveSession() { - if (typeof localStorage !== 'undefined' && currentSession) { - localStorage.setItem(SESSION_KEY, JSON.stringify(currentSession)); + if (currentSession) { + storage.setItem(SESSION_KEY, currentSession); } } diff --git a/packages/gitbook/src/lib/local-storage.ts b/packages/gitbook/src/lib/local-storage.ts new file mode 100644 index 000000000..f96653dff --- /dev/null +++ b/packages/gitbook/src/lib/local-storage.ts @@ -0,0 +1,36 @@ +/** + * Get an item from local storage safely. + */ +export function getItem(key: string, defaultValue: T): T { + if (typeof localStorage === 'undefined') { + return defaultValue; + } + + try { + const stored = localStorage.getItem(key); + return stored ? (JSON.parse(stored) as T) : defaultValue; + } catch (error) { + if (error instanceof Error && error.name === 'SecurityError') { + return defaultValue; + } + throw error; + } +} + +/** + * Set an item in local storage safely. + */ +export function setItem(key: string, value: unknown) { + if (typeof localStorage === 'undefined') { + return; + } + + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + if (error instanceof Error && error.name === 'SecurityError') { + return; + } + throw error; + } +}