diff --git a/packages/webapp/pages/_app.tsx b/packages/webapp/pages/_app.tsx index f8cd9622e2..989defd642 100644 --- a/packages/webapp/pages/_app.tsx +++ b/packages/webapp/pages/_app.tsx @@ -41,6 +41,10 @@ import { useThemedAsset } from '@dailydotdev/shared/src/hooks/utils'; import { DndContextProvider } from '@dailydotdev/shared/src/contexts/DndContext'; import { structuredCloneJsonPolyfill } from '@dailydotdev/shared/src/lib/structuredClone'; import { fromCDN } from '@dailydotdev/shared/src/lib'; +import { + BrowserName, + getCurrentBrowserName, +} from '@dailydotdev/shared/src/lib/func'; import { useOnboardingActions } from '@dailydotdev/shared/src/hooks/auth'; import { useCheckCoresRole } from '@dailydotdev/shared/src/hooks/useCheckCoresRole'; import { @@ -108,14 +112,60 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement { useEffect(() => { if ( - user && - !didRegisterSwRef.current && - 'serviceWorker' in globalThis?.navigator && - window.serwist !== undefined + !user || + didRegisterSwRef.current || + typeof navigator === 'undefined' || + !('serviceWorker' in navigator) ) { + return; + } + + const unregisterSerwist = async () => { + if (!navigator.serviceWorker) { + return; + } + + try { + if (navigator.serviceWorker.getRegistrations) { + const registrations = + await navigator.serviceWorker.getRegistrations(); + await Promise.all( + registrations + .filter((registration) => { + const scriptURL = + registration.active?.scriptURL || + registration.installing?.scriptURL || + registration.waiting?.scriptURL; + + return scriptURL?.endsWith('/sw.js'); + }) + .map((registration) => registration.unregister()), + ); + } else if (navigator.serviceWorker.getRegistration) { + const registration = await navigator.serviceWorker.getRegistration( + '/sw.js', + ); + await registration?.unregister(); + } + } catch { + // Ignore cleanup failures – Safari sometimes rejects during shutdown. + } + }; + + const browserName = getCurrentBrowserName(); + + if (browserName === BrowserName.Safari) { didRegisterSwRef.current = true; - window.serwist.register(); + unregisterSerwist(); + return; } + + if (window.serwist === undefined) { + return; + } + + didRegisterSwRef.current = true; + window.serwist.register(); }, [user]); useEffect(() => { diff --git a/packages/webapp/sw.ts b/packages/webapp/sw.ts index ef29712060..6f57087ba5 100644 --- a/packages/webapp/sw.ts +++ b/packages/webapp/sw.ts @@ -15,17 +15,39 @@ declare global { declare const self: ServiceWorkerGlobalScope; -const serwist = new Serwist({ - // eslint-disable-next-line no-underscore-dangle - precacheEntries: self.__SW_MANIFEST, - skipWaiting: true, - clientsClaim: true, - navigationPreload: true, - runtimeCaching: defaultCache, - precacheOptions: { - concurrency: 3, - cleanupOutdatedCaches: true, - }, -}); +const excludedSafariVendors = /(Chrome|Chromium|Android|CriOS|FxiOS|Edg|OPR)/i; +const isSafariWorker = (): boolean => { + const userAgent = self.navigator?.userAgent || ''; + return /Safari/i.test(userAgent) && !excludedSafariVendors.test(userAgent); +}; -serwist.addEventListeners(); +if (isSafariWorker()) { + // Safari crashes with WebKitInternal:0 when this SW intercepts navigations (ENG-210). + self.addEventListener('install', (event) => { + event.waitUntil(self.skipWaiting()); + }); + + self.addEventListener('activate', (event) => { + event.waitUntil( + (async () => { + await self.clients.claim(); + await self.registration.unregister(); + })(), + ); + }); +} else { + const serwist = new Serwist({ + // eslint-disable-next-line no-underscore-dangle + precacheEntries: self.__SW_MANIFEST, + skipWaiting: true, + clientsClaim: true, + navigationPreload: true, + runtimeCaching: defaultCache, + precacheOptions: { + concurrency: 3, + cleanupOutdatedCaches: true, + }, + }); + + serwist.addEventListeners(); +}