@rivium/push-web 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivium/push-web",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Web Push SDK for browsers - Firebase alternative that works everywhere",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/service-worker.js CHANGED
@@ -6,7 +6,73 @@
6
6
  */
7
7
 
8
8
  // Cache name for offline support
9
- const CACHE_NAME = 'rivium-push-v1';
9
+ const CACHE_NAME = 'rivium-push-v0.1.4';
10
+
11
+ /**
12
+ * Config passed by the SDK on the registration URL.
13
+ *
14
+ * A service worker is terminated between pushes, so anything kept in memory is
15
+ * gone by the time the next one arrives. The script URL is persisted by the
16
+ * browser, so reading config from its query string works on every wake-up
17
+ * without needing IndexedDB.
18
+ */
19
+ const RIVIUM_CONFIG = (() => {
20
+ try {
21
+ const params = new URL(self.location.href).searchParams;
22
+ return {
23
+ apiKey: params.get('riviumApiKey') || null,
24
+ serverUrl: params.get('riviumServerUrl') || 'https://push-api.rivium.co',
25
+ deviceId: params.get('riviumDeviceId') || null,
26
+ };
27
+ } catch (e) {
28
+ return { apiKey: null, serverUrl: 'https://push-api.rivium.co', deviceId: null };
29
+ }
30
+ })();
31
+
32
+ /**
33
+ * Confirm delivery to Rivium Push.
34
+ *
35
+ * Web Push, APNs and FCM all report only that the push service *accepted* a
36
+ * notification — none of them confirm it reached the device. This ack is the
37
+ * only signal that it actually arrived, so the dashboard can distinguish
38
+ * "accepted" from "delivered".
39
+ *
40
+ * Best-effort: a failure here must never stop the notification being shown.
41
+ */
42
+ function riviumReportDelivered(messageId) {
43
+ if (!messageId || !RIVIUM_CONFIG.apiKey || !RIVIUM_CONFIG.deviceId) {
44
+ console.warn('[RiviumPush SW] Delivery ack skipped — missing messageId or SDK config', {
45
+ messageId: messageId || null,
46
+ hasApiKey: !!RIVIUM_CONFIG.apiKey,
47
+ hasDeviceId: !!RIVIUM_CONFIG.deviceId,
48
+ });
49
+ return Promise.resolve();
50
+ }
51
+
52
+ // No `keepalive`: event.waitUntil already holds the worker open, and
53
+ // keepalive is unreliable for fetches issued from a service worker.
54
+ return fetch(`${RIVIUM_CONFIG.serverUrl}/receipts/delivered`, {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ 'x-api-key': RIVIUM_CONFIG.apiKey,
59
+ },
60
+ body: JSON.stringify({ messageId, deviceId: RIVIUM_CONFIG.deviceId }),
61
+ })
62
+ .then((res) => {
63
+ // A non-2xx is a failure — resolving on it would hide the problem the
64
+ // same way the server used to hide an ack for a receipt that did not
65
+ // exist yet.
66
+ if (!res.ok) {
67
+ console.warn('[RiviumPush SW] Delivery ack rejected', res.status);
68
+ return;
69
+ }
70
+ console.log('[RiviumPush SW] Delivery confirmed', messageId);
71
+ })
72
+ .catch((err) => {
73
+ console.warn('[RiviumPush SW] Delivery ack failed:', err && err.message);
74
+ });
75
+ }
10
76
 
11
77
  // Install event
12
78
  self.addEventListener('install', (event) => {
@@ -50,10 +116,33 @@ self.addEventListener('push', (event) => {
50
116
 
51
117
  const options = {
52
118
  body: localizedBody || data.body,
53
- icon: data.iconUrl || data.icon,
54
- badge: data.iconUrl || data.icon,
119
+ // Icons must resolve to a real file. Android Chrome silently drops a
120
+ // notification when the icon 404s (desktop is more forgiving), so keep
121
+ // /icon.png and /badge.png present in your public directory, or always
122
+ // send iconUrl from the backend.
123
+ icon: data.iconUrl || data.icon || '/icon.png',
124
+ // `badge` is the small monochrome icon Android shows in the status bar.
125
+ badge: data.badgeIcon || data.iconUrl || data.icon || '/badge.png',
55
126
  image: data.imageUrl || data.image,
56
- tag: data.tag || data.collapseKey || data.threadId || 'rivium-push-notification',
127
+ // Per-message uniqueness by default. A fixed tag collapses every push
128
+ // into one slot in the Android shade — new notifications silently replace
129
+ // older ones and the user never sees the second, third, … arrive. The
130
+ // backend still opts into grouping by sending an explicit tag or
131
+ // collapseKey (e.g. to fold "N liked your post" into one).
132
+ tag:
133
+ data.tag ||
134
+ data.collapseKey ||
135
+ data.threadId ||
136
+ data.messageId ||
137
+ `rivium-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
138
+ // renotify forces Android Chrome to actually surface the notification
139
+ // while Chrome is backgrounded but not closed. Without it Chrome treats
140
+ // the origin as "about to be focused" and swallows showNotification — the
141
+ // worker logs success and the shade stays empty. Harmless elsewhere.
142
+ renotify: true,
143
+ // A second signal to Android that this is a fresh user-visible event.
144
+ // Ignored on desktop.
145
+ vibrate: [200, 100, 200],
57
146
  data: {
58
147
  ...data.data,
59
148
  deepLink: data.deepLink,
@@ -78,8 +167,105 @@ self.addEventListener('push', (event) => {
78
167
  // Show notification with potentially localized title
79
168
  const title = localizedTitle || data.title;
80
169
 
170
+ event.waitUntil((async () => {
171
+ // Mirror the payload to any visible page. On mobile the app is usually
172
+ // backgrounded or the screen is locked, so the page-side connection is
173
+ // asleep and only the worker sees the push. Pages should dedupe by
174
+ // messageId, since a foreground client may receive it both ways.
175
+ try {
176
+ const clientsList = await self.clients.matchAll({
177
+ type: 'window',
178
+ includeUncontrolled: true,
179
+ });
180
+ for (const client of clientsList) {
181
+ client.postMessage({
182
+ type: 'rivium-push-message',
183
+ message: {
184
+ title,
185
+ body: localizedBody || data.body,
186
+ data: data.data || {},
187
+ deepLink: data.deepLink,
188
+ messageId: data.messageId,
189
+ campaignId: data.campaignId,
190
+ },
191
+ });
192
+ }
193
+ } catch (err) {
194
+ console.warn('[RiviumPush SW] client postMessage failed:', err);
195
+ }
196
+
197
+ // Always show the OS notification: Chrome's user-visible contract revokes
198
+ // subscriptions that push silently. Wrapped so an Android quirk that would
199
+ // otherwise fail silently surfaces here; the retry drops the optional
200
+ // fields (icon/badge/image/actions) that are the usual cause.
201
+ try {
202
+ await self.registration.showNotification(title, options);
203
+ } catch (err) {
204
+ console.warn('[RiviumPush SW] showNotification failed, retrying minimal', err);
205
+ try {
206
+ await self.registration.showNotification(title, {
207
+ body: options.body,
208
+ tag: options.tag,
209
+ data: options.data,
210
+ });
211
+ } catch (err2) {
212
+ console.error('[RiviumPush SW] showNotification (minimal) failed too', err2);
213
+ }
214
+ }
215
+
216
+ // Confirm arrival so the dashboard can show `delivered`, not only `sent`.
217
+ await riviumReportDelivered(data.messageId);
218
+ })());
219
+ });
220
+
221
+ /**
222
+ * The browser can rotate a push subscription at any time — after a long idle
223
+ * period, a browser update, or when it decides the old endpoint is stale.
224
+ * Without this handler the old endpoint stays registered server-side, keeps
225
+ * returning 410 Gone, and the user silently stops receiving notifications.
226
+ *
227
+ * Re-subscribe with the same VAPID key and register the new endpoint.
228
+ */
229
+ self.addEventListener('pushsubscriptionchange', (event) => {
230
+ console.log('[RiviumPush SW] Push subscription changed, re-subscribing');
231
+
232
+ const applicationServerKey =
233
+ (event.oldSubscription &&
234
+ event.oldSubscription.options &&
235
+ event.oldSubscription.options.applicationServerKey) ||
236
+ null;
237
+
238
+ if (!applicationServerKey || !RIVIUM_CONFIG.apiKey || !RIVIUM_CONFIG.deviceId) {
239
+ console.warn(
240
+ '[RiviumPush SW] Cannot re-subscribe: missing VAPID key or SDK config',
241
+ );
242
+ return;
243
+ }
244
+
81
245
  event.waitUntil(
82
- self.registration.showNotification(title, options)
246
+ self.registration.pushManager
247
+ .subscribe({ userVisibleOnly: true, applicationServerKey })
248
+ .then((subscription) =>
249
+ fetch(`${RIVIUM_CONFIG.serverUrl}/devices/register`, {
250
+ method: 'POST',
251
+ headers: {
252
+ 'Content-Type': 'application/json',
253
+ 'x-api-key': RIVIUM_CONFIG.apiKey,
254
+ },
255
+ body: JSON.stringify({
256
+ deviceId: RIVIUM_CONFIG.deviceId,
257
+ platform: 'web',
258
+ appIdentifier: self.location.origin,
259
+ webPushSubscription: subscription.toJSON(),
260
+ }),
261
+ }),
262
+ )
263
+ .then(() => {
264
+ console.log('[RiviumPush SW] Re-subscribed and re-registered');
265
+ })
266
+ .catch((err) => {
267
+ console.error('[RiviumPush SW] Re-subscribe failed:', err && err.message);
268
+ }),
83
269
  );
84
270
  });
85
271