1/** 2 * PWA Utilities 3 * 4 * Handles: install prompt, app badge, storage persistence, 5 * offline UI indicator, SW communication, post-update toast. 6 * 7 * Usage: 8 * import { initPwa, appBadge, installPrompt } from './pwa.js'; 9 * await initPwa(); // call once in app bootstrap 10 * appBadge.set(5); // set notification count on app icon 11 * installPrompt.prompt(); // trigger install prompt 12 */ 13 14// ============================================================================= 15// INSTALL PROMPT (beforeinstallprompt) 16// ============================================================================= 17 18let _deferredPrompt = null; 19let _installListeners = []; 20 21window.addEventListener('beforeinstallprompt', (e) => { 22 e.preventDefault(); 23 _deferredPrompt = e; 24 for (const fn of _installListeners) { 25 try { fn(true); } catch { /* listener error */ } 26 } 27}); 28 29window.addEventListener('appinstalled', () => { 30 _deferredPrompt = null; 31 console.log('[PWA] App installed'); 32 for (const fn of _installListeners) { 33 try { fn(false); } catch { /* listener error */ } 34 } 35}); 36 37/** 38 * Install prompt manager. 39 * Use `canInstall` to check availability, `prompt()` to trigger. 40 */ 41export const installPrompt = { 42 /** Whether the browser has a deferred install prompt available */ 43 get canInstall() { return !!_deferredPrompt; }, 44 45 /** 46 * Show the install prompt. Returns true if user accepted. 47 * @returns {Promise<boolean>} 48 */ 49 async prompt() { 50 if (!_deferredPrompt) return false; 51 _deferredPrompt.prompt(); 52 const { outcome } = await _deferredPrompt.userChoice; 53 _deferredPrompt = null; 54 return outcome === 'accepted'; 55 }, 56 57 /** 58 * Register a callback for install availability changes. 59 * Called with `true` when install becomes available, `false` after install. 60 * @param {(available: boolean) => void} fn 61 */ 62 onChange(fn) { 63 if (typeof fn === 'function') _installListeners.push(fn); 64 } 65}; 66 67// ============================================================================= 68// APP BADGE (notification count on app icon) 69// ============================================================================= 70 71export const appBadge = { 72 /** 73 * Set the app badge count. Pass 0 to clear. 74 * @param {number} count 75 */ 76 async set(count) { 77 if (!('setAppBadge' in navigator)) return; 78 try { 79 if (count > 0) { 80 await navigator.setAppBadge(count); 81 } else { 82 await navigator.clearAppBadge(); 83 } 84 } catch { /* badge API failure â not critical */ } 85 }, 86 87 /** Clear the app badge */ 88 async clear() { 89 if (!('clearAppBadge' in navigator)) return; 90 try { await navigator.clearAppBadge(); } catch { /* ignore */ } 91 } 92}; 93 94// ============================================================================= 95// STORAGE PERSISTENCE 96// ============================================================================= 97 98/** 99 * Request persistent storage so the browser won't evict our caches. 100 * @returns {Promise<boolean>} Whether storage is persistent 101 */ 102export async function requestPersistentStorage() { 103 if (!navigator.storage?.persist) return false; 104 const already = await navigator.storage.persisted(); 105 if (already) return true; 106 return navigator.storage.persist(); 107} 108 109/** 110 * Get current storage usage estimate. 111 * @returns {Promise<{usage: number, quota: number} | null>} 112 */ 113export async function getStorageEstimate() { 114 if (!navigator.storage?.estimate) return null; 115 return navigator.storage.estimate(); 116} 117 118// ============================================================================= 119// ONLINE / OFFLINE UI BANNER 120// ============================================================================= 121// Offline banners are handled by offline-ui.js (template-based, uses connectivity.js 122// health-ping detection). pwa.js no longer creates its own inline banner to avoid 123// duplicate/conflicting indicators. 124 125// ============================================================================= 126// INIT â call once from app.js bootstrap 127// ============================================================================= 128 129/** 130 * Initialize all PWA features. Call once during app bootstrap. 131 */ 132export async function initPwa() { 133 // --- Post-update toast --- 134 try { 135 if (sessionStorage.getItem('sb_sw_updated')) { 136 sessionStorage.removeItem('sb_sw_updated'); 137 if (window.showToast) { 138 window.showToast('App Updated', 'success', { 139 description: 'smrtBuild has been updated to the latest version.' 140 }); 141 } 142 } 143 } catch { /* sessionStorage blocked */ } 144 145 // --- Persistent storage --- 146 const persisted = await requestPersistentStorage(); 147 if (persisted) console.log('[PWA] Storage is persistent'); 148 149 // --- Connectivity banner is handled by offline-ui.js --- 150 // (uses connectivity.js health-ping, not navigator.onLine) 151 152 // --- SW push-navigate messages (deep link from notification tap) --- 153 navigator.serviceWorker?.addEventListener('message', (event) => { 154 if (event.data?.type === 'PUSH_NAVIGATE') { 155 const url = event.data.url; 156 if (typeof page === 'function') { 157 page(url); // SPA navigation via page.js 158 } else { 159 window.location.href = url; 160 } 161 } 162 }); 163 164 // --- Badge integration with notification pubsub --- 165 try { 166 const { pubsub } = await import('./utils/pubsub.js'); 167 pubsub.subscribe('notification:unreadCountChanged', ({ count }) => {
168 appBadge.set(count); 169 }); 170 } catch { /* notification system not loaded â skip badge wiring */ } 171 172 // --- Reference cache auto-refresh --- 173 // Keeps login-warmed lookups (projects / team / contacts / etc.) fresh while 174 // the app is online. Offline â no-op. Fire-and-forget the dynamic import so 175 // it doesn't block bootstrap. 176 import('./offline/reference-cache.js') 177 .then(({ referenceCache }) => referenceCache.startAutoRefresh()) 178 .catch(() => { /* reference cache unavailable â non-fatal */ }); 179 180 console.log('[PWA] Initialized'); 181}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.