PageSourceSearch

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

js realsmile.online collected 2026-09-24 13:42:53 UTC 4,095 bytes, 127 lines download raw bytes

1// public/sw-push.js — Sprint 2026-05-23 (Push Notifications)
2//
3// Browser service worker. Owns three concerns:
4//   1. push           — render notification from server-pushed JSON payload
5//   2. notificationclick — open the click_url (focus tab if already open)
6//   3. message        — none here yet; analytics post happens on 'click'
7//
8// Registered by components/PushOptInPrompt.tsx via:
9//   navigator.serviceWorker.register('/sw-push.js', { scope: '/' })
10//
11// IMPORTANT: this file is served from /public/ and runs in the SW thread. It
12// cannot import from app/ or lib/. Keep it self-contained.
13
14self.addEventListener('install', (event) => {
15  // Activate immediately on first install — no waiting for old tabs to close.
16  self.skipWaiting();
17});
18
19self.addEventListener('activate', (event) => {
20  event.waitUntil(self.clients.claim());
21});
22
23self.addEventListener('push', (event) => {
24  // Payload contract (set by lib/push-notify.ts):
25  //   { title: string, body: string, url?: string, tag?: string, icon?: string }
26  let payload = {};
27  try {
28    payload = event.data ? event.data.json() : {};
29  } catch (_e) {
30    payload = { title: 'RealSmile', body: event.data ? event.data.text() : '' };
31  }
32
33  const title = payload.title || 'RealSmile';
34  const options = {
35    body: payload.body || '',
36    icon: payload.icon || '/icon-192.png',
37    badge: '/icon-192.png',
38    // tag collapses repeated notifications of the same kind (e.g. daily-tip)
39    tag: payload.tag || 'realsmile',
40    // Always re-notify so the user sees the latest tip if they ignored the previous.
41    renotify: true,
42    data: {
43      url: payload.url || '/account',
44      tag: payload.tag || 'realsmile',
45      sentAt: payload.sentAt || Date.now(),
46    },
47  };
48
49  event.waitUntil(self.registration.showNotification(title, options));
50});
51
52self.addEventListener('notificationclick', (event) => {
53  event.notification.close();
54  const data = event.notification.data || {};
55  const targetUrl = data.url || '/account';
56
57  // Fire-and-forget click beacon to /api/push/track. Service worker fetch
58  // ignores the response — analytics is best-effort.
59  event.waitUntil(
60    (async () => {
61      try {
62        await fetch('/api/push/track', {
63          method: 'POST',
64          headers: { 'Content-Type': 'application/json' },
65          body: JSON.stringify({
66            event: 'click',
67            tag: data.tag || 'realsmile',
68            sentAt: data.sentAt || null,
69          }),
70          // keepalive lets the beacon survive SW shutdown.
71          keepalive: true,
72        });
73      } catch (_e) {
74        // ignore
75      }
76
77      // Focus an existing realsmile tab if any, otherwise open a new one.
78      const allClients = await self.clients.matchAll({
79        type: 'window',
80        includeUncontrolled: true,
81      });
82      for (const client of allClients) {
83        try {
84          const u = new URL(client.url);
85          if (u.origin === self.location.origin) {
86            await client.focus();
87            // Navigate the focused tab to the target URL.
88            if ('navigate' in client) {
89              try {
90                await client.navigate(targetUrl);
91              } catch (_e) {
92                // navigate can fail if cross-origin redirected — fall back to openWindow
93              }
94            }
95            return;
96          }
97        } catch (_e) {
98          // ignore
99        }
100      }
101      await self.clients.openWindow(targetUrl);
102    })(),
103  );
104});
105
106self.addEventListener('notificationclose', (event) => {
107  // Best-effort dismissal analytics — same endpoint, different event.
108  const data = event.notification.data || {};
109  event.waitUntil(
110    (async () => {
111      try {
112        await fetch('/api/push/track', {
113          method: 'POST',
114          headers: { 'Content-Type': 'application/json' },
115          body: JSON.stringify({
116            event: 'dismiss',
117            tag: data.tag || 'realsmile',
118            sentAt: data.sentAt || null,
119          }),
120          keepalive: true,
121        });
122      } catch (_e) {
123        // ignore
124      }
125    })(),
126  );
127});

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.