import { registerSW } from "virtual:pwa-register"; import { canUseWindow, isEnterpriseMessengerContainer, matchesMedia } from "./lib/runtimeContext"; import { PWA_UPDATE_READY_EVENT } from "./lib/pwaUpdateEvents"; const MOBILE_APPROVAL_TASK_PATH = /^\/workflow\/tasks\/([^/]+)$/i; const OFFLINE_ALLOWED_PATHS = ["/mobile/approvals"]; const CLEARABLE_CACHE_PREFIXES = ["app-api-runtime", "app-static-runtime", "workbox-precache"]; const AUTH_INPUT_PATHS = ["/login", "/change-password", "/forgot-password", "/reset-password"]; const BUILD_SHA_DISPLAY_LENGTH = 12; type ReadyResponse = { data?: { buildSha?: unknown; }; }; let buildFreshnessMonitorInstalled = false; function isAuthInputPath(pathname: string) { return AUTH_INPUT_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`)); } function bindServiceWorkerUpdateCleanup(registration: ServiceWorkerRegistration | undefined) { if (!canUseWindow() || !registration) { return; } const watchInstallingWorker = (worker: ServiceWorker | null) => { if (!worker) { return; } worker.addEventListener("statechange", () => { if (worker.state === "installed" && window.navigator.serviceWorker.controller) { void clearPwaRuntimeCaches(); } }); }; watchInstallingWorker(registration.installing); registration.addEventListener("updatefound", () => { watchInstallingWorker(registration.installing); }); } function normalizeBuildSha(value: unknown) { const normalized = String(value ?? "").trim().toLowerCase(); if ( !normalized || normalized === "unknown" || normalized === "development" || !/^[a-f0-9]{7,64}$/.test(normalized) ) { return null; } return normalized.slice(0, BUILD_SHA_DISPLAY_LENGTH); } function buildUserControlledRefresh() { return () => { void clearPwaRuntimeCaches() .catch(() => undefined) .finally(() => { window.location.reload(); }); }; } export function installBuildFreshnessMonitor() { if (!canUseWindow() || buildFreshnessMonitorInstalled) { return; } buildFreshnessMonitorInstalled = true; let checkInFlight = false; let mismatchNotified = false; const checkBuildFreshness = async () => { if (document.visibilityState !== "visible" || checkInFlight || mismatchNotified) { return; } const clientBuildSha = normalizeBuildSha(document.documentElement.dataset.buildSha); if (!clientBuildSha) { return; } checkInFlight = true; try { const response = await window.fetch("/ready", { cache: "no-store", credentials: "same-origin", headers: { accept: "application/json" }, }); if (!response.ok) { return; } const payload = await response.json() as ReadyResponse; const serverBuildSha = normalizeBuildSha(payload.data?.buildSha); if (!serverBuildSha || serverBuildSha === clientBuildSha) { return; } mismatchNotified = true; window.dispatchEvent(new CustomEvent(PWA_UPDATE_READY_EVENT, { detail: { applyUpdate: buildUserControlledRefresh() }, })); } catch { } finally { checkInFlight = false; } }; document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { void checkBuildFreshness(); } }); } export function registerPwaIfEnabled() { if (import.meta.env.DEV) { return; } if (import.meta.env.VITE_PWA_ENABLED === "false") { return; } let updateServiceWorker: ((reloadPage?: boolean) => Promise) | null = null; updateServiceWorker = registerSW({ immediate: true, onNeedRefresh() { const applyUpdate = () => { void clearPwaRuntimeCaches() .catch(() => undefined) .finally(() => { void updateServiceWorker?.(true); }); }; if (canUseWindow() && isAuthInputPath(window.location.pathname)) { window.dispatchEvent(new CustomEvent(PWA_UPDATE_READY_EVENT, { detail: { applyUpdate } })); return; } applyUpdate(); }, onOfflineReady() { if (canUseWindow()) { window.dispatchEvent(new CustomEvent("hyb-oa:pwa-offline-ready")); } }, onRegisteredSW(_swScriptUrl, registration) { bindServiceWorkerUpdateCleanup(registration); }, }); } export function buildMobileApprovalsPath(taskId?: string | null) { const normalizedTaskId = String(taskId ?? "").trim(); if (!normalizedTaskId) { return "/mobile/approvals"; } return `/mobile/approvals/${encodeURIComponent(normalizedTaskId)}`; } export function extractWorkflowTaskId(pathname: string) { const match = pathname.match(MOBILE_APPROVAL_TASK_PATH); return match?.[1] ?? null; } export function isOfflineSnapshotAllowedPath(pathname: string) { if (OFFLINE_ALLOWED_PATHS.includes(pathname)) { return true; } if (/^\/mobile\/approvals\/[^/]+$/.test(pathname)) { return true; } return Boolean(extractWorkflowTaskId(pathname)); } export function shouldPreferMobileApprovalExperience() { if (!canUseWindow()) { return false; } const isStandalone = matchesMedia("(display-mode: standalone)") || (window.navigator as Navigator & { standalone?: boolean }).standalone === true; const isTouchSmallScreen = window.navigator.maxTouchPoints > 0 && matchesMedia("(max-width: 1024px)"); return isStandalone || isEnterpriseMessengerContainer() || isTouchSmallScreen; } export async function clearPwaRuntimeCaches() { if (!canUseWindow() || !("caches" in window)) { return; } const cacheNames = await window.caches.keys(); const cachesToDelete = cacheNames.filter((cacheName) => CLEARABLE_CACHE_PREFIXES.some((prefix) => cacheName.startsWith(prefix)), ); await Promise.all(cachesToDelete.map(async (cacheName) => { try { await window.caches.delete(cacheName); } catch { } })); }