PageSourceSearch

https://www.stjohnschelsea.org/serviceworker.js

js stjohnschelsea.org collected 2026-10-09 01:28:55 UTC 16,450 bytes, 394 lines download raw bytes

1
2importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');
3
4// VAPID public key for push notification resubscription
5self.vapidPublicKey = 'BLGkZ8bo-pdGJpsHMeZ6r1_JBNg0nCF0RUX6wJvx18kQfJmFMAHEfUuhwqK8n3KvVm00asoK-D4yhc97ZdA01zs';
6
7// ============================================================================
8// WORKBOX CONFIGURATION - Caching & Offline Support
9// ============================================================================
10
11var FALLBACK_HTML_URL = '/hubb/offline.aspx';
12var FALLBACK_IMAGE_URL = '/admin/images/image_unavailable.png';
13var FALLBACK_FONT_URL = '/admin/assets/thirdparty/materialize/font/roboto/roboto-regular.woff';
14
15// Skip waiting and claim clients immediately
16workbox.core.skipWaiting();
17workbox.core.clientsClaim();
18
19// ============================================================================
20// CHAT API - No SW caching (uses IndexedDB via ChatOffline module instead)
21// ============================================================================
22// Chat API responses contain user-specific data. The ChatOffline module (in app.js) uses
23// IndexedDB with user-specific cache keys (threads_${siteId}_${usrId}) for proper
24// isolation. SW caching would be keyed by URL alone, creating a privacy risk on
25// shared devices where one user could see another's cached data.
26
27// Explicitly use NetworkOnly for Chat API to prevent any caching
28workbox.routing.registerRoute(
29    /\/hubb\/api\/chat\.aspx/i,
30    new workbox.strategies.NetworkOnly()
31);
32
33// Navigations to app pages: network-first, but fail fast to the precached shell if the network
34// stalls (5s). Scoped to navigations so slow API .aspx calls are NOT forced onto stale cache by
35// the timeout (see #1555 / CodeRabbit).
36workbox.routing.registerRoute(
37    ({ request, url }) => request.mode === 'navigate' && /\/hubb\/.*\.aspx/i.test(url.pathname),
38    new workbox.strategies.NetworkFirst({ networkTimeoutSeconds: 5 })
39);
40// Non-navigation /hubb/*.aspx (e.g. /Hubb/API/UnreadCount.aspx): unchanged network-first, no
41// aggressive timeout (preserves pre-#1555 behaviour).
42workbox.routing.registerRoute(
43    /\/hubb\/.*\.aspx/i,
44    new workbox.strategies.NetworkFirst()
45);
46
47workbox.precaching.precacheAndRoute([
48    FALLBACK_HTML_URL,
49    FALLBACK_FONT_URL,
50    FALLBACK_IMAGE_URL,
51    '/Admin/Images/Branding/HubbChurch-Logo-Dark.png',
52    '/Admin/Images/favicons/apple-touch-icon.png',
53    '/Admin/Images/favicons/browserconfig.xml',
54    '/Admin/Images/favicons/favicon.ico',
55    '/Admin/Images/favicons/favicon-32x32.png',
56    '/Admin/Images/favicons/favicon-16x16.png',
57    '/Admin/Images/favicons/safari-pinned-tab.svg',
58    '/Admin/Assets/ThirdParty/footable3/footable.standalone.min.css',
59    '/Admin/Assets/ThirdParty/jquery/jquery-and-plugins.min.js?SV=V136BM',
60    '/Admin/Assets/ThirdParty/material-icons/material-icons.css',
61    '/Admin/Assets/ThirdParty/Materialize/font/material-design-icons/material-design-icons.woff2',
62    '/Admin/Assets/ThirdParty/Materialize/font/material-design-icons/material-design-icons.woff',
63    '/Admin/Assets/ThirdParty/Materialize/js/materialize.min.js?SV=V136BM',
64    '/Common/ThirdParty/jquery/endis-jquery-ui/jquery-ui-1.10.4.custom.css?SV=V136BM',
65    '/Hubb/Assets/icons/hubb-icons.css?SV=V136BM',
66    '/Hubb/Assets/icons/fonts/hubb-icons.ttf?3k4c4j',
67    '/Hubb/Assets/icons/fonts/hubb-icons.woff?3k4c4j',
68    '/Hubb/Assets/js/app.min.js?SV=V136BM',
69    '/Hubb/Assets/styles/app.min.css?SV=V136BM'
70    
71    ],
72    {
73        cleanUrls: false,
74        directoryIndex: 'default.aspx'
75    }
76);
77
78// Cache JS, CSS, fonts, and images with stale-while-revalidate
79workbox.routing.registerRoute(
80    /\/hubb\/.*\.(?:js|css|woff|woff2|png|jpg|jpeg|svg|gif)$/i,
81    // Use cache but update in the background.
82    new workbox.strategies.StaleWhileRevalidate()
83);
84
85// This route will go against the network if there isn't a cache match,
86// but it won't populate the cache at runtime.
87// If there is a cache match, then it will properly serve partial responses.
88workbox.routing.registerRoute(
89    /.*\.(?:mp3|mp4)$/i,
90    new workbox.strategies.CacheFirst({
91        cacheName: 'media',
92        plugins: [
93            new workbox.cacheableResponse.Plugin({ statuses: [200] }),
94            new workbox.rangeRequests.Plugin(),
95            new workbox.expiration.Plugin({
96                // Keep at most 50 entries.
97                maxEntries: 50,
98                // Don't keep any entries for more than 60 days.
99                maxAgeSeconds: 60 * 24 * 60 * 60,
100                // Automatically cleanup if quota is exceeded.
101                purgeOnQuotaError: true,
102            }),
103        ],
104    }),
105);
106
107// Use a stale-while-revalidate strategy for all other hubb requests.
108workbox.routing.registerRoute(
109    /\/hubb\/.*/i,
110    new workbox.strategies.StaleWhileRevalidate()
111);
112
113// Use a stale-while-revalidate strategy for all other Admin/Asset requests.
114workbox.routing.registerRoute(
115    /\/admin\/assets\/.*/i,
116    new workbox.strategies.StaleWhileRevalidate()
117);
118
119// This "catch" handler is triggered when any of the other routes fail to
120// generate a response.
121workbox.routing.setCatchHandler(({ event }) => {
122    // The FALLBACK_URL entries must be added to the cache ahead of time, either via runtime
123    // or precaching.
124    // If they are precached, then call workbox.precaching.getCacheKeyForURL(FALLBACK_URL)
125    // to get the correct cache key to pass in to caches.match().
126    //
127    // Use event, request, and url to figure out how to respond.
128    // One approach would be to use request.destination, see
129    // https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c
130    switch (event.request.destination) {
131        case 'document':
132            return caches.match(workbox.precaching.getCacheKeyForURL(FALLBACK_HTML_URL));
133
134        case 'image':
135            return caches.match(workbox.precaching.getCacheKeyForURL(FALLBACK_IMAGE_URL));
136
137        case 'font':
138            return caches.match(workbox.precaching.getCacheKeyForURL(FALLBACK_FONT_URL));
139
140        default:
141            // If we don't have a fallback, just return an error response.
142            return Response.error();
143    }
144});
145
146// ============================================================================
147// PUSH NOTIFICATIONS
148// ============================================================================
149
150// App icon for notifications (uses site's PWA icon if configured, otherwise default)
151var NOTIFICATION_ICON = '/Images/Content/3507/1351847.png?modify_dt=638652777588400000';
152var NOTIFICATION_BADGE = '/Admin/Images/favicons/favicon-32x32.png';
153
154// Handle incoming push notifications
155self.addEventListener('push', function(event) {
156    console.log('[ServiceWorker] Push Received:', event);
157
158    let notificationData = {
159        title: 'New Message',
160        body: 'You have a new notification',
161        icon: NOTIFICATION_ICON,
162        badge: NOTIFICATION_BADGE,
163        tag: 'hubb-notification',
164        requireInteraction: false,
165        data: {
166            url: '/Hubb/Default.aspx'
167        }
168    };
169
170    if (event.data) {
171        try {
172            const payload = event.data.json();
173            notificationData = {
174                title: payload.title || notificationData.title,
175                body: payload.body || notificationData.body,
176                icon: payload.icon || notificationData.icon,
177                badge: payload.badge || notificationData.badge,
178                tag: payload.tag || notificationData.tag,
179                requireInteraction: payload.requireInteraction || false,
180                data: {
181                    url: payload.url || notificationData.data.url,
182                    mailId: payload.mailId,
183                    recipientId: payload.recipientId
184                }
185            };
186        } catch (e) {
187            notificationData.body = event.data.text();
188        }
189    }
190
191    const promiseChain = self.registration.showNotification(
192        notificationData.title,
193        {
194            body: notificationData.body,
195            icon: notificationData.icon,
196            badge: notificationData.badge,
197            tag: notificationData.tag,
198            requireInteraction: notificationData.requireInteraction,
199            data: notificationData.data
200        }
201    ).then(function() {
202        // Notify any open clients so they can update their UI (e.g. inbox list)
203        return clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(clientList) {
204            clientList.forEach(function(client) {
205                client.postMessage({
206                    type: 'PUSH_RECEIVED',
207                    title: notificationData.title,
208                    body: notificationData.body,
209                    mailId: notificationData.data.mailId,
210                    url: notificationData.data.url
211                });
212            });
213        });
214    }).then(function() {
215        // Update the PWA badge after showing notification
216        return updateBadge();
217    });
218
219    event.waitUntil(promiseChain);
220});
221
222// True if the URL is the top-level app shell (Default.aspx or the /Hubb/ root) on
223// THIS origin, as opposed to an inner iframe page like /Hubb/Me/ChatThread.aspx.
224// The origin check is required: a path-only test would treat a cross-origin URL such
225// as https://evil.example/Hubb/Default.aspx as trusted, opening a redirect vector.
226function isShellUrl(url) {
227    try {
228        var u = new URL(url);
229        if (u.origin !== self.location.origin) return false;
230        var p = u.pathname.toLowerCase();
231        return p === '/hubb/' || p === '/hubb' || p === '/hubb/default.aspx';
232    } catch (e) {
233        return false;
234    }
235}
236
237// Normalise a notification target to a shell-hosted URL. Notification payloads use two
238// shapes: shell URLs (e.g. /Hubb/Default.aspx?p=Inbox&view_mail_id=123, used by mail)
239// and inner pages (e.g. /Hubb/Me/ChatThread.aspx?thread_id=45, used by chat). We always
240// route through the app shell: shell URLs pass through; inner pages are wrapped in the
241// shell's ?d= base64 deep-link param so the shell loads them into the correct iframe
242// (fraChat/fraContent) rather than opening the bare page outside the app chrome.
243function toShellUrl(target) {
244    var fallback = new URL('/Hubb/Default.aspx', self.location.origin).href;
245    try {
246        var u = new URL(target, self.location.origin);
247        // Never route to another origin — fall back to the safe default shell URL.
248        if (u.origin !== self.location.origin) {
249            return fallback;
250        }
251        if (isShellUrl(u.href)) {
252            return u.href;
253        }
254        return self.location.origin + '/Hubb/Default.aspx?d=' + btoa(u.href);
255    } catch (e) {
256        return fallback;
257    }
258}
259
260// Handle notification click
261self.addEventListener('notificationclick', function(event) {
262    console.log('[ServiceWorker] Notification click received:', event);
263
264    event.notification.close();
265
266    var notificationData = event.notification.data || {};
267    var shellUrl = toShellUrl(notificationData.url || '/Hubb/Default.aspx');
268
269    event.waitUntil(
270        clients.matchAll({
271            type: 'window',
272            includeUncontrolled: true
273        }).then(function(clientList) {
274            // Only act on the top-level app shell window. matchAll also returns the inner
275            // iframe documents (fraChat/fraContent); focusing or navigating those is
276            // unreliable, and on iOS navigating an iframe client nests the whole shell
277            // inside the frame (the "doubled header" report), so we never target them.
278            for (const client of clientList) {
279                if (isShellUrl(client.url) && 'focus' in client) {
280                    return client.focus().then(function(focused) {
281                        var target = focused || client;
282                        // Drive the navigation through the page's SW_NAVIGATE handler
283                        // (location.href assignment). That reliably reloads even for a
284                        // query-only change, which WindowClient.navigate() does NOT
285                        // guarantee on iOS WebKit — the cause of the non-deterministic
286                        // deep-linking on iOS.
287                        target.postMessage({ type: 'SW_NAVIGATE', url: shellUrl });
288                        return target;
289                    });
290                }
291            }
292
293            // No shell window open — open a fresh one at the shell-routed target.
294            return clients.openWindow(shellUrl);
295        }).then(function() {
296            // Update badge after a short delay to allow the message to be marked as read
297            return new Promise(function(resolve) { setTimeout(resolve, 1000); });
298        }).then(function() {
299            return updateBadge();
300        }).catch(function(error) {
301            // Last resort fallback - try to open window directly
302            console.error('[ServiceWorker] Error handling notification click:', error);
303            return clients.openWindow(shellUrl);
304        })
305    );
306});
307
308// Handle notification close
309self.addEventListener('notificationclose', function(event) {
310    console.log('[ServiceWorker] Notification closed:', event);
311});
312
313// ============================================================================
314// PWA BADGE - Unread Count
315// ============================================================================
316
317// Update the PWA app badge with unread count
318async function updateBadge() {
319    // Check if Badge API is supported
320    if (!('setAppBadge' in navigator)) {
321        console.log('[ServiceWorker] Badge API not supported');
322        return;
323    }
324
325    try {
326        const response = await fetch('/Hubb/API/UnreadCount.aspx', {
327            method: 'GET',
328            credentials: 'same-origin'
329        });
330
331        if (response.ok) {
332            const data = await response.json();
333            const count = data.count || 0;
334
335            if (count > 0) {
336                await navigator.setAppBadge(count);
337                console.log('[ServiceWorker] Badge set to:', count);
338            } else {
339                await navigator.clearAppBadge();
340                console.log('[ServiceWorker] Badge cleared');
341            }
342        }
343    } catch (error) {
344        console.log('[ServiceWorker] Error updating badge:', error);
345    }
346}
347
348// Listen for messages from the main thread to update badge
349self.addEventListener('message', function(event) {
350    if (event.data && event.data.type === 'UPDATE_BADGE') {
351        updateBadge();
352    }
353});
354
355// Convert base64url VAPID key to Uint8Array for pushManager.subscribe
356function urlBase64ToUint8Array(base64String) {
357    var padding = '='.repeat((4 - base64String.length % 4) % 4);
358    var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
359    var rawData = atob(base64);
360    var outputArray = new Uint8Array(rawData.length);
361    for (var i = 0; i < rawData.length; ++i) {
362        outputArray[i] = rawData.charCodeAt(i);
363    }
364    return outputArray;
365}
366
367// Handle push subscription change (e.g., when subscription expires)
368self.addEventListener('pushsubscriptionchange', function(event) {
369    console.log('[ServiceWorker] Push subscription changed');
370
371    if (!self.vapidPublicKey) {
372        console.error('[ServiceWorker] VAPID public key not available for resubscription');
373        return;
374    }
375
376    var applicationServerKey = urlBase64ToUint8Array(self.vapidPublicKey);
377    event.waitUntil(
378        self.registration.pushManager.subscribe({
379            userVisibleOnly: true,
380            applicationServerKey: applicationServerKey
381        }).then(function(subscription) {
382            return fetch('/Hubb/API/PushSubscription.aspx', {
383                method: 'POST',
384                headers: {
385                    'Content-Type': 'application/json'
386                },
387                body: JSON.stringify({
388                    action: 'subscribe',
389                    subscription: subscription.toJSON()
390                })
391            });
392        })
393    );
394});

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.