1// 1. Gunakan versi spesifik 9.23.0 agar link valid 2importScripts('https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js'); 3importScripts('https://www.gstatic.com/firebasejs/9.23.0/firebase-messaging-compat.js'); 4 5// 2. Inisialisasi dengan Config yang benar (Copy-paste dari Firebase Console) 6const firebaseConfig = { 7 apiKey: "AIzaSyDOqj1WZ3aa22YxD9pj-Fm2UgNy66kioK4", 8 authDomain: "vidgap-push-notification.firebaseapp.com", 9 projectId: "vidgap-push-notification", 10 storageBucket: "vidgap-push-notification.firebasestorage.app", 11 messagingSenderId: "192165573723", 12 appId: "1:192165573723:web:9a8e80111efff4c42f7b7e" 13}; 14 15firebase.initializeApp(firebaseConfig); 16 17// 3. Ambil objek messaging 18const messaging = firebase.messaging(); 19 20const META_DB = "FcmAnalytics"; 21const META_STORE = "meta"; 22 23function openMetaDb() { 24 return new Promise((resolve, reject) => { 25 const request = indexedDB.open(META_DB, 1); 26 request.onupgradeneeded = (event) => { 27 const db = event.target.result; 28 if (!db.objectStoreNames.contains(META_STORE)) { 29 db.createObjectStore(META_STORE); 30 } 31 }; 32 request.onsuccess = () => resolve(request.result); 33 request.onerror = () => reject(request.error); 34 }); 35} 36 37function saveInstallationId(installationId) { 38 if (!installationId) { 39 return Promise.resolve(); 40 } 41 return openMetaDb() 42 .then((db) => new Promise((resolve) => { 43 const tx = db.transaction(META_STORE, "readwrite"); 44 tx.objectStore(META_STORE).put(installationId, "installationId"); 45 tx.oncomplete = () => resolve(); 46 tx.onerror = () => resolve(); 47 })) 48 .catch(() => undefined); 49} 50 51function getStoredInstallationId() { 52 return openMetaDb() 53 .then((db) => new Promise((resolve) => { 54 const tx = db.transaction(META_STORE, "readonly"); 55 const request = tx.objectStore(META_STORE).get("installationId"); 56 request.onsuccess = () => resolve(request.result || ""); 57 request.onerror = () => resolve(""); 58 })) 59 .catch(() => ""); 60} 61 62function resolveAllowedUrl(url) { 63 if (!url || typeof url !== "string") { 64 return self.location.origin + "/"; 65 } 66 try { 67 const parsed = new URL(url, self.location.origin); 68 const host = parsed.hostname; 69 const sameOrigin = parsed.origin === self.location.origin; 70 const vidgapHost = host === "vidgap.com" || host.endsWith(".vidgap.com"); 71 if (sameOrigin || vidgapHost) { 72 return parsed.href; 73 } 74 } catch (error) { 75 // invalid URL 76 } 77 return self.location.origin + "/"; 78} 79 80function reportDelivered(payloadData, title, body) { 81 const notificationId = payloadData.notification_id || payloadData.article_id || ""; 82 if (!notificationId) { 83 return Promise.resolve(); 84 } 85 86 return getStoredInstallationId() 87 .then((installationId) => 88 fetch("/api/notifications/delivered", { 89 method: "POST", 90 headers: { "Content-Type": "application/json" }, 91 body: JSON.stringify({ 92 notificationId, 93 article_id: payloadData.article_id || notificationId, 94 campaignId: payloadData.campaign_id || "", 95 type: payloadData.type || "", 96 url: payloadData.url || "", 97 title, 98 body, 99 installationId, 100 }), 101 }) 102 ) 103 .catch(() => undefined); 104} 105 106self.addEventListener("message", (event) => { 107 if (event.data && event.data.type === "FCM_INSTALLATION_ID") { 108 saveInstallationId(event.data.installationId); 109 } 110}); 111 112self.addEventListener("notificationclick", (event) => { 113 event.notification.close(); 114 const data = event.notification.data || {}; 115 const notificationId = data.notificationId || data.article_id || ""; 116 const targetUrl = resolveAllowedUrl(data.url); 117 118 event.waitUntil((async () => { 119 try { 120 const installationId = await getStoredInstallationId(); 121 if (notificationId && installationId) { 122 await fetch("/api/notifications/opened", { 123 method: "POST", 124 headers: { "Content-Type": "application/json" }, 125 body: JSON.stringify({ 126 notificationId, 127 installationId, 128 campaignId: data.campaignId || "", 129 type: data.type || "", 130 }), 131 }); 132 } 133 } catch (error) { 134 // open tracking must not block navigation 135 } 136 137 const windowClients = await self.clients.matchAll({ 138 type: "window", 139 includeUncontrolled: true, 140 }); 141 142 for (const client of windowClients) { 143 if ("focus" in client) { 144 await client.focus(); 145 if (targetUrl && "navigate" in client) { 146 try { 147 await client.navigate(targetUrl); 148 } catch (error) { 149 // keep focused tab if navigate is unsupported 150 } 151 } 152 return; 153 } 154 } 155 156 if (self.clients.openWindow) { 157 await self.clients.openWindow(targetUrl); 158 } 159 })()); 160}); 161
162// 4. Pastikan messaging ada sebelum panggil onBackgroundMessage 163if (messaging) { 164 messaging.onBackgroundMessage((payload) => { 165 // Note: Service Worker tidak bisa akses process.env, jadi log selalu muncul 166 // Tapi ini penting untuk debugging notifikasi, jadi biarkan tetap ada 167 console.log('[sw.js] Pesan masuk di background:', payload); 168 169 const payloadData = payload.data || {}; 170 const notificationTitle = (payload.notification && payload.notification.title) || "Pesan Baru"; 171 const title = payload.notification && payload.notification.title; 172 const body = payload.notification && payload.notification.body; 173 const notificationId = payloadData.notification_id || payloadData.article_id || ""; 174 const notificationOptions = { 175 body: body || "Ada info baru untukmu.", 176 icon: '/next.svg', // Pastikan file ini ada di folder public/ 177 data: { 178 notificationId, 179 campaignId: payloadData.campaign_id || "", 180 type: payloadData.type || "", 181 url: payloadData.url || "/", 182 article_id: payloadData.article_id || notificationId, 183 }, 184 }; 185 186 // Cek apakah notifikasi perlu disimpan ke database 187 const { is_save_db, article_id, country_code } = payloadData; 188 189 if (is_save_db === "true" && article_id) { 190 // Jalankan operasi async untuk simpan ke IndexedDB dan server 191 const syncTask = async () => { 192 try { 193 // 1. Open IndexedDB 194 const dbRequest = indexedDB.open("NotificationStore", 1); 195 196 dbRequest.onupgradeneeded = (event) => { 197 const db = event.target.result; 198 if (!db.objectStoreNames.contains("notifications")) { 199 const store = db.createObjectStore("notifications", { keyPath: "article_id" }); 200 // Create index for efficient querying 201 store.createIndex("sync_status", "sync_status", { unique: false }); 202 store.createIndex("at", "at", { unique: false }); 203 } 204 }; 205 206 dbRequest.onsuccess = async (event) => { 207 const db = event.target.result; 208 209 // 2. Check if notification already exists (prevent duplicates) 210 const txCheck = db.transaction("notifications", "readonly"); 211 const storeCheck = txCheck.objectStore("notifications"); 212 const existing = await new Promise((resolve) => { 213 const req = storeCheck.get(article_id); 214 req.onsuccess = () => resolve(req.result); 215 req.onerror = () => resolve(null); 216 }); 217 218 // If already exists and synced, skip 219 if (existing && existing.sync_status === 'synced') { 220 console.log('[sw.js] Notification already exists and synced:', article_id); 221 return; 222 } 223 224 // 3. Save to IndexedDB with pending status 225 const tx = db.transaction("notifications", "readwrite"); 226 const store = tx.objectStore("notifications"); 227 228 const notificationData = { 229 article_id, 230 title, 231 body, 232 country_code: country_code || '', 233 sync_status: 'pending', 234 at: new Date().toISOString(), 235 retry_count: existing ? (existing.retry_count || 0) : 0, 236 }; 237 238 store.put(notificationData); 239 240 console.log('[sw.js] Data disimpan ke IndexedDB:', article_id); 241
242 // 4. Try to send to server (with retry mechanism) 243 const maxRetries = 3; 244 let retryCount = 0; 245 let saveSuccess = false; 246 247 while (retryCount < maxRetries && !saveSuccess) { 248 try { 249 const res = await fetch('/api/notifications/save', { 250 method: 'POST', 251 body: JSON.stringify({ 252 article_id, 253 title, 254 body, 255 country_code, 256 notificationId, 257 campaignId: payloadData.campaign_id || "", 258 type: payloadData.type || "", 259 url: payloadData.url || "", 260 metadata: { 261 source: 'service_worker', 262 received_at: new Date().toISOString(), 263 } 264 }), 265 headers: { 'Content-Type': 'application/json' } 266 }); 267 268 if (res.ok) { 269 const result = await res.json(); 270 console.log('[sw.js] Data berhasil dikirim ke server:', article_id); 271 272 // Update status to synced 273 const tx2 = db.transaction("notifications", "readwrite"); 274 const store2 = tx2.objectStore("notifications"); 275 store2.put({ 276 article_id, 277 title, 278 body, 279 country_code: country_code || '', 280 sync_status: 'synced', 281 at: notificationData.at, 282 synced_at: new Date().toISOString(), 283 is_duplicate: result.isDuplicate || false, 284 retry_count: retryCount, 285 }); 286 287 saveSuccess = true; 288 } else { 289 const errorText = await res.text(); 290 console.log(`[sw.js] Gagal sync (attempt ${retryCount + 1}/${maxRetries}):`, errorText); 291 retryCount++; 292 293 // Wait before retry (exponential backoff) 294 if (retryCount < maxRetries) { 295 await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); 296 } 297 } 298 } catch (e) { 299 console.log(`[sw.js] Network error (attempt ${retryCount + 1}/${maxRetries}):`, e.message); 300 retryCount++; 301 302 // Wait before retry 303 if (retryCount < maxRetries) { 304 await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); 305 } 306 } 307 } 308 309 // If all retries failed, update retry count in IndexedDB 310 if (!saveSuccess) { 311 console.log('[sw.js] All sync attempts failed, marked as pending for later sync'); 312 const tx3 = db.transaction("notifications", "readwrite"); 313 const store3 = tx3.objectStore("notifications"); 314 store3.put({ 315 ...notificationData, 316 retry_count: retryCount, 317 last_retry_at: new Date().toISOString(), 318 }); 319 } 320 }; 321 322 dbRequest.onerror = (event) => { 323 console.error('[sw.js] Error membuka IndexedDB:', event.target.error); 324 }; 325 } catch (error) { 326 console.error('[sw.js] Error dalam syncTask:', error); 327 } 328 }; 329 330 // Jalankan sync task 331 syncTask(); 332 } 333 334 return Promise.all([ 335 self.registration.showNotification(notificationTitle, notificationOptions), 336 reportDelivered(payloadData, title, body), 337 ]); 338 }); 339}
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.