1// Firebase Cloud Messaging Service Worker 2// 3// GENERATED FILE â do not edit. Source: scripts/generate-firebase-sw.js 4// Regenerated on every build (prebuild/build/dev_build/stg_build) and by `npm run dev`. 5importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js'); 6importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js'); 7 8// Activate immediately and take control of pages that are already open. 9// This worker is registered lazily at runtime (when the FCM token is created), 10// so without clients.claim() the tabs that are already open stay *uncontrolled* 11// â and WindowClient.navigate() rejects with a TypeError on an uncontrolled 12// client. That is why a notification click did nothing while the site was open 13// but worked fine when it had to open a brand new window. 14self.addEventListener('install', () => { 15 self.skipWaiting(); 16}); 17 18self.addEventListener('activate', (event) => { 19 event.waitUntil(self.clients.claim()); 20}); 21 22// Notifications rendered by the FCM SDK itself â which happens for any push 23// whose payload carries a `notification` block â nest the real message under an 24// FCM_MSG envelope rather than exposing our data bag directly. Unwrap both 25// shapes so either kind of notification routes the same way. 26function unwrapNotificationData(data) { 27 const envelope = data && data['FCM_MSG']; 28 if (!envelope) { 29 return data || {}; 30 } 31 32 return { 33 ...(envelope.data || {}), 34 click_action: 35 envelope.data?.click_action || 36 envelope.fcmOptions?.link || 37 envelope.notification?.click_action || 38 '/', 39 }; 40} 41 42// Map a notification data bag to the in-app URL it should open. 43// Keep in sync with resolveNotificationPath() in src/lib/notificationRoute.ts, 44// which the in-page toast uses for the same payloads. 45function resolveNotificationUrl(rawData) { 46 const data = unwrapNotificationData(rawData); 47 const type = data?.type || 'home'; 48 const urlKey = data?.url_key || ''; 49 const clickAction = data?.click_action || '/'; 50 51 let target; 52 switch (type) { 53 case 'movies': 54 case 'movie_details': 55 target = urlKey ? `/movies/${urlKey}` : clickAction; 56 break; 57 case 'events': 58 case 'event_details': 59 target = urlKey ? `/events/${urlKey}` : clickAction; 60 break; 61 case 'promotions': 62 target = urlKey ? `/promotions/${urlKey}` : clickAction; 63 break; 64 case 'promotion_details': 65 target = urlKey ? `/promotion/${urlKey}` : clickAction; 66 break; 67 case 'home': 68 default: 69 target = clickAction || '/'; 70 break; 71 } 72 73 // click_action can arrive as an absolute URL. Normalise everything to a 74 // same-origin URL object and fall back home for anything off-origin or 75 // malformed, so a bad payload can never navigate the user off the site. 76 try { 77 const resolved = new URL(target, self.location.origin); 78 if (resolved.origin !== self.location.origin) { 79 return new URL('/', self.location.origin); 80 } 81 return resolved; 82 } catch (err) { 83 return new URL('/', self.location.origin); 84 } 85} 86 87// Ask an open page to route client-side. Resolves true only when the page 88// acknowledges, so the caller can fall back to a hard navigation otherwise 89// (e.g. a stale page without the message listener). 90function requestClientRoute(client, path) { 91 return new Promise((resolve) => { 92 let settled = false; 93 const finish = (handled) => { 94 if (settled) return; 95 settled = true; 96 resolve(handled); 97 }; 98 99 try { 100 const channel = new MessageChannel(); 101 channel.port1.onmessage = (event) => finish(event.data?.handled === true); 102 setTimeout(() => finish(false), 400); 103 client.postMessage({ type: 'NOTIFICATION_CLICK', path: path }, [channel.port2]); 104 } catch (err) { 105 finish(false); 106 } 107 }); 108} 109 110// Handle notification clicks. 111// 112// IMPORTANT: this must be registered *before* firebase.messaging() below. The 113// SDK registers its own notificationclick listener at that call, and for 114// notifications it rendered itself it calls stopImmediatePropagation() â so a 115// listener added afterwards never runs. Its handler then navigates only if the 116// payload set fcmOptions.link / notification.click_action, and otherwise does 117// nothing at all. Registering first lets us route every notification with the 118// same type/url_key rules, and we stop propagation ourselves so the SDK's 119// handler can't also act on it. 120self.addEventListener('notificationclick', (event) => { 121 122 event.stopImmediatePropagation(); 123 event.notification.close(); 124 125 const targetUrl = resolveNotificationUrl(event.notification.data); 126 const targetPath = targetUrl.pathname + targetUrl.search + targetUrl.hash; 127 128 event.waitUntil((async () => { 129 // matchAll with includeUncontrolled matches on origin, not scope, so
130 // this finds the page even from a worker that controls nothing. 131 const clientList = await self.clients.matchAll({ 132 type: 'window', 133 includeUncontrolled: true, 134 }); 135 136 const client = 137 clientList.find((c) => c.focused) || 138 clientList.find((c) => c.visibilityState === 'visible') || 139 clientList[0]; 140 141 if (client) { 142 // focus() is always permitted from within a notificationclick handler. 143 try { 144 await client.focus(); 145 } catch (err) { 146 // Non-fatal: keep going and still route the window. 147 } 148 149 // Preferred: let the loaded app route itself (no reload, keeps state). 150 if (await requestClientRoute(client, targetPath)) { 151 return; 152 } 153 154 // Fallback: hard navigation. Only legal for a client this worker 155 // controls, so a rejection drops through to opening a new window. 156 try { 157 await client.navigate(targetUrl.href); 158 return; 159 } catch (err) { 160 // Uncontrolled client â fall through. 161 } 162 } 163 164 if (self.clients.openWindow) { 165 await self.clients.openWindow(targetUrl.href); 166 } 167 })()); 168}); 169 170// Initialize Firebase in the service worker 171// Note: These values are injected during build time 172firebase.initializeApp({ 173 apiKey: "", 174 authDomain: "", 175 projectId: "", 176 storageBucket: "", 177 messagingSenderId: "", 178 appId: "" 179}); 180 181const messaging = firebase.messaging(); 182 183// Handle background messages 184messaging.onBackgroundMessage((payload) => { 185 186 // When the push payload carries a `notification` block, the SDK's own push 187 // handler has already called showNotification() by the time this runs â its 188 // comment reads "display if possible *and* pass to onBackgroundMessage 189 // hook" â and then still invokes this one. Rendering again here is what 190 // made every notification appear twice on web. externalizePayload() only 191 // sets payload.notification when that block was present, so this is an 192 // exact test; bail out and let the SDK-rendered one stand, which the click 193 // handler above routes by unwrapping its FCM_MSG envelope. 194 // 195 // Note this is web-only behaviour: the native SDKs do not call their 196 // message handler for notification payloads received in the background, 197 // which is why the mobile apps never showed a duplicate for the same push. 198 // 199 // Data-only payloads skip this branch and get the full treatment below. 200 if (payload.notification) { 201 return; 202 } 203 204 const notificationTitle = payload.data?.title || 'New Notification'; 205 const notificationOptions = { 206 body: payload.data?.body || '', 207 icon: payload.data?.icon || '/assets/icons/icon-192x192.png', 208 image: payload.data?.image || '', 209 badge: '/assets/icons/icon-72x72.png', 210 tag: payload.data?.tag || 'default', 211 data: { 212 click_action: payload.data?.click_action || '/', 213 type: payload.data?.type || 'home', 214 id: payload.data?.id || '', 215 url_key: payload.data?.url_key || '', 216 }, 217 requireInteraction: false, 218 vibrate: [200, 100, 200], 219 }; 220 221 return self.registration.showNotification(notificationTitle, notificationOptions); 222});
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.