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],