1a95a5f2cb
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
Migrate the former install-only public/sw.js to a custom (injectManifest)
service worker built from ui/src/sw.ts by vite-plugin-pwa. It precaches the
app shell + hashed assets (Workbox) so an installed web PWA cold-launches with
no network, and falls in-scope navigations back to the precached shell (the
hash router resolves the route client-side). This is the C1 prerequisite for a
usable offline mode: without it a cold offline launch cannot load the bundle.
- ui/src/sw.ts: skipWaiting + clientsClaim + cleanupOutdatedCaches +
precacheAndRoute(__WB_MANIFEST) + a NavigationRoute fallback to index.html,
deny-listing the RPC path and /_gm. The Connect stream and API POSTs are
never precached nor intercepted.
- vite.config.ts: VitePWA(injectManifest); manifest:false (we ship our own),
injectRegister:false (registration stays manual + web-only in pwa.svelte.ts),
disabled in the mock build (Playwright unperturbed); landing + polyfills
excluded from the precache.
- Removed public/sw.js; the registration path (dist/sw.js at /app/) is unchanged.
- New dev deps: vite-plugin-pwa + workbox-{core,precaching,routing}.
Docs: ARCHITECTURE + gateway/README. Offline cold-launch is contour-verified
(the mock e2e harness disables the SW).
91 lines
3.7 KiB
TypeScript
91 lines
3.7 KiB
TypeScript
// Reactive PWA install runtime: captures the Chromium install prompt, tracks the installed
|
|
// state, and registers the install-only service worker. The pure platform branching lives in
|
|
// pwa.ts. Structured after the other reactive lib modules (app.svelte.ts, router.svelte.ts).
|
|
|
|
import { isIosSafari, isStandalone, resolveInstallMode, type InstallMode } from './pwa';
|
|
import { insideTelegram } from './telegram';
|
|
import { insideVK } from './vk';
|
|
|
|
/** inMiniApp reports whether the app runs inside a Telegram or VK Mini App webview, where a web
|
|
* install makes no sense (the platform manages its own launcher). Kept here, out of the pure
|
|
* pwa.ts, so that module stays free of app imports for the node unit tests. */
|
|
function inMiniApp(): boolean {
|
|
return insideTelegram() || insideVK();
|
|
}
|
|
|
|
// beforeinstallprompt is a Chromium-only event, absent from the DOM lib types.
|
|
interface BeforeInstallPromptEvent extends Event {
|
|
readonly userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
|
|
prompt(): Promise<void>;
|
|
}
|
|
declare global {
|
|
interface WindowEventMap {
|
|
beforeinstallprompt: BeforeInstallPromptEvent;
|
|
appinstalled: Event;
|
|
}
|
|
}
|
|
|
|
// Not named `state` (a Svelte-check hazard: `$state` would then read as a store subscription).
|
|
const install = $state<{ deferred: BeforeInstallPromptEvent | null; installed: boolean }>({
|
|
deferred: null,
|
|
installed: isStandalone(),
|
|
});
|
|
|
|
/**
|
|
* installMode is the reactive install-CTA mode for the current platform and any captured prompt.
|
|
* Read inside a component's markup / $derived so the CTA updates when a Chromium prompt arrives
|
|
* (beforeinstallprompt fires shortly after load) or the app is installed.
|
|
*/
|
|
export function installMode(): InstallMode {
|
|
return resolveInstallMode({
|
|
deferredAvailable: install.deferred !== null,
|
|
standalone: install.installed,
|
|
iosSafari: isIosSafari(),
|
|
inMiniApp: inMiniApp(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* initInstallListeners wires the Chromium install prompt and the installed event. Harmless on
|
|
* any platform — the events fire only in an installable browser — so it may run unconditionally
|
|
* at startup, early enough to catch a prompt that fires before the CTA mounts.
|
|
*/
|
|
export function initInstallListeners(): void {
|
|
if (typeof window === 'undefined') return;
|
|
window.addEventListener('beforeinstallprompt', (e) => {
|
|
// Suppress Chromium's mini-infobar; the app presents its own CTA instead.
|
|
e.preventDefault();
|
|
install.deferred = e;
|
|
});
|
|
window.addEventListener('appinstalled', () => {
|
|
install.deferred = null;
|
|
install.installed = true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* promptInstall shows the native Chromium install dialog. It MUST be called directly from a user
|
|
* gesture (the deferred prompt is gesture-gated — no await before it). A deferred prompt is
|
|
* single-use, so it is cleared here; a no-op when nothing was captured.
|
|
*/
|
|
export async function promptInstall(): Promise<void> {
|
|
const deferred = install.deferred;
|
|
if (!deferred) return;
|
|
install.deferred = null;
|
|
await deferred.prompt();
|
|
}
|
|
|
|
/**
|
|
* registerServiceWorker registers the app-shell service worker (built from ui/src/sw.ts to
|
|
* dist/sw.js by vite-plugin-pwa): it satisfies Chromium's installability requirement and precaches
|
|
* the shell + assets so an installed PWA cold-launches offline. Web-only: skipped in the mock build
|
|
* (a real worker would perturb the Playwright run) and inside a Telegram/VK Mini App. Best-effort —
|
|
* a failed registration only means the app is not installable and cannot launch offline.
|
|
*/
|
|
export function registerServiceWorker(): void {
|
|
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return;
|
|
if (import.meta.env.MODE === 'mock') return;
|
|
if (inMiniApp()) return;
|
|
void navigator.serviceWorker.register('sw.js').catch(() => {});
|
|
}
|