diff --git a/site/source/ServiceWorker.tsx b/site/source/ServiceWorker.tsx index fd224562c..9512b19d6 100644 --- a/site/source/ServiceWorker.tsx +++ b/site/source/ServiceWorker.tsx @@ -1,4 +1,5 @@ -import { useEffect } from 'react' +import { getItem, removeItem, setItem } from '@/storage/safeLocalStorage' +import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import styled from 'styled-components' import { useRegisterSW } from 'virtual:pwa-register/react' @@ -36,35 +37,57 @@ const StyledHideButton = styled.div` right: 0.375rem; ` +const pwaPromptDelayKey = 'update-pwa-prompt-delay' + export const ServiceWorker = () => { const { t } = useTranslation() + const [showPrompt, setShowPrompt] = useState(false) const { - offlineReady: [offlineReady, setOfflineReady], needRefresh: [needRefresh, setNeedRefresh], updateServiceWorker, } = useRegisterSW({ + immediate: true, onRegistered: (r) => { // eslint-disable-next-line no-console console.log('=> SW Registered: ', r) + + // If no service worker is waiting, delete the old prompt delay + if (!r?.waiting) { + removeItem(pwaPromptDelayKey) + } }, + + onNeedRefresh() { + const promptDelay = parseInt(getItem(pwaPromptDelayKey) ?? '0') + + // If we need a refresh and there is no prompt delay, create one in 3 days + if (promptDelay === 0) { + setItem( + pwaPromptDelayKey, + (Date.now() + 3 * 24 * 60 * 60 * 1000).toString() + ) + } + // If we need a refresh and the prompt delay has passed, show the prompt + if (promptDelay > 0 && promptDelay < Date.now()) { + setShowPrompt(true) + } + }, + + onOfflineReady() { + // eslint-disable-next-line no-console + console.log('App is ready to work offline.') + }, + onRegisterError: (error) => { // eslint-disable-next-line no-console console.log('SW registration error', error) }, }) - useEffect(() => { - if (offlineReady) { - setOfflineReady(false) - // eslint-disable-next-line no-console - console.log('App is ready to work offline.') - } - }, [offlineReady, setOfflineReady]) - return ( - {needRefresh && ( + {needRefresh && showPrompt && ( diff --git a/site/source/sw.ts b/site/source/sw.ts index 8162e7285..439d0c7dc 100644 --- a/site/source/sw.ts +++ b/site/source/sw.ts @@ -1,14 +1,16 @@ import { ExpirationPlugin } from 'workbox-expiration' -import { - cleanupOutdatedCaches, - createHandlerBoundToURL, - precacheAndRoute, -} from 'workbox-precaching' -import { NavigationRoute, registerRoute, Route } from 'workbox-routing' +import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching' +import { offlineFallback } from 'workbox-recipes' +import { registerRoute, Route, setDefaultHandler } from 'workbox-routing' import { NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies' declare let self: ServiceWorkerGlobalScope +const HOUR = 60 * 60 +const DAY = 24 * HOUR +const YEAR = 365 * DAY +const MONTH = YEAR / 12 + self.addEventListener('message', (event) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (event.data && event.data.type === 'SKIP_WAITING') { @@ -18,23 +20,41 @@ self.addEventListener('message', (event) => { cleanupOutdatedCaches() -precacheAndRoute(self.__WB_MANIFEST) - -// Allow work offline -registerRoute( - new NavigationRoute( - createHandlerBoundToURL( +// Filter EN files on the FR app and vice versa +const precache = self.__WB_MANIFEST.filter( + (entry) => + typeof entry !== 'string' && + !entry.url.includes( location.href.startsWith(import.meta.env.VITE_FR_BASE_URL) - ? 'mon-entreprise.html' - : 'infrance.html' - ), - { denylist: [/^\/api\/.*/, /^\/twemoji\/.*/, /^\/dev\/storybook\/.*/] } - ) + ? 'infrance' + : 'mon-entreprise' + ) ) -const HOUR = 60 * 60 -const DAY = HOUR * 24 -const YEAR = DAY * 365 +precacheAndRoute(precache) + +// Allow work offline +offlineFallback({ + // When the user is offline and loads a page that is not cached, we return the French/English index file. + pageFallback: location.href.startsWith(import.meta.env.VITE_FR_BASE_URL) + ? '/mon-entreprise.html' + : '/infrance.html', +}) + +// Set the default handler with network first so that every requests (css, js, html, image, etc.) +// goes through the service worker and will be cache +setDefaultHandler( + new NetworkFirst({ + cacheName: 'default-network-first', + networkTimeoutSeconds: 1, + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 3 * MONTH, + maxEntries: 40, + }), + ], + }) +) const networkFirstJS = new Route( ({ sameOrigin, url }) => { @@ -44,11 +64,10 @@ const networkFirstJS = new Route( cacheName: 'js-cache', plugins: [ new ExpirationPlugin({ - maxAgeSeconds: 30 * DAY, + maxAgeSeconds: 1 * MONTH, maxEntries: 40, }), ], - fetchOptions: {}, }) ) @@ -58,11 +77,13 @@ const staleWhileRevalidate = new Route( ({ request, sameOrigin, url }) => { return ( sameOrigin && - (url.pathname.startsWith('/twemoji/') || request.destination === 'image') + (url.pathname.startsWith('/twemoji/') || + request.destination === 'image' || + request.destination === 'font') ) }, new StaleWhileRevalidate({ - cacheName: 'images', + cacheName: 'media', plugins: [ new ExpirationPlugin({ maxAgeSeconds: 1 * YEAR, @@ -83,7 +104,7 @@ const networkFirstPolyfill = new Route( cacheName: 'external-polyfill', plugins: [ new ExpirationPlugin({ - maxAgeSeconds: 1 * YEAR, + maxAgeSeconds: 3 * MONTH, maxEntries: 5, }), ], diff --git a/site/vite-pwa-options.ts b/site/vite-pwa-options.ts index c372d6755..0f364fad9 100644 --- a/site/vite-pwa-options.ts +++ b/site/vite-pwa-options.ts @@ -1,7 +1,7 @@ import { Options } from 'vite-plugin-pwa' export const pwaOptions: Partial = { - selfDestroying: true, // Unregister PWA + // selfDestroying: true, // Unregister PWA registerType: 'prompt', strategies: 'injectManifest', srcDir: 'source', @@ -11,18 +11,18 @@ export const pwaOptions: Partial = { manifestTransforms: [ (entries) => { const manifest = entries.filter( - (entry) => !/assets\/.*(-legacy|lazy_)/.test(entry.url) + (entry) => + !/assets\/.*(-legacy|lazy_)/.test(entry.url) && + (entry.url.endsWith('.html') + ? /(infrance|mon-entreprise)\.html/.test(entry.url) + : true) ) return { manifest } }, ], }, - includeAssets: [ - 'logo-*.png', - 'fonts/*.{woff,woff2}', - 'références-images/*.{jpg,png,svg}', - ], + includeAssets: ['logo-*.png'], manifest: { start_url: '/', name: 'Mon entreprise',