diff --git a/src/components/LazyModalSlot.tsx b/src/components/LazyModalSlot.tsx index 7c60f6b8abc9..e9f33387ec95 100644 --- a/src/components/LazyModalSlot.tsx +++ b/src/components/LazyModalSlot.tsx @@ -1,10 +1,9 @@ import * as Sentry from '@sentry/react-native'; import React, {Suspense} from 'react'; import {ErrorBoundary as ReactErrorBoundary} from 'react-error-boundary'; +import isChunkLoadError from '@libs/isChunkLoadError'; import Log from '@libs/Log'; -const isChunkLoadError = (error: Error) => error.name === 'ChunkLoadError' || /Loading chunk \S+ failed/i.test(error.message); - const logModalError = (error: Error, info: {componentStack?: string | null}) => { const componentStack = info.componentStack ?? undefined; diff --git a/src/hooks/usePageRefresh/index.ts b/src/hooks/usePageRefresh/index.ts index e8f4f9bc6877..76ed85576cb8 100644 --- a/src/hooks/usePageRefresh/index.ts +++ b/src/hooks/usePageRefresh/index.ts @@ -1,5 +1,6 @@ import {differenceInMilliseconds} from 'date-fns/differenceInMilliseconds'; import {useErrorBoundary} from 'react-error-boundary'; +import clearWorkboxRecoveryCaches from '@libs/clearWorkboxRecoveryCaches'; import CONST from '@src/CONST'; import type UsePageRefresh from './type'; @@ -16,8 +17,16 @@ const usePageRefresh: UsePageRefresh = () => { return; } - window.location.reload(); sessionStorage.removeItem(CONST.SESSION_STORAGE_KEYS.LAST_REFRESH_TIMESTAMP); + if (isChunkLoadError && navigator.onLine) { + // The error page is shown after lazyRetry has already done a plain reload and it did + // not fix the problem. When online, clear the service worker cache so the next load + // fetches a fresh app shell from the CDN. When offline we must not clear it: the + // cached shell is the only thing keeping the PWA usable until connectivity returns. + clearWorkboxRecoveryCaches().then(() => window.location.reload()); + } else { + window.location.reload(); + } }; }; diff --git a/src/libs/Navigation/AppNavigator/AppNavigator.tsx b/src/libs/Navigation/AppNavigator/AppNavigator.tsx index 063fdd9e0a7c..a7d07c0ff1e3 100644 --- a/src/libs/Navigation/AppNavigator/AppNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/AppNavigator.tsx @@ -1,8 +1,8 @@ import React, {lazy, memo, Suspense} from 'react'; import lazyRetry from '@src/utils/lazyRetry'; -const AuthScreens = lazy(() => lazyRetry(() => import(/* webpackChunkName: "authScreens.prefetch" */ './AuthScreens'))); -const PublicScreens = lazy(() => lazyRetry(() => import(/* webpackMode: "eager" */ './PublicScreens'))); +const AuthScreens = lazy(() => lazyRetry(() => import(/* webpackChunkName: "authScreens.prefetch" */ './AuthScreens'), 'authScreens')); +const PublicScreens = lazy(() => lazyRetry(() => import(/* webpackMode: "eager" */ './PublicScreens'), 'publicScreens')); type AppNavigatorProps = { /** If we have an authToken this is true */ diff --git a/src/libs/Navigation/AppNavigator/index.tsx b/src/libs/Navigation/AppNavigator/index.tsx index 834ba7f281fa..d1d570a78c35 100644 --- a/src/libs/Navigation/AppNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/index.tsx @@ -2,7 +2,7 @@ import React, {lazy, Suspense} from 'react'; import type {ComponentProps} from 'react'; import lazyRetry from '@src/utils/lazyRetry'; -const AppNavigator = lazy(() => lazyRetry(() => import(/* webpackChunkName: "appNavigator.prefetch" */ './AppNavigator'))); +const AppNavigator = lazy(() => lazyRetry(() => import(/* webpackChunkName: "appNavigator.prefetch" */ './AppNavigator'), 'appNavigator')); function AppNavigatorLoader({authenticated}: ComponentProps) { return ( diff --git a/src/libs/clearWorkboxRecoveryCaches/index.ts b/src/libs/clearWorkboxRecoveryCaches/index.ts index 003c23cad188..2bc9c5bad44d 100644 --- a/src/libs/clearWorkboxRecoveryCaches/index.ts +++ b/src/libs/clearWorkboxRecoveryCaches/index.ts @@ -4,8 +4,8 @@ import Log from '@libs/Log'; /** * Clears all Cache Storage entries for this origin and unregisters every * service worker. Used when the user runs Troubleshoot > Clear cache and - * restart so Workbox precache/runtime caches do not survive that recovery - * path (stale app shell / chunks would otherwise keep serving from SW). + * restart, and during ChunkLoadError recovery, so stale cached assets do + * not survive those recovery paths and keep re-serving broken chunks. */ async function clearWorkboxRecoveryCaches(): Promise { // Normally platform-specific behaviour is achieved with .native.ts / .ts file pairs. diff --git a/src/libs/isChunkLoadError.ts b/src/libs/isChunkLoadError.ts new file mode 100644 index 000000000000..174a291e3946 --- /dev/null +++ b/src/libs/isChunkLoadError.ts @@ -0,0 +1,14 @@ +import CONST from '@src/CONST'; + +/** + * Returns true if the given error is a webpack ChunkLoadError — the error thrown when a + * dynamically-imported script cannot be fetched (e.g. after a deploy removes old chunk hashes). + */ +function isChunkLoadError(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + return error.name === CONST.CHUNK_LOAD_ERROR || /Loading chunk \S+ failed/i.test(error.message); +} + +export default isChunkLoadError; diff --git a/src/pages/ErrorPage/GenericErrorPage.tsx b/src/pages/ErrorPage/GenericErrorPage.tsx index d11397d13b52..5a15d766a833 100644 --- a/src/pages/ErrorPage/GenericErrorPage.tsx +++ b/src/pages/ErrorPage/GenericErrorPage.tsx @@ -13,6 +13,7 @@ import usePageRefresh from '@hooks/usePageRefresh'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import isChunkLoadError from '@libs/isChunkLoadError'; import variables from '@styles/variables'; import {signOutAndRedirectToSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; @@ -24,7 +25,7 @@ function GenericErrorPage({error}: {error?: Error}) { const StyleUtils = useStyleUtils(); const isAuthenticated = useIsAuthenticated(); const {translate} = useLocalize(); - const isChunkLoadError = error?.name === CONST.CHUNK_LOAD_ERROR || /Loading chunk [\d]+ failed/.test(error?.message ?? ''); + const chunkLoadError = isChunkLoadError(error); const refreshPage = usePageRefresh(); const icons = useMemoizedLazyExpensifyIcons(['ExpensifyWordmark', 'Bug']); @@ -63,7 +64,7 @@ function GenericErrorPage({error}: {error?: Error}) { success text={translate('genericErrorPage.refresh')} style={styles.mr3} - onPress={() => refreshPage(isChunkLoadError)} + onPress={() => refreshPage(chunkLoadError)} /> {isAuthenticated && (