80 lines
2.5 KiB
JavaScript
80 lines
2.5 KiB
JavaScript
/** Connects install/update lifecycle to the React interface and returns cleanup.
|
|
* @param {object} ui - Interface bridge.
|
|
* @returns {function} Releases browser event subscriptions.
|
|
*/
|
|
export function setupPwa(ui) {
|
|
const controller = new AbortController(),
|
|
{ signal } = controller;
|
|
let deferred = null,
|
|
reloading = false,
|
|
disposed = false;
|
|
const standalone =
|
|
matchMedia('(display-mode: standalone), (display-mode: fullscreen)').matches ||
|
|
navigator.standalone;
|
|
const ios =
|
|
/iphone|ipad|ipod/i.test(navigator.userAgent) ||
|
|
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
if (!standalone && ios) ui.patch('install', { hidden: false });
|
|
window.addEventListener(
|
|
'beforeinstallprompt',
|
|
(event) => {
|
|
event.preventDefault();
|
|
deferred = event;
|
|
ui.patch('install', { hidden: false });
|
|
},
|
|
{ signal },
|
|
);
|
|
window.addEventListener('appinstalled', () => ui.patch('install', { hidden: true }), { signal });
|
|
ui.on('install', async () => {
|
|
if (deferred) {
|
|
deferred.prompt();
|
|
await deferred.userChoice;
|
|
deferred = null;
|
|
ui.patch('install', { hidden: true });
|
|
} else if (ios) ui.open('ios-install');
|
|
});
|
|
ui.on('ios-close', () => ui.close('ios-install'));
|
|
if ('serviceWorker' in navigator && !import.meta.env.DEV) {
|
|
/** Offers a version change only after a worker has installed successfully. */
|
|
const offer = (worker) => {
|
|
ui.patch('update-toast', { hidden: false });
|
|
ui.on('update-reload', () => worker.postMessage('skip-waiting'));
|
|
};
|
|
navigator.serviceWorker
|
|
.register('./sw.js')
|
|
.then((registration) => {
|
|
if (disposed) return;
|
|
if (registration.waiting && navigator.serviceWorker.controller) offer(registration.waiting);
|
|
registration.addEventListener(
|
|
'updatefound',
|
|
() => {
|
|
const worker = registration.installing;
|
|
worker?.addEventListener(
|
|
'statechange',
|
|
() => {
|
|
if (worker.state === 'installed' && navigator.serviceWorker.controller)
|
|
offer(worker);
|
|
},
|
|
{ signal },
|
|
);
|
|
},
|
|
{ signal },
|
|
);
|
|
})
|
|
.catch(() => {});
|
|
navigator.serviceWorker.addEventListener(
|
|
'controllerchange',
|
|
() => {
|
|
if (reloading) return;
|
|
reloading = true;
|
|
location.reload();
|
|
},
|
|
{ signal },
|
|
);
|
|
}
|
|
return () => {
|
|
disposed = true;
|
|
controller.abort();
|
|
};
|
|
}
|