agent: Host WRNexus push service worker at site root
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
/*!
|
||||||
|
* WRNexus Web Push — service worker.
|
||||||
|
*
|
||||||
|
* ┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
* │ HOST THIS FILE ON YOUR OWN ORIGIN. │
|
||||||
|
* │ │
|
||||||
|
* │ A service worker can only control the origin it is served from, so the │
|
||||||
|
* │ browser will NOT let wrnexus.com register a worker for your site. Copy │
|
||||||
|
* │ this file to the ROOT of your site and serve it at: │
|
||||||
|
* │ │
|
||||||
|
* │ https://your-domain.example/wrnexus-push-sw.js │
|
||||||
|
* │ │
|
||||||
|
* │ It must be served same-origin, over HTTPS, with │
|
||||||
|
* │ `Content-Type: text/javascript`. To control the whole site from a │
|
||||||
|
* │ non-root path, also send `Service-Worker-Allowed: /`. │
|
||||||
|
* └─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*
|
||||||
|
* Core behaviour needs NO configuration — the click/delivery tracking URLs are
|
||||||
|
* carried inside each push payload built by the WRNexus API. The optional
|
||||||
|
* CONFIG block below is only used to silently re-subscribe after the browser
|
||||||
|
* rotates a subscription (`pushsubscriptionchange`); leave it blank to skip
|
||||||
|
* that.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
|
||||||
|
// ── OPTIONAL: only needed for automatic re-subscription on key rotation ──────
|
||||||
|
var WRNEXUS_PUSH_CONFIG = {
|
||||||
|
// Your site's publicKey (the same value as data-site-id in the snippet).
|
||||||
|
siteKey: '',
|
||||||
|
// The WRNexus API origin (e.g. 'https://api.wrnexus.com').
|
||||||
|
apiBase: '',
|
||||||
|
// Your site's VAPID public key (shown in Push → Settings).
|
||||||
|
vapidPublicKey: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
self.addEventListener('install', function () {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', function (event) {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
// CORS-simple POST (text/plain, no custom headers, no credentials) so the
|
||||||
|
// receipt reaches the WRNexus API cross-origin without a preflight.
|
||||||
|
function postReceipt(url) {
|
||||||
|
if (!url) return Promise.resolve();
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
|
||||||
|
credentials: 'omit',
|
||||||
|
keepalive: true,
|
||||||
|
mode: 'cors',
|
||||||
|
}).catch(function () {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlBase64ToUint8Array(base64String) {
|
||||||
|
var padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
var raw = self.atob(base64);
|
||||||
|
var out = new Uint8Array(raw.length);
|
||||||
|
for (var i = 0; i < raw.length; ++i) out[i] = raw.charCodeAt(i);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyToB64(buf) {
|
||||||
|
if (!buf) return '';
|
||||||
|
var bytes = new Uint8Array(buf);
|
||||||
|
var binary = '';
|
||||||
|
for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||||||
|
return self
|
||||||
|
.btoa(binary)
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Incoming notification ─────────────────────────────────────────────────
|
||||||
|
self.addEventListener('push', function (event) {
|
||||||
|
var data = {};
|
||||||
|
try {
|
||||||
|
data = event.data ? event.data.json() : {};
|
||||||
|
} catch (_e) {
|
||||||
|
try {
|
||||||
|
data = { title: event.data && event.data.text() };
|
||||||
|
} catch (_e2) {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = data.title || 'Notification';
|
||||||
|
var options = {
|
||||||
|
body: data.body || '',
|
||||||
|
icon: data.icon || undefined,
|
||||||
|
image: data.image || undefined,
|
||||||
|
badge: data.badge || undefined,
|
||||||
|
tag: data.tag || data.campaignId || undefined,
|
||||||
|
requireInteraction: !!data.requireInteraction,
|
||||||
|
data: {
|
||||||
|
clickUrl: data.clickUrl || data.url || '/',
|
||||||
|
url: data.url || '/',
|
||||||
|
openUrl: data.openUrl || '',
|
||||||
|
campaignId: data.campaignId || '',
|
||||||
|
subscriptionId: data.subscriptionId || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
Promise.all([
|
||||||
|
self.registration.showNotification(title, options),
|
||||||
|
// Delivery receipt — records a `delivered` event for analytics.
|
||||||
|
postReceipt(data.deliveredUrl),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Click → record + open the destination ─────────────────────────────────
|
||||||
|
self.addEventListener('notificationclick', function (event) {
|
||||||
|
var d = (event.notification && event.notification.data) || {};
|
||||||
|
event.notification.close();
|
||||||
|
|
||||||
|
// `clickUrl` is a signed WRNexus redirect: hitting it records the `clicked`
|
||||||
|
// event and 302s to the real destination, so tracking fires from the open.
|
||||||
|
var target = d.clickUrl || d.url || '/';
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (list) {
|
||||||
|
// Reuse an existing tab on the destination when possible.
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
var client = list[i];
|
||||||
|
if (d.url && client.url === d.url && 'focus' in client) return client.focus();
|
||||||
|
}
|
||||||
|
if (self.clients.openWindow) return self.clients.openWindow(target);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Optional: re-subscribe after the browser rotates the subscription ──────
|
||||||
|
self.addEventListener('pushsubscriptionchange', function (event) {
|
||||||
|
var cfg = WRNEXUS_PUSH_CONFIG || {};
|
||||||
|
if (!cfg.siteKey || !cfg.apiBase || !cfg.vapidPublicKey) return;
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.pushManager
|
||||||
|
.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(cfg.vapidPublicKey) })
|
||||||
|
.then(function (sub) {
|
||||||
|
return fetch(cfg.apiBase.replace(/\/+$/, '') + '/api/push/subscriptions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
|
||||||
|
credentials: 'omit',
|
||||||
|
mode: 'cors',
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: cfg.siteKey,
|
||||||
|
endpoint: sub.endpoint,
|
||||||
|
keys: {
|
||||||
|
p256dh: keyToB64(sub.getKey('p256dh')),
|
||||||
|
auth: keyToB64(sub.getKey('auth')),
|
||||||
|
},
|
||||||
|
metadata: { source: 'pushsubscriptionchange' },
|
||||||
|
}),
|
||||||
|
}).catch(function () {});
|
||||||
|
})
|
||||||
|
.catch(function () {})
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user