From 326bd91d1bba9f92c45879c6d97eed18042f11b8 Mon Sep 17 00:00:00 2001 From: Adrien Matissart Date: Thu, 19 Sep 2024 16:18:14 +0200 Subject: [PATCH] [front] fix: check localStorage is available on app load before read/write --- frontend/src/app/localStorage.ts | 13 +++++ frontend/src/features/frame/Frame.tsx | 8 +-- .../frame/components/topbar/PwaBanner.tsx | 10 ++-- frontend/src/hooks/useCurrentPoll.tsx | 5 +- frontend/src/hooks/useLastPoll.ts | 3 +- frontend/src/utils/comparison/pending.ts | 49 +++++++++++-------- frontend/src/utils/comparisonSeries/skip.ts | 17 +++---- .../src/utils/entityContexts/collapsed.ts | 18 +++---- .../src/utils/recommendationsLanguages.ts | 8 +-- 9 files changed, 70 insertions(+), 61 deletions(-) create mode 100644 frontend/src/app/localStorage.ts diff --git a/frontend/src/app/localStorage.ts b/frontend/src/app/localStorage.ts new file mode 100644 index 0000000000..591d0d6eb3 --- /dev/null +++ b/frontend/src/app/localStorage.ts @@ -0,0 +1,13 @@ +function getLocalStorage() { + try { + const key = '__checking_local_storage__'; + localStorage.setItem(key, key); + localStorage.removeItem(key); + return localStorage; + } catch (e) { + console.error(`Cannot to use localstorage: ${e}`); + return null; + } +} + +export const storage = getLocalStorage(); diff --git a/frontend/src/features/frame/Frame.tsx b/frontend/src/features/frame/Frame.tsx index 5e9ee3986a..65a72e956e 100644 --- a/frontend/src/features/frame/Frame.tsx +++ b/frontend/src/features/frame/Frame.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import clsx from 'clsx'; import makeStyles from '@mui/styles/makeStyles'; import { Box } from '@mui/material'; +import { storage } from 'src/app/localStorage'; import TopBar, { topBarHeight } from './components/topbar/TopBar'; import Footer from './components/footer/Footer'; import SideBar from './components/sidebar/SideBar'; @@ -52,12 +53,7 @@ const hasLocalStorageAccess = async () => { if (hasStorageAccess != null) { return hasStorageAccess; } - try { - localStorage; - return true; - } catch (err) { - return false; - } + return storage != null; }; const applyEmbeddedStyle = () => { diff --git a/frontend/src/features/frame/components/topbar/PwaBanner.tsx b/frontend/src/features/frame/components/topbar/PwaBanner.tsx index fc6c09a8d5..674b477a4b 100644 --- a/frontend/src/features/frame/components/topbar/PwaBanner.tsx +++ b/frontend/src/features/frame/components/topbar/PwaBanner.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { storage } from 'src/app/localStorage'; import { BeforeInstallPromptEvent } from '../../pwaPrompt'; import { Avatar, Button, Grid, Typography } from '@mui/material'; import { useTranslation } from 'react-i18next'; @@ -6,12 +7,11 @@ import { useTranslation } from 'react-i18next'; const pwaBannerIgnoredKey = 'pwaBannerIgnoredAt'; const hasPwaBannerBeenIgnored = () => { - try { - const value = localStorage.getItem(pwaBannerIgnoredKey); - return value != null; - } catch { + if (!storage) { + // Avoid showing the banner when ignore action can't be persisted return true; } + return storage.getItem(pwaBannerIgnoredKey) != null; }; interface Props { @@ -53,7 +53,7 @@ const PwaBanner = ({ beforeInstallPromptEvent }: Props) => { color="inherit" onClick={() => { setPwaBannerVisible(false); - localStorage.setItem(pwaBannerIgnoredKey, new Date().toISOString()); + storage?.setItem(pwaBannerIgnoredKey, new Date().toISOString()); }} > {t('pwaBanner.ignore')} diff --git a/frontend/src/hooks/useCurrentPoll.tsx b/frontend/src/hooks/useCurrentPoll.tsx index 5d87562841..3cef3010a9 100644 --- a/frontend/src/hooks/useCurrentPoll.tsx +++ b/frontend/src/hooks/useCurrentPoll.tsx @@ -6,6 +6,7 @@ import React, { useMemo, } from 'react'; import { useTranslation } from 'react-i18next'; +import { storage } from 'src/app/localStorage'; import { PollsService, Poll } from 'src/services/openapi'; import { LAST_POLL_NAME_STORAGE_KEY, polls } from 'src/utils/constants'; import { YOUTUBE_POLL_NAME } from 'src/utils/constants'; @@ -71,9 +72,7 @@ export const PollProvider = ({ children }: { children: React.ReactNode }) => { useEffect(() => { // Persist the last poll in localStorage for future sessions (after signup, etc.) - if (localStorage) { - localStorage.setItem(LAST_POLL_NAME_STORAGE_KEY, contextValue.name); - } + storage?.setItem(LAST_POLL_NAME_STORAGE_KEY, contextValue.name); }, [contextValue.name]); useEffect(() => { diff --git a/frontend/src/hooks/useLastPoll.ts b/frontend/src/hooks/useLastPoll.ts index be8c449232..cbae423c1f 100644 --- a/frontend/src/hooks/useLastPoll.ts +++ b/frontend/src/hooks/useLastPoll.ts @@ -1,8 +1,9 @@ import { useEffect } from 'react'; +import { storage } from 'src/app/localStorage'; import { LAST_POLL_NAME_STORAGE_KEY, polls } from 'src/utils/constants'; import { useCurrentPoll } from './useCurrentPoll'; -const lastSessionPollName = localStorage?.getItem(LAST_POLL_NAME_STORAGE_KEY); +const lastSessionPollName = storage?.getItem(LAST_POLL_NAME_STORAGE_KEY); const useLastPoll = () => { // Hook that will activate the last poll that was persisted diff --git a/frontend/src/utils/comparison/pending.ts b/frontend/src/utils/comparison/pending.ts index 27560014ab..0ca9167b58 100644 --- a/frontend/src/utils/comparison/pending.ts +++ b/frontend/src/utils/comparison/pending.ts @@ -1,3 +1,4 @@ +import { storage } from 'src/app/localStorage'; import { PollCriteria } from 'src/services/openapi'; import { CriteriaValuesType } from 'src/utils/types'; @@ -53,20 +54,20 @@ export const setPendingRating = ( criterion: string, rating: number ) => { - let pending = localStorage.getItem(PENDING_NS); - + if (!storage) { + return; + } + let pending = storage.getItem(PENDING_NS); if (pending == null) { pending = initPending(); } if (keyIsInvalid(poll, uidA, uidB, criterion)) { - return null; + return; } - const pendingJSON = JSON.parse(pending); pendingJSON[makePendingKey(poll, uidA, uidB, criterion)] = rating; - - localStorage.setItem(PENDING_NS, JSON.stringify(pendingJSON)); + storage.setItem(PENDING_NS, JSON.stringify(pendingJSON)); }; /** @@ -83,10 +84,12 @@ export const getPendingRating = ( uidA: string, uidB: string, criterion: string, - pop?: boolean + pop = false ): number | null => { - const pending = localStorage.getItem(PENDING_NS) ?? initPending(); - + if (!storage) { + return null; + } + const pending = storage.getItem(PENDING_NS) ?? initPending(); if (pendingIsEmpty(pending)) { return null; } @@ -97,12 +100,11 @@ export const getPendingRating = ( const pendingJSON = JSON.parse(pending); const pendingKey = makePendingKey(poll, uidA, uidB, criterion); - const rating = pendingJSON[pendingKey]; if (pop) { delete pendingJSON[pendingKey]; - localStorage.setItem(PENDING_NS, JSON.stringify(pendingJSON)); + storage.setItem(PENDING_NS, JSON.stringify(pendingJSON)); } return rating ?? null; @@ -121,7 +123,10 @@ export const clearPendingRating = ( uidB: string, criterion: string ) => { - const pending = localStorage.getItem(PENDING_NS) ?? initPending(); + if (!storage) { + return; + } + const pending = storage.getItem(PENDING_NS) ?? initPending(); if (pendingIsEmpty(pending)) { return; @@ -133,7 +138,7 @@ export const clearPendingRating = ( const pendingJSON = JSON.parse(pending); delete pendingJSON[makePendingKey(poll, uidA, uidB, criterion)]; - localStorage.setItem(PENDING_NS, JSON.stringify(pendingJSON)); + storage.setItem(PENDING_NS, JSON.stringify(pendingJSON)); }; /** @@ -150,11 +155,10 @@ export const getAllPendingRatings = ( uidA: string, uidB: string, criterias: PollCriteria[], - pop?: boolean + pop = false ): CriteriaValuesType => { const pendingCriteria: CriteriaValuesType = {}; - - const pending = localStorage.getItem(PENDING_NS) ?? initPending(); + const pending = storage?.getItem(PENDING_NS) ?? initPending(); if (pendingIsEmpty(pending)) { return {}; @@ -183,7 +187,7 @@ export const getAllPendingRatings = ( } }); - localStorage.setItem(PENDING_NS, JSON.stringify(pendingJSON)); + storage?.setItem(PENDING_NS, JSON.stringify(pendingJSON)); return pendingCriteria; }; @@ -200,7 +204,10 @@ export const clearAllPendingRatings = ( uidB: string, criterias: PollCriteria[] ) => { - const pending = localStorage.getItem(PENDING_NS) ?? initPending(); + if (!storage) { + return; + } + const pending = storage.getItem(PENDING_NS) ?? initPending(); if (pendingIsEmpty(pending)) { return; @@ -216,13 +223,13 @@ export const clearAllPendingRatings = ( const pendingJSON = JSON.parse(pending); - criterias.map((criterion) => { + criterias.forEach((criterion) => { delete pendingJSON[makePendingKey(poll, uidA, uidB, criterion.name)]; }); - localStorage.setItem(PENDING_NS, JSON.stringify(pendingJSON)); + storage.setItem(PENDING_NS, JSON.stringify(pendingJSON)); }; export const resetPendingRatings = () => { - localStorage.setItem(PENDING_NS, initPending()); + storage?.setItem(PENDING_NS, initPending()); }; diff --git a/frontend/src/utils/comparisonSeries/skip.ts b/frontend/src/utils/comparisonSeries/skip.ts index 87e09db4be..4a1b9ff4db 100644 --- a/frontend/src/utils/comparisonSeries/skip.ts +++ b/frontend/src/utils/comparisonSeries/skip.ts @@ -1,3 +1,5 @@ +import { storage } from 'src/app/localStorage'; + /** * The day we will have different components using the skippedBy state, we * should consider using Redux instead of our custom localstorage accessors. @@ -16,9 +18,9 @@ const skippedByIsEmpty = (skippedBy: string) => { * @param username The username of the user. */ export const setSkippedBy = (skipKey: string, username: string) => { - const skippedBy = localStorage.getItem(skipKey) ?? INITAL_SKIPPED_BY; - let users: Array; + const skippedBy = storage?.getItem(skipKey) ?? INITAL_SKIPPED_BY; + let users: Array; if (skippedByIsEmpty(skippedBy)) { users = []; } else { @@ -29,7 +31,7 @@ export const setSkippedBy = (skipKey: string, username: string) => { users.push(username); } - localStorage.setItem(skipKey, users.join(',')); + storage?.setItem(skipKey, users.join(',')); }; /** @@ -37,17 +39,12 @@ export const setSkippedBy = (skipKey: string, username: string) => { * skipped by the user. */ export const getSkippedBy = (skipKey: string, username: string): boolean => { - const skippedBy = localStorage.getItem(skipKey) ?? INITAL_SKIPPED_BY; + const skippedBy = storage?.getItem(skipKey) ?? INITAL_SKIPPED_BY; if (skippedByIsEmpty(skippedBy)) { return false; } const users = skippedBy.split(','); - - if (users.includes(username)) { - return true; - } - - return false; + return users.includes(username); }; diff --git a/frontend/src/utils/entityContexts/collapsed.ts b/frontend/src/utils/entityContexts/collapsed.ts index f5c10acce1..168eb6ad5a 100644 --- a/frontend/src/utils/entityContexts/collapsed.ts +++ b/frontend/src/utils/entityContexts/collapsed.ts @@ -1,32 +1,26 @@ +import { storage } from 'src/app/localStorage'; + // Name of the key used in the browser's local storage. const COLLAPSED_NS = 'entityContextsCollapsed'; const initCollapsed = () => '{}'; -const collapsedIsEmpty = (collapsed: string) => { - return collapsed == null || collapsed === initCollapsed(); -}; - export const setCollapsedState = (uid: string, state: boolean) => { - let collapsed = localStorage.getItem(COLLAPSED_NS); - + let collapsed = storage?.getItem(COLLAPSED_NS); if (collapsed == null) { collapsed = initCollapsed(); } const collapsedJSON = JSON.parse(collapsed); collapsedJSON[uid] = state; - - localStorage.setItem(COLLAPSED_NS, JSON.stringify(collapsedJSON)); + storage?.setItem(COLLAPSED_NS, JSON.stringify(collapsedJSON)); }; export const getCollapsedState = (uid: string): boolean | null => { - const collapsed = localStorage.getItem(COLLAPSED_NS) ?? initCollapsed(); - - if (collapsedIsEmpty(collapsed)) { + const collapsed = storage?.getItem(COLLAPSED_NS) ?? initCollapsed(); + if (!collapsed) { return null; } - const pendingJSON = JSON.parse(collapsed); const state = pendingJSON[uid]; return state ?? null; diff --git a/frontend/src/utils/recommendationsLanguages.ts b/frontend/src/utils/recommendationsLanguages.ts index bc4fa55b38..4138d92608 100644 --- a/frontend/src/utils/recommendationsLanguages.ts +++ b/frontend/src/utils/recommendationsLanguages.ts @@ -1,4 +1,5 @@ import { TFunction } from 'react-i18next'; +import { storage } from 'src/app/localStorage'; import { uniq } from 'src/utils/array'; export const recommendationsLanguages: { @@ -79,7 +80,7 @@ export const getLanguageName = (t: TFunction, language: string) => { }; export const saveRecommendationsLanguages = (value: string) => { - localStorage.setItem('recommendationsLanguages', value); + storage?.setItem('recommendationsLanguages', value); const event = new CustomEvent('tournesol:recommendationsLanguagesChange', { detail: { recommendationsLanguages: value }, }); @@ -96,8 +97,9 @@ export const initRecommendationsLanguages = (): string => { return languages; }; -export const loadRecommendationsLanguages = (): string | null => - localStorage.getItem('recommendationsLanguages'); +export const loadRecommendationsLanguages = (): string | null => { + return storage?.getItem('recommendationsLanguages') ?? null; +}; export const recommendationsLanguagesFromNavigator = (): string => // This function also exists in the browser extension so it should be updated there too if it changes here.