1/** 2 * Firebase Cloud Messaging Service Worker 3 * Handles background push notifications 4 */ 5 6// Import Firebase scripts for service worker 7importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js"); 8importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js"); 9 10// Initialize Firebase in the service worker 11// Note: These values must match your Firebase config 12firebase.initializeApp({ 13 apiKey: "AIzaSyCXtGKZhjCk6kUtw7F9-sxZUUH_yxEbyf4", 14 authDomain: "easybook-ai.firebaseapp.com", 15 projectId: "easybook-ai", 16 storageBucket: "easybook-ai.firebasestorage.app", 17 messagingSenderId: "915929748056", 18 appId: "1:915929748056:web:c10ffc6c90f42c7efef02d", 19}); 20 21const messaging = firebase.messaging(); 22 23const PUSH_CACHE = "easybook-push"; 24const PUSH_STRINGS_URL = "/push-strings.json"; 25const PENDING_MFA_APPROVAL_URL = "/pending-mfa-approval.json"; 26const NEUTRAL_APPROVAL_LABELS = { approve: "\u2713", deny: "\u2715" }; 27 28function readPushStrings() { 29 return caches 30 .open(PUSH_CACHE) 31 .then((cache) => cache.match(PUSH_STRINGS_URL)) 32 .then((response) => (response ? response.json() : {})) 33 .catch(() => ({})); 34} 35 36function titleOf(payload, strings) { 37 return payload.notification?.title || payload.data?.title || strings.defaultTitle || "EasyBook"; 38} 39 40function bodyOf(payload) { 41 return payload.notification?.body || payload.data?.body || ""; 42} 43 44function payloadActions(payload) { 45 try { 46 const actions = JSON.parse(payload.data?.actions || "[]"); 47 return Array.isArray(actions) ? actions : []; 48 } catch { 49 return []; 50 } 51} 52 53function approvalLabels(strings) { 54 if (strings.approve && strings.deny) return { approve: strings.approve, deny: strings.deny }; 55 return NEUTRAL_APPROVAL_LABELS; 56} 57 58function stashPendingApproval(data) { 59 const entry = { 60 approvalCode: data.approvalCode, 61 requestIp: data.requestIp, 62 expiresAt: data.expiresAt, 63 location: data.location, 64 userAgent: data.userAgent, 65 }; 66 return caches 67 .open(PUSH_CACHE) 68 .then((cache) => 69 cache.put( 70 PENDING_MFA_APPROVAL_URL, 71 new Response(JSON.stringify(entry), { headers: { "Content-Type": "application/json" } }) 72 ) 73 ) 74 .catch(() => undefined); 75} 76 77messaging.onBackgroundMessage((payload) => 78 readPushStrings().then((strings) => { 79 if (payload.data?.type === "MFA_APPROVAL") { 80 const approvalCode = payload.data.approvalCode; 81 const labels = approvalLabels(strings); 82 return self.registration.showNotification(titleOf(payload, strings), { 83 body: bodyOf(payload), 84 icon: "/icons/notification-icon.png", 85 badge: "/icons/badge-icon.png", 86 tag: `mfa-approval-${approvalCode}`, 87 requireInteraction: true, 88 data: { 89 type: "MFA_APPROVAL", 90 approvalCode, 91 requestIp: payload.data.requestIp, 92 expiresAt: payload.data.expiresAt, 93 location: payload.data.location, 94 userAgent: payload.data.userAgent, 95 url: "/", 96 }, 97 vibrate: [200, 100, 200, 100, 200], 98 actions: [ 99 { action: "approve", title: labels.approve }, 100 { action: "deny", title: labels.deny }, 101 ], 102 }); 103 } 104 105 return self.registration.showNotification(titleOf(payload, strings), { 106 body: bodyOf(payload), 107 icon: "/icons/notification-icon.png", 108 badge: "/icons/badge-icon.png", 109 tag: payload.data?.tag || "easybook-notification", 110 data: { 111 url: payload.data?.url || payload.data?.click_action || "/", 112 ...payload.data, 113 }, 114 vibrate: [100, 50, 100], 115 actions: payloadActions(payload), 116 }); 117 }) 118); 119 120// Handle notification click 121self.addEventListener("notificationclick", (event) => { 122 const notificationData = event.notification.data || {}; 123 124 event.notification.close(); 125 126 // Handle MFA approval action clicks 127 if (notificationData.type === "MFA_APPROVAL") { 128 const approvalCode = notificationData.approvalCode; 129 130 // Open the app and trigger the MFA approval modal 131 // The app will handle showing the approval UI 132 event.waitUntil( 133 clients 134 .matchAll({ 135 type: "window", 136 includeUncontrolled: true, 137 }) 138 .then((clientList) => { 139 // Try to find and focus an existing window 140 for (const client of clientList) { 141 if (client.url.includes(self.location.origin) && "focus" in client) { 142 client.focus(); 143 // Post message to trigger approval modal in the app 144 client.postMessage({ 145 type: "MFA_APPROVAL_NOTIFICATION", 146 action: event.action || "open", // 'approve', 'deny', or 'open' 147 approvalCode, 148 requestIp: notificationData.requestIp, 149 expiresAt: notificationData.expiresAt, 150 location: notificationData.location, 151 userAgent: notificationData.userAgent, 152 }); 153 return; 154 } 155 } 156 if (clients.openWindow) { 157 return stashPendingApproval(notificationData).then(() => clients.openWindow("/")); 158 } 159 }) 160 ); 161 return; 162 } 163 164 // Get the URL to open for normal notifications 165 const urlToOpen = notificationData.url || "/"; 166 167 // Handle action clicks for normal notifications 168 if (event.action) { 169 const actionUrl = notificationData[`action_${event.action}_url`]; 170 if (actionUrl) { 171 event.waitUntil(clients.openWindow(actionUrl)); 172 return; 173 } 174 } 175 176 // Focus existing window or open new one 177 event.waitUntil( 178 clients 179 .matchAll({ 180 type: "window", 181 includeUncontrolled: true, 182 }) 183 .then((clientList) => { 184 // Try to focus an existing window 185 for (const client of clientList) { 186 if (client.url.includes(self.location.origin) && "focus" in client) { 187 client.focus(); 188 // Navigate to the URL 189 if (urlToOpen !== "/") { 190 client.navigate(urlToOpen); 191 } 192 return; 193 } 194 } 195 // Open new window if none found 196 if (clients.openWindow) { 197 return clients.openWindow(urlToOpen); 198 } 199 }) 200 ); 201});
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.