diff --git a/release-notes/unreleased/2924-security-error.yml b/release-notes/unreleased/2924-security-error.yml
new file mode 100644
index 0000000000..1c5f94a204
--- /dev/null
+++ b/release-notes/unreleased/2924-security-error.yml
@@ -0,0 +1,5 @@
+issue_key: 2924
+show_in_stores: false
+platforms:
+ - web
+en: Fix crashes if local storage is not available
diff --git a/web/src/hooks/__tests__/useLocalStorage.spec.tsx b/web/src/hooks/__tests__/useLocalStorage.spec.tsx
new file mode 100644
index 0000000000..71cc7a926e
--- /dev/null
+++ b/web/src/hooks/__tests__/useLocalStorage.spec.tsx
@@ -0,0 +1,75 @@
+import { fireEvent, render } from '@testing-library/react'
+import React from 'react'
+
+import Button from '../../components/base/Button'
+import useLocalStorage from '../useLocalStorage'
+
+describe('useLocalStorage', () => {
+ const key = 'my_storage_key'
+ const MockComponent = () => {
+ const { value, updateLocalStorageItem } = useLocalStorage({ key, initialValue: 0 })
+ return (
+
+ {value}
+
+
+ )
+ }
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ localStorage.clear()
+ })
+
+ it('should correctly set initial value and update value', () => {
+ const { getByText } = render()
+
+ expect(getByText(0)).toBeTruthy()
+ expect(localStorage.getItem(key)).toBe('0')
+
+ fireEvent.click(getByText('Increment'))
+
+ expect(getByText(1)).toBeTruthy()
+ expect(localStorage.getItem(key)).toBe('1')
+
+ fireEvent.click(getByText('Increment'))
+ fireEvent.click(getByText('Increment'))
+ fireEvent.click(getByText('Increment'))
+
+ expect(getByText(4)).toBeTruthy()
+ expect(localStorage.getItem(key)).toBe('4')
+ })
+
+ it('should not use initial value if already set', () => {
+ localStorage.setItem(key, '10')
+ const { getByText } = render()
+
+ expect(getByText(10)).toBeTruthy()
+ expect(localStorage.getItem(key)).toBe('10')
+
+ fireEvent.click(getByText('Increment'))
+
+ expect(getByText(11)).toBeTruthy()
+ expect(localStorage.getItem(key)).toBe('11')
+ })
+
+ it('should continue to work even if local storage is not usable', () => {
+ localStorage.getItem = () => {
+ throw new Error('SecurityError')
+ }
+ localStorage.setItem = () => {
+ throw new Error('SecurityError')
+ }
+ const { getByText } = render()
+
+ expect(getByText(0)).toBeTruthy()
+ expect(localStorage.getItem(key)).toBe('0')
+
+ fireEvent.click(getByText('Increment'))
+
+ expect(getByText(1)).toBeTruthy()
+ expect(localStorage.getItem(key)).toBe('1')
+ })
+})
diff --git a/web/src/hooks/useLocalStorage.ts b/web/src/hooks/useLocalStorage.ts
index 24e861697c..cb3d20abdd 100644
--- a/web/src/hooks/useLocalStorage.ts
+++ b/web/src/hooks/useLocalStorage.ts
@@ -1,5 +1,7 @@
import { useState, useCallback } from 'react'
+import { reportError } from '../utils/sentry'
+
type UseLocalStorageProps = {
key: string
initialValue: T
@@ -12,17 +14,35 @@ type UseLocalStorageReturn = {
const useLocalStorage = ({ key, initialValue }: UseLocalStorageProps): UseLocalStorageReturn => {
const [value, setValue] = useState(() => {
- const localStorageItem = localStorage.getItem(key)
- if (localStorageItem) {
- return JSON.parse(localStorageItem)
+ try {
+ const localStorageItem = localStorage.getItem(key)
+ if (localStorageItem) {
+ return JSON.parse(localStorageItem)
+ }
+ localStorage.setItem(key, JSON.stringify(initialValue))
+ } catch (e) {
+ // Prevent the following error crashing the app if the browser blocks access to local storage (see #2924)
+ // SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.
+ const accessDenied = e instanceof Error && e.message.includes('Access is denied for this document')
+ if (!accessDenied) {
+ reportError(e)
+ }
}
- localStorage.setItem(key, JSON.stringify(initialValue))
return initialValue
})
const updateLocalStorageItem = useCallback(
(newValue: T) => {
- localStorage.setItem(key, JSON.stringify(newValue))
+ try {
+ localStorage.setItem(key, JSON.stringify(newValue))
+ } catch (e) {
+ // Prevent the following error crashing the app if the browser blocks access to local storage (see #2924)
+ // SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.
+ const accessDenied = e instanceof Error && e.message.includes('Access is denied for this document')
+ if (!accessDenied) {
+ reportError(e)
+ }
+ }
setValue(newValue)
},
[key],