1 //push 2'use strict'; 3 4// load indexedDb helper library 5importScripts('/js/split/idbStorage.js'); 6 7// Push messaging logic 8self.Pusher = { 9 10 _recentlyOpenedNotifications: {}, 11 12 init() { 13 console.debug('[Service Worker]: Pusher init '); 14 // register SW event listeners 15 this._initServiceWorkerEvents(); 16 }, 17 18 _initServiceWorkerEvents() { 19 // Messages from controlled pages 20 21 // SW lifecycle 22 self.addEventListener('install', event => { 23 self.skipWaiting(); 24 }); 25 26 self.addEventListener('activate', event => { 27 event.waitUntil(self.clients.claim()); 28 }); 29 30 // Push event from FCM 31 self.addEventListener('push', event => { 32 event.waitUntil(this._handlePush(event)); 33 }); 34 35 self.addEventListener('notificationclick', event => { 36 event.waitUntil(this._handleNotificationClick(event)); 37 }); 38 39 self.addEventListener('notificationclose', event => { 40 event.waitUntil(this._handleNotificationClose(event)); 41 }); 42 }, 43 44 async sendTrackerEvent(status, extraJson = null) { 45 if (!status) { 46 console.error('[Service Worker]: sendTrackerEvent called without status'); 47 return; 48 } 49 50 if (!(['opened', 'closed', 'received', 'shown'].includes(status))) { 51 console.error('[Service Worker]: sendTrackerEvent called with invalid status: ' + status); 52 return; 53 } 54 55 const storedUserData = await self.idbStorage.getPusherUserData() || {}; 56 57 const fcmToken = storedUserData.fcm_token; 58 if (!fcmToken) { 59 console.error('[Service Worker]: fcmToken not set in idbStorage, cannot send tracker event'); 60 return; 61 } 62 63 const trackerUrl = storedUserData.trackerEventUrl; 64 if (!trackerUrl) { 65 console.error('[Service Worker]: trackerEventUrl not set in idbStorage, cannot send tracker event'); 66 return; 67 } 68 69 const userUid = storedUserData.uid; 70 if (!userUid) { 71 console.error('[Service Worker]: uid not set in idbStorage, cannot send tracker event'); 72 return; 73 } 74 75 // Normalize extra_json 76 let extraJsonPayload = '{}'; 77 let brochureId = null; 78 let brochureSef = null; 79 let shopId = null; 80 let shopSef = null; 81 let createType = null; 82 let utmCampaign = null; 83 if (extraJson && typeof extraJson === 'object') { 84 try { 85 extraJsonPayload = JSON.stringify({ 86 taskId: extraJson.task_id ?? null 87 }); 88 89 brochureId = this.toIntOrNull(extraJson.brochure_id ?? null); 90 brochureSef = extraJson.brochure_sef ?? null; 91 shopId = this.toIntOrNull(extraJson.shop_id ?? null); 92 shopSef = extraJson.shop_sef ?? null; 93 createType = extraJson.create_type ?? null; 94 utmCampaign = extraJson.utm_campaign ?? null; 95 96 } catch (e) { 97 console.warn('[Service Worker]: Failed to stringify extra_json', e); 98 } 99 } 100 101 const payload = { 102 uid: storedUserData.uid, 103 fcm_token: storedUserData.fcm_token, 104 fcm_token_changed: !!storedUserData.fcm_token_changed, 105 status: status, 106 ip: '', 107 ua: storedUserData.ua || '', 108 local_time: this.getLocalDateTimeString(), 109 brochure_id: brochureId, 110 brochure_sef: brochureSef, 111 shop_id: shopId, 112 shop_sef: shopSef, 113 create_type: createType, 114 utm_campaign: utmCampaign, 115 extra_json: extraJsonPayload 116 }; 117 118 console.debug('[Service Worker]: Sending tracker event', payload); 119 120 try { 121 const res = await fetch(trackerUrl, { 122 method: 'POST', 123 headers: { 124 'Content-Type': 'application/json' 125 }, 126 body: JSON.stringify(payload) 127 }); 128 129 if (!res.ok) { 130 console.error( 131 '[Service Worker]: Tracker event failed', 132 res.status, 133 res.statusText 134 ); 135 } 136 } catch (err) { 137 console.error('[Service Worker]: Tracker event fetch error', err); 138 } 139 }, 140 141 getLocalDateTimeString() { 142 const d = new Date(); 143 const pad = n => String(n).padStart(2, '0'); 144 return ( 145 d.getFullYear() + '-' + 146 pad(d.getMonth() + 1) + '-' + 147 pad(d.getDate()) + ' ' + 148 pad(d.getHours()) + ':' + 149 pad(d.getMinutes()) + ':' + 150 pad(d.getSeconds()) 151 ); 152 }, 153 154 toIntOrNull(value) { 155 if (value === null || value === undefined || value === '') { 156 return null; 157 }
158 const n = Number(value); 159 return Number.isInteger(n) && n > 0 ? n : null; 160 }, 161 162 /** 163 * Expected FCM data payload: 164 * { 165 * "data": { 166 * "project_id": "", 167 * "device_id": "394", 168 * "store": "dm drogerie", 169 * "fcm_token": "dqhoFZz_IDw679Pmd5Haoo:APA91bGjbwcspK-SjfBJqGivr7XgzapJ9V3IlgfCTNXm0bOR06M_d8r_acRmNwOOn7sHNY_8qXAZ7eUr1eT-Mk0_gUQLhSJP3tt2d4c7UI7SwR9c691FhQ0", 170 * "user_id": "1", 171 * "send_mode": "single", 172 * "task_id": "fb91a28dfca07807", 173 * "dedupe_key": "bcec8a886ec7a229b6b5a57a40603dbe7c64471847366f812987022ea95dcbc5", 174 * "channel": "push", 175 * "source": "worker_auto", 176 * "message_key": "msg_69b2bb05_b7544d9b_5fc385062c587686", 177 * "click_action": "https://www.letakomat.sk/dm/", 178 * "style": "promo" 179 * }, 180 * "from": "778037625297", 181 * "priority": "normal", 182 * "notification": { 183 * "title": "ð Nedeľná rodinná akcia v dm! ð", 184 * "body": "NavÅ¡tÃvte nás v Žiline od 1. do 7. júla a užite si Å¡peciálne zľavy! NezmeÅ¡kajte to!", 185 * "icon": "https://review-smartpush.vpn.prod.ams3.k8s.hyperia.sk/assets/lidl-logo.png", 186 * "click_action": "https://www.letakomat.sk/dm/" 187 * }, 188 * "fcmMessageId": "00c72ab9-62f4-446c-b42b-529adfd38323" 189 * } 190 */ 191 async _handlePush(event) { 192 if (!event.data) { 193 console.error('[Service Worker]: No push event data provided'); 194 return; 195 } 196 197 let responseData = {}; 198 199 try { 200 responseData = event.data.json(); 201 console.debug('[Service Worker]: push event', responseData); 202 } catch (e) { 203 console.error('[Service Worker]: push payload not JSON', e); 204 return; 205 } 206 207 const notification = responseData.notification || {}; 208 const data = responseData.data || {}; 209 210 const title = notification.title || 'Notification'; 211 const body = notification.body || ''; 212 const icon = notification.icon || '/img/maskot/pwa/icon180_maskable.png'; 213 const image = notification.image || undefined; 214 const url = data.click_action || notification.click_action || self.location.origin; 215 216 const options = { 217 body, 218 icon, 219 image, 220 data: { 221 ...data, 222 url 223 }, 224 requireInteraction: true 225 }; 226 227 await this.sendTrackerEvent('received', data); 228 229 await self.registration.showNotification(title, options); 230 231 await this.sendTrackerEvent('shown', data); 232 }, 233 234 async _handleNotificationClick(event) { 235 console.debug('[Service Worker]: notificationclick', event.notification, event.action); 236 event.notification.close(); 237 238 const data = event.notification?.data || {}; 239 const finalUrl = data.url || self.location.origin; 240 241 // we need to remember opening notification, so we block close event to be sent to tracker 242 const messageKey = data.message_key || null; 243 if (messageKey) { 244 this._recentlyOpenedNotifications[messageKey] = Date.now(); 245 } 246 247 await this.sendTrackerEvent('opened', data); 248 249 const allClients = await self.clients.matchAll({ 250 type: 'window', 251 includeUncontrolled: true 252 }); 253 254 const targetOrigin = new URL(finalUrl).origin; 255 let client = allClients.find(c => { 256 try { 257 return new URL(c.url).origin === targetOrigin; 258 } catch (e) { 259 return false; 260 } 261 }); 262 263 if (client) { 264 await client.focus(); 265 if (client.url !== finalUrl && 'navigate' in client) { 266 await client.navigate(finalUrl); 267 } 268 return; 269 } 270 271 await self.clients.openWindow(finalUrl); 272 }, 273 274 async _handleNotificationClose(event) { 275 console.debug('[Service Worker]: notificationclose', event.notification); 276 277 const data = event.notification?.data || {}; 278 const messageKey = data.message_key || null; 279 280 // if notification was opened, we do not send closed event to tracker 281 if (messageKey && this._recentlyOpenedNotifications[messageKey]) { 282 console.debug('[Service Worker]: skipping closed tracker because notification was opened'); 283 delete this._recentlyOpenedNotifications[messageKey]; 284 return; 285 } 286 287 await this.sendTrackerEvent('closed', data); 288 } 289}; 290 291self.Pusher.init(); 292 293// PWA logic
294importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js'); 295if (typeof workbox !== 'undefined') { 296 297 // under optimization 298 workbox.precaching.addPlugins([ 299 new workbox.cacheableResponse.Plugin({ 300 statuses: [0, 200] 301 }) 302 ]); 303 workbox.precaching.precacheAndRoute([{"url":"./?utm_source=ofertero_es_pwa&utm_medium=pwa","revision":"b975d50455bda765b68f3142496b6988"}]); 304 305 /* static files cache */ 306 workbox.routing.registerRoute( 307 new RegExp('manifest\.json|favicon\.ico'), 308 workbox.strategies.networkFirst({ 309 cacheName: 'static-files-cache', 310 }) 311 ); 312 313 /* external files cache */ 314 workbox.routing.registerRoute( 315 new RegExp('.*\/analytics\.js|.*\/gtm\.js|.*\/bootstrap\.min\.js'), 316 workbox.strategies.networkFirst({ 317 cacheName: 'external-files-cache', 318 }) 319 ); 320 321 /* Cache CSS files */ 322 workbox.routing.registerRoute( 323 new RegExp('.*\.css'), 324 // Use cache but update in the background ASAP 325 workbox.strategies.staleWhileRevalidate({ 326 cacheName: 'css-cache', 327 plugins: [ 328 new workbox.cacheableResponse.Plugin({ 329 statuses: [0, 200] 330 }) 331 ], 332 }) 333 ); 334 335 /* Cache CSS files */ 336 workbox.routing.registerRoute( 337 new RegExp('.*\.leafletscdn\.com.*\.js'), 338 // Use cache but update in the background ASAP 339 workbox.strategies.staleWhileRevalidate({ 340 cacheName: 'js-cache', 341 plugins: [ 342 new workbox.cacheableResponse.Plugin({ 343 statuses: [0, 200] 344 }) 345 ], 346 }) 347 ); 348 /* Cache leaflet pages - with timestamp if it is there*/ 349 workbox.routing.registerRoute( 350 new RegExp('.*\/data\/[0-9]*\/[0-9]*\/.*\.(?:png|jpg|jpeg|svg|gif)'), 351 workbox.strategies.cacheFirst({ 352 cacheName: 'brochure-image-cache', 353 plugins: [ 354 new workbox.expiration.Plugin({ 355 maxEntries: 800, 356 maxAgeSeconds: 31 * 24 * 60 * 60, 357 }), 358 new workbox.cacheableResponse.Plugin({ 359 statuses: [0, 200] 360 }) 361 ], 362 }) 363 ); 364 365 /* Cache shops logo */ 366 workbox.routing.registerRoute( 367 new RegExp('.*\/data\/[0-9]*\/logo_.{1,2}\.(?:png|jpg|jpeg|svg|gif)'), 368 workbox.strategies.cacheFirst({ 369 cacheName: 'shop-image-cache', 370 plugins: [ 371 new workbox.expiration.Plugin({ 372 maxAgeSeconds: 31 * 24 * 60 * 60, 373 }), 374 new workbox.cacheableResponse.Plugin({ 375 statuses: [0, 200] 376 }) 377 ], 378 }) 379 ); 380 /* Cache static web img */ 381 workbox.routing.registerRoute( 382 new RegExp('.*\/img\/.*\.(?:png|jpg|jpeg|svg|gif)'), 383 workbox.strategies.cacheFirst({ 384 cacheName: 'static-image-cache', 385 plugins: [ 386 new workbox.expiration.Plugin({ 387 maxAgeSeconds: 31 * 24 * 60 * 60, 388 }), 389 new workbox.cacheableResponse.Plugin({ 390 statuses: [0, 200] 391 }) 392 ], 393 }) 394 ); 395 396 }
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.