PageSourceSearch

https://smrtbld.ai/js/pwa.js

js smrtbld.ai collected 2026-10-05 15:07:02 UTC 6,354 bytes, 181 lines download raw bytes

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.