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=V136BK', 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=V136BK', 64 '/Common/ThirdParty/jquery/endis-jquery-ui/jquery-ui-1.10.4.custom.css?SV=V136BK', 65 '/Hubb/Assets/icons/hubb-icons.css?SV=V136BK', 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=V136BK', 69 '/Hubb/Assets/styles/app.min.css?SV=V136BK' 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 = '/Admin/Images/favicons/android-chrome-192x192.png'; 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.