PageSourceSearch

https://mario888.online/push-sw.js

js mario888.online collected 2026-10-08 08:30:40 UTC 5,843 bytes, 191 lines download raw bytes

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.