1/* xbtech push-sw v3 â iOS deep-link via Cache + postMessage */ 2const PUSH_NAVIGATE_MESSAGE_TYPE = "xbtech-push-navigate"; 3const PUSH_NAV_CACHE = "xbtech-push-nav-v1"; 4const PUSH_NAV_PENDING_KEY = "/__xbtech_push_pending_nav__"; 5const PUSH_NAV_TAG_PREFIX = "/__xbtech_push_tag__/"; 6 7self.addEventListener("install", (event) => { 8 event.waitUntil(self.skipWaiting()); 9}); 10 11self.addEventListener("activate", (event) => { 12 event.waitUntil(self.clients.claim()); 13}); 14 15async function stashJson(key, value) { 16 const cache = await caches.open(PUSH_NAV_CACHE); 17 await cache.put( 18 key, 19 new Response(JSON.stringify(value), { 20 headers: { "Content-Type": "application/json" }, 21 }), 22 ); 23} 24 25async function readJson(key) { 26 try { 27 const cache = await caches.open(PUSH_NAV_CACHE); 28 const res = await cache.match(key); 29 if (!res) return null; 30 return await res.json(); 31 } catch { 32 return null; 33 } 34} 35 36self.addEventListener("push", (event) => { 37 let data = {}; 38 try { 39 data = event.data ? event.data.json() : {}; 40 } catch { 41 data = {}; 42 } 43 44 const title = data.title || "Notification"; 45 const targetUrl = data.targetUrl || "/"; 46 const campaignId = data.campaignId != null ? Number(data.campaignId) : 0; 47 const recipientId = data.recipientId != null ? Number(data.recipientId) : 0; 48 const actions = Array.isArray(data.actions) 49 ? data.actions 50 .filter((a) => a && typeof a.action === "string" && typeof a.title === "string") 51 .slice(0, 2) 52 .map((a) => ({ 53 action: String(a.action), 54 title: String(a.title), 55 ...(a.icon ? { icon: String(a.icon) } : {}), 56 })) 57 : []; 58 59 const actionUrls = {}; 60 if (Array.isArray(data.actions)) { 61 for (const a of data.actions) { 62 if (a && typeof a.action === "string" && a.url) { 63 actionUrls[String(a.action)] = String(a.url); 64 } 65 } 66 } 67 68 const tag = data.tag || "site-notification"; 69 const options = { 70 body: data.body || "You have a new notification", 71 icon: data.icon || "/icon.png", 72 badge: data.badge || "/icon.png", 73 tag, 74 data: { targetUrl, campaignId, recipientId, actionUrls }, 75 }; 76 77 if (data.image) options.image = String(data.image); 78 if (actions.length > 0) options.actions = actions; 79 if (data.requireInteraction === true) options.requireInteraction = true; 80 if (data.renotify === true) options.renotify = true; 81 82 event.waitUntil( 83 (async () => { 84 // Persist routing payload â iOS sometimes returns empty notification.data on click. 85 await stashJson(`${PUSH_NAV_TAG_PREFIX}${encodeURIComponent(tag)}`, { 86 targetUrl, 87 actionUrls, 88 campaignId, 89 recipientId, 90 at: Date.now(), 91 }); 92 await self.registration.showNotification(title, options); 93 })(), 94 ); 95}); 96 97function reportPushClick(campaignId, recipientId) { 98 if (!campaignId || !Number.isFinite(campaignId) || campaignId <= 0) return Promise.resolve(); 99 if (!recipientId || !Number.isFinite(recipientId) || recipientId <= 0) return Promise.resolve(); 100 return fetch("/api/member/push/click", { 101 method: "POST", 102 headers: { "Content-Type": "application/json" }, 103 body: JSON.stringify({ campaignId, recipientId }), 104 credentials: "same-origin", 105 keepalive: true, 106 }).catch(() => {}); 107} 108 109function isAppleWebKitPwa() { 110 const ua = self.navigator.userAgent || ""; 111 if (/iPhone|iPad|iPod/.test(ua)) return true; 112 if (/Macintosh/.test(ua) && !/Chrome|Chromium|Edg\//.test(ua)) return true; 113 return false; 114} 115 116self.addEventListener("notificationclick", (event) => { 117 event.notification.close(); 118 const tag = event.notification.tag || "site-notification"; 119 const rawData = event.notification.data || {}; 120 121 event.waitUntil( 122 (async () => { 123 let data = rawData && typeof rawData === "object" ? { ...rawData } : {}; 124 if (!data.targetUrl) { 125 const cached = await readJson(`${PUSH_NAV_TAG_PREFIX}${encodeURIComponent(tag)}`); 126 if (cached && typeof cached === "object") { 127 data = { ...cached, ...data }; 128 } 129 } 130 131 const actionUrls = data.actionUrls || {}; 132 const action = event.action || ""; 133 const targetUrl = (action && actionUrls[action]) || data.targetUrl || "/"; 134 const url = new URL(targetUrl, self.location.origin).href; 135 const campaignId = Number(data.campaignId) || 0; 136 const recipientId = Number(data.recipientId) || 0; 137 138 // Page reads this on focus/visibility â most reliable on iOS lock-screen wake. 139 await stashJson(PUSH_NAV_PENDING_KEY, { url, at: Date.now() }); 140 141 await reportPushClick(campaignId, recipientId); 142 143 const clientList = await self.clients.matchAll({ 144 type: "window", 145 includeUncontrolled: true, 146 }); 147 148 for (const client of clientList) { 149 if (!("focus" in client)) continue; 150 try { 151 await client.focus(); 152 } catch { 153 /* ignore */ 154 } 155 try {
156 client.postMessage({ type: PUSH_NAVIGATE_MESSAGE_TYPE, url }); 157 } catch { 158 /* ignore */ 159 } 160 } 161 162 if (isAppleWebKitPwa()) { 163 try { 164 const opened = await self.clients.openWindow(url); 165 if (opened && "focus" in opened) await opened.focus(); 166 } catch { 167 /* existing client already focused + pending cache set */ 168 } 169 return; 170 } 171 172 for (const client of clientList) { 173 if ("navigate" in client) { 174 try { 175 const navigated = await client.navigate(url); 176 if (navigated && "focus" in navigated) { 177 await navigated.focus(); 178 return; 179 } 180 } catch { 181 /* fall through */ 182 } 183 } 184 } 185 186 if (clientList.length === 0) { 187 await self.clients.openWindow(url); 188 } 189 })(), 190 ); 191});
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.