/*! * 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 () {}) ); });