@active-reach/web-sdk 1.8.0 → 1.8.5

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/dist/aegis-sw.js CHANGED
@@ -1 +1 @@
1
- !function(){"use strict";function i(i){self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(t=>{for(const n of t)n.postMessage(i)}).catch(()=>{})}self.addEventListener("install",()=>{self.skipWaiting()}),self.addEventListener("activate",i=>{i.waitUntil(self.clients.claim())}),self.addEventListener("push",t=>{if(!t.data)return;let n;try{n=t.data.json()}catch{n={title:t.data.text()||"Notification"}}const a=n.title||"Notification",e={campaign_id:n.campaign_id,action_url:n.action_url||n.url,timestamp:Date.now()},o={body:n.body||"",icon:n.icon||void 0,badge:n.badge||void 0,image:n.image||void 0,tag:n.tag||n.campaign_id||void 0,data:e,...Array.isArray(n.actions)&&n.actions.length>0?{actions:n.actions.slice(0,2)}:{},requireInteraction:n.require_interaction??!1};t.waitUntil(self.registration.showNotification(a,o).then(()=>{i({type:"push.delivered",campaign_id:n.campaign_id})}).catch(t=>(console.error("[aegis-sw] showNotification failed, retrying minimal:",t),self.registration.showNotification(a,{body:n.body||"",data:e}).then(()=>{i({type:"push.delivered",campaign_id:n.campaign_id,degraded:!0})}).catch(i=>{console.error("[aegis-sw] minimal showNotification also failed:",i)}))))}),self.addEventListener("notificationclick",t=>{t.notification.close();const n=t.notification.data||{},a=n.action_url||"/";let e=a;if(t.action&&Array.isArray(t.notification.actions)){if(t.notification.actions.find(i=>i.action===t.action)){const i=n.action_urls||{};e=i[t.action]||a}}i({type:"push.clicked",campaign_id:n.campaign_id,action_url:e,action:t.action||"default"}),t.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(i=>{for(const t of i)if(new URL(t.url).origin===self.location.origin)return t.focus(),void t.navigate(e);return self.clients.openWindow(e)}))}),self.addEventListener("notificationclose",t=>{i({type:"push.dismissed",campaign_id:(t.notification.data||{}).campaign_id})}),self.addEventListener("pushsubscriptionchange",t=>{var n,a;t.waitUntil(self.registration.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:(null==(a=null==(n=t.oldSubscription)?void 0:n.options)?void 0:a.applicationServerKey)||void 0}).then(t=>{i({type:"push.resubscribed",subscription:t.toJSON()})}).catch(i=>{console.error("[aegis-sw] Failed to resubscribe:",i)}))})}();
1
+ !function(){"use strict";function i(i){self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(t=>{for(const a of t)a.postMessage(i)}).catch(()=>{})}self.addEventListener("install",()=>{self.skipWaiting()}),self.addEventListener("activate",i=>{i.waitUntil(self.clients.claim())}),self.addEventListener("push",t=>{var a,n;if(!t.data)return;let o;try{o=t.data.json()}catch{o={title:t.data.text()||"Notification"}}const e=o.title||"Notification",c={campaign_id:o.campaign_id,action_url:o.action_url||o.url,action_type:null==(a=o.data)?void 0:a.action_type,action_payload:null==(n=o.data)?void 0:n.action_payload,timestamp:Date.now()},s={body:o.body||"",icon:o.icon||void 0,badge:o.badge||void 0,image:o.image||void 0,tag:o.tag||o.campaign_id||void 0,data:c,...Array.isArray(o.actions)&&o.actions.length>0?{actions:o.actions.slice(0,2)}:{},requireInteraction:o.require_interaction??!1};t.waitUntil(self.registration.showNotification(e,s).then(()=>{i({type:"push.delivered",campaign_id:o.campaign_id})}).catch(t=>(console.error("[aegis-sw] showNotification failed, retrying minimal:",t),self.registration.showNotification(e,{body:o.body||"",data:c}).then(()=>{i({type:"push.delivered",campaign_id:o.campaign_id,degraded:!0})}).catch(i=>{console.error("[aegis-sw] minimal showNotification also failed:",i)}))))}),self.addEventListener("notificationclick",t=>{t.notification.close();const a=t.notification.data||{},n=a.action_url||"/";let o=n;if(t.action&&Array.isArray(t.notification.actions)){if(t.notification.actions.find(i=>i.action===t.action)){const i=a.action_urls||{};o=i[t.action]||n}}t.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(n=>{const e=n.find(i=>new URL(i.url).origin===self.location.origin);return e?(e.postMessage({type:"push.clicked",campaign_id:a.campaign_id,action_url:o,action:t.action||"default",action_type:a.action_type,action_payload:a.action_payload}),e.focus()):(i({type:"push.clicked",campaign_id:a.campaign_id,action_url:o,action:t.action||"default",action_type:a.action_type,action_payload:a.action_payload}),self.clients.openWindow(o))}))}),self.addEventListener("notificationclose",t=>{i({type:"push.dismissed",campaign_id:(t.notification.data||{}).campaign_id})}),self.addEventListener("pushsubscriptionchange",t=>{var a,n;t.waitUntil(self.registration.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:(null==(n=null==(a=t.oldSubscription)?void 0:a.options)?void 0:n.applicationServerKey)||void 0}).then(t=>{i({type:"push.resubscribed",subscription:t.toJSON()})}).catch(i=>{console.error("[aegis-sw] Failed to resubscribe:",i)}))})}();
@@ -18,6 +18,77 @@ export interface AegisWebPushConfig {
18
18
  export interface PushEventTracker {
19
19
  track(eventName: string, properties: Record<string, unknown>): void;
20
20
  }
21
+ /**
22
+ * Payload for the `push-tap` lifecycle event. CANCELLABLE — return `false`
23
+ * (or call `evt.preventDefault()`) from a handler to suppress the SDK's
24
+ * default `window.location.href` fallback navigation. Use this to dispatch
25
+ * an in-place state change in a SPA host instead of a hard reload.
26
+ *
27
+ * `action_type` + `action_payload` are author-controlled fields baked into
28
+ * the push template's `data` block:
29
+ *
30
+ * ```json
31
+ * {
32
+ * "title": "Your cart is waiting",
33
+ * "body": "...",
34
+ * "action_url": "https://shop.actii.me/?step=cart",
35
+ * "data": {
36
+ * "action_type": "open_cart",
37
+ * "action_payload": { "step": "cart" }
38
+ * }
39
+ * }
40
+ * ```
41
+ */
42
+ export interface PushTapEvent {
43
+ /** Author-controlled action discriminator (e.g. `open_cart`,
44
+ * `open_payment_link`, `open_order`). When unset, the SDK runs its
45
+ * default fallback navigation to `action_url`. */
46
+ action_type?: string;
47
+ /** Optional structured args the host SPA forwards to its handler. */
48
+ action_payload?: Record<string, unknown>;
49
+ /** Sanitized destination URL — used as the SDK's default fallback nav
50
+ * target when no host handler cancels. */
51
+ action_url: string;
52
+ /** Action-button id when the user clicked a specific button on a
53
+ * multi-button notification; `'default'` for body taps. */
54
+ action: string;
55
+ /** Source campaign id from the push payload — useful for SPA routing
56
+ * (e.g. open_cart from a cart-recovery campaign vs from a generic
57
+ * catch-all template). */
58
+ campaign_id?: string;
59
+ /** Mutate via `evt.preventDefault()` to signal the SDK to skip its
60
+ * default hard-navigation. Identical semantics to DOM CustomEvent. */
61
+ preventDefault: () => void;
62
+ defaultPrevented: boolean;
63
+ }
64
+ /** Payload for `push-shown` — fires after `showNotification` resolves on
65
+ * the SW side. The `degraded` flag is true when the SW had to fall back
66
+ * to its minimal `{title, body}` retry path (broken icon URL etc.). */
67
+ export interface PushShownEvent {
68
+ campaign_id?: string;
69
+ degraded?: boolean;
70
+ }
71
+ /** Payload for `push-dismissed` — fires when the user closes the
72
+ * notification without clicking. */
73
+ export interface PushDismissEvent {
74
+ campaign_id?: string;
75
+ }
76
+ /** Payload for `error` — non-fatal SDK errors (subscribe failures,
77
+ * resubscribe persist failures, host-handler throws). Wire into Sentry
78
+ * / Datadog RUM. */
79
+ export interface PushErrorEvent {
80
+ message: string;
81
+ error: unknown;
82
+ context: Record<string, unknown>;
83
+ }
84
+ /** Map of lifecycle event name → handler signature. Mirrors 1.8.0
85
+ * in-app lifecycle for consistency. */
86
+ export interface WebPushLifecycleEventMap {
87
+ 'push-tap': (evt: PushTapEvent) => void | false;
88
+ 'push-shown': (evt: PushShownEvent) => void;
89
+ 'push-dismissed': (evt: PushDismissEvent) => void;
90
+ 'error': (evt: PushErrorEvent) => void;
91
+ }
21
92
  /**
22
93
  * AegisWebPush — Web Push notification manager.
23
94
  *
@@ -30,7 +101,29 @@ export declare class AegisWebPush {
30
101
  private swRegistration;
31
102
  private config;
32
103
  private initialized;
104
+ private hooks;
33
105
  constructor(apiClient: AegisAPIClient, config: AegisWebPushConfig, eventTracker?: PushEventTracker);
106
+ /** Generic typed subscribe — useful when the host wants to dispatch
107
+ * multiple events through one wrapper. For single-event subscriptions
108
+ * prefer the `on*` sugar methods below. */
109
+ on<E extends keyof WebPushLifecycleEventMap>(event: E, handler: WebPushLifecycleEventMap[E]): () => void;
110
+ /** Subscribe to push notification taps. CANCELLABLE — return `false`
111
+ * (or call `evt.preventDefault()`) to suppress the SDK's default
112
+ * `window.location.href = action_url` fallback. Use this to wire a
113
+ * SPA router into the typed `action_type` payload (e.g. open the
114
+ * cart drawer in-place instead of a full reload). */
115
+ onPushTap(handler: (evt: PushTapEvent) => void | false): () => void;
116
+ /** Subscribe to push impressions — fires after the OS notification
117
+ * has actually rendered. `degraded=true` when the SW fell back to
118
+ * the minimal `{title, body}` path (broken icon, etc.). */
119
+ onPushShown(handler: (evt: PushShownEvent) => void): () => void;
120
+ /** Subscribe to push dismissals (notification closed without a tap). */
121
+ onPushDismiss(handler: (evt: PushDismissEvent) => void): () => void;
122
+ /** Subscribe to non-fatal SDK errors. Wire into Sentry / Datadog. */
123
+ onError(handler: (evt: PushErrorEvent) => void): () => void;
124
+ private register;
125
+ private emit;
126
+ private emitError;
34
127
  initialize(): Promise<void>;
35
128
  requestPermission(): Promise<boolean>;
36
129
  identify(params: ContactIdentity): Promise<void>;
@@ -58,6 +151,18 @@ export declare class AegisWebPush {
58
151
  * Handle messages forwarded from the service worker (push click, dismiss).
59
152
  */
60
153
  private handleSWMessage;
154
+ /**
155
+ * Translate a SW `push.clicked` postMessage into a typed PushTapEvent,
156
+ * dispatch through the lifecycle hook bus, and fall back to a hard
157
+ * navigation if no host handler claimed it.
158
+ *
159
+ * The fallback path matters: if a tenant ships a push template with
160
+ * an unknown `action_type` (no SPA handler registered) OR pushes the
161
+ * SDK before the host wires up its `onPushTap` subscriber, we still
162
+ * want the user to land somewhere — `action_url` is always set by the
163
+ * SW's notificationclick handler (it falls back to '/').
164
+ */
165
+ private dispatchPushTap;
61
166
  private persistResubscription;
62
167
  private trackPushEvent;
63
168
  private getBrowserInfo;
@@ -1 +1 @@
1
- {"version":3,"file":"AegisWebPush.d.ts","sourceRoot":"","sources":["../../src/push/AegisWebPush.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC3B,IAAI,CACA,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAC/B,cAAc,EAAE,MAAM,CAAC;IAKvB,UAAU,EAAE,MAAM,CAAC;IAInB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC7B,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACvE;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IACrB,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,cAAc,CAA0C;IAChE,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,WAAW,CAAS;gBAGxB,SAAS,EAAE,cAAc,EACzB,MAAM,EAAE,kBAAkB,EAC1B,YAAY,CAAC,EAAE,gBAAgB;IAa7B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAwD3B,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC;IAYrC,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAmChD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;YA4Bf,eAAe;IAiC7B;;;;;OAKG;YACW,oBAAoB;IAuBlC;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;OAEG;YACW,YAAY;IAgB1B;;OAEG;IACH,OAAO,CAAC,eAAe;YAkCT,qBAAqB;IA0BnC,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,qBAAqB;CAUhC"}
1
+ {"version":3,"file":"AegisWebPush.d.ts","sourceRoot":"","sources":["../../src/push/AegisWebPush.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@ class AegisWebPush {
2
2
  constructor(apiClient, config, eventTracker) {
3
3
  this.swRegistration = null;
4
4
  this.initialized = false;
5
+ this.hooks = /* @__PURE__ */ new Map();
5
6
  this.apiClient = apiClient;
6
7
  this.config = {
7
8
  serviceWorkerPath: "/aegis-sw.js",
@@ -12,6 +13,78 @@ class AegisWebPush {
12
13
  };
13
14
  this.eventTracker = eventTracker ?? null;
14
15
  }
16
+ // ── Public lifecycle hook API ──────────────────────────────────────────
17
+ /** Generic typed subscribe — useful when the host wants to dispatch
18
+ * multiple events through one wrapper. For single-event subscriptions
19
+ * prefer the `on*` sugar methods below. */
20
+ on(event, handler) {
21
+ return this.register(event, handler);
22
+ }
23
+ /** Subscribe to push notification taps. CANCELLABLE — return `false`
24
+ * (or call `evt.preventDefault()`) to suppress the SDK's default
25
+ * `window.location.href = action_url` fallback. Use this to wire a
26
+ * SPA router into the typed `action_type` payload (e.g. open the
27
+ * cart drawer in-place instead of a full reload). */
28
+ onPushTap(handler) {
29
+ return this.register("push-tap", handler);
30
+ }
31
+ /** Subscribe to push impressions — fires after the OS notification
32
+ * has actually rendered. `degraded=true` when the SW fell back to
33
+ * the minimal `{title, body}` path (broken icon, etc.). */
34
+ onPushShown(handler) {
35
+ return this.register("push-shown", handler);
36
+ }
37
+ /** Subscribe to push dismissals (notification closed without a tap). */
38
+ onPushDismiss(handler) {
39
+ return this.register("push-dismissed", handler);
40
+ }
41
+ /** Subscribe to non-fatal SDK errors. Wire into Sentry / Datadog. */
42
+ onError(handler) {
43
+ return this.register("error", handler);
44
+ }
45
+ register(eventName, handler) {
46
+ if (!this.hooks.has(eventName)) this.hooks.set(eventName, /* @__PURE__ */ new Set());
47
+ this.hooks.get(eventName).add(handler);
48
+ return () => {
49
+ var _a;
50
+ (_a = this.hooks.get(eventName)) == null ? void 0 : _a.delete(handler);
51
+ };
52
+ }
53
+ emit(eventName, payload) {
54
+ const handlers = this.hooks.get(eventName);
55
+ if (!handlers || handlers.size === 0) return true;
56
+ let proceed = true;
57
+ for (const handler of handlers) {
58
+ try {
59
+ const result = handler(payload);
60
+ if (result === false) proceed = false;
61
+ } catch (err) {
62
+ this.emitError(err, { event: eventName });
63
+ }
64
+ }
65
+ return proceed;
66
+ }
67
+ emitError(err, context) {
68
+ const handlers = this.hooks.get("error");
69
+ if (!handlers || handlers.size === 0) {
70
+ console.warn(
71
+ "[AegisWebPush] error:",
72
+ err instanceof Error ? err.message : String(err),
73
+ context ?? {}
74
+ );
75
+ return;
76
+ }
77
+ for (const handler of handlers) {
78
+ try {
79
+ handler({
80
+ message: err instanceof Error ? err.message : String(err),
81
+ error: err,
82
+ context: context ?? {}
83
+ });
84
+ } catch {
85
+ }
86
+ }
87
+ }
15
88
  async initialize() {
16
89
  if (this.initialized) return;
17
90
  if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
@@ -192,21 +265,31 @@ class AegisWebPush {
192
265
  handleSWMessage(data) {
193
266
  if (!data || typeof data.type !== "string") return;
194
267
  switch (data.type) {
195
- case "push.clicked":
268
+ case "push.clicked": {
196
269
  this.trackPushEvent("push.clicked", {
197
270
  campaign_id: data.campaign_id,
198
- action_url: data.action_url
271
+ action_url: data.action_url,
272
+ action_type: data.action_type
199
273
  });
274
+ this.dispatchPushTap(data);
200
275
  break;
276
+ }
201
277
  case "push.dismissed":
202
278
  this.trackPushEvent("push.dismissed", {
203
279
  campaign_id: data.campaign_id
204
280
  });
281
+ this.emit("push-dismissed", {
282
+ campaign_id: data.campaign_id
283
+ });
205
284
  break;
206
285
  case "push.delivered":
207
286
  this.trackPushEvent("push.delivered", {
208
287
  campaign_id: data.campaign_id
209
288
  });
289
+ this.emit("push-shown", {
290
+ campaign_id: data.campaign_id,
291
+ degraded: data.degraded === true ? true : void 0
292
+ });
210
293
  break;
211
294
  case "push.resubscribed":
212
295
  void this.persistResubscription(
@@ -215,6 +298,41 @@ class AegisWebPush {
215
298
  break;
216
299
  }
217
300
  }
301
+ /**
302
+ * Translate a SW `push.clicked` postMessage into a typed PushTapEvent,
303
+ * dispatch through the lifecycle hook bus, and fall back to a hard
304
+ * navigation if no host handler claimed it.
305
+ *
306
+ * The fallback path matters: if a tenant ships a push template with
307
+ * an unknown `action_type` (no SPA handler registered) OR pushes the
308
+ * SDK before the host wires up its `onPushTap` subscriber, we still
309
+ * want the user to land somewhere — `action_url` is always set by the
310
+ * SW's notificationclick handler (it falls back to '/').
311
+ */
312
+ dispatchPushTap(data) {
313
+ if (typeof window === "undefined") return;
314
+ const actionUrl = data.action_url || "/";
315
+ let defaultPrevented = false;
316
+ const evt = {
317
+ action_type: data.action_type,
318
+ action_payload: data.action_payload,
319
+ action_url: actionUrl,
320
+ action: data.action || "default",
321
+ campaign_id: data.campaign_id,
322
+ preventDefault: () => {
323
+ defaultPrevented = true;
324
+ evt.defaultPrevented = true;
325
+ },
326
+ defaultPrevented: false
327
+ };
328
+ const proceed = this.emit("push-tap", evt);
329
+ if (proceed && !defaultPrevented) {
330
+ try {
331
+ window.location.href = actionUrl;
332
+ } catch {
333
+ }
334
+ }
335
+ }
218
336
  async persistResubscription(subscription) {
219
337
  if (!subscription || !subscription.endpoint || !subscription.keys) return;
220
338
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"AegisWebPush.js","sources":["../../src/push/AegisWebPush.ts"],"sourcesContent":["export interface AegisAPIClient {\n post(\n endpoint: string,\n payload: Record<string, unknown>,\n headers?: Record<string, string>\n ): Promise<unknown>;\n}\n\nexport interface ContactIdentity {\n contactId?: string;\n shopifyCustomerId?: string;\n email?: string;\n}\n\nexport interface AegisWebPushConfig {\n vapidPublicKey: string;\n // SubscriptionProperty binding — supplied by the embedding host (e.g.\n // the storefront SSR via the `sdk.property_id` field on /store/{slug}).\n // Required: every subscribe call sends it back as `X-Property-Id` so\n // the backend pins the row to the right origin.\n propertyId: string;\n // Stable per-browser cookie id (e.g. aegis_fpc). Used as the\n // device-fingerprint salt so a fresh subscribe after VAPID rotation\n // upserts onto the same `web_push_subscriptions` row.\n firstPartyCookieId: string;\n autoPrompt?: boolean;\n promptDelay?: number;\n serviceWorkerPath?: string;\n serviceWorkerScope?: string;\n}\n\nexport interface PushEventTracker {\n track(eventName: string, properties: Record<string, unknown>): void;\n}\n\n/**\n * AegisWebPush — Web Push notification manager.\n *\n * Handles service worker registration, push subscription via VAPID,\n * token registration with the backend, and push event tracking.\n */\nexport class AegisWebPush {\n private apiClient: AegisAPIClient;\n private eventTracker: PushEventTracker | null;\n private swRegistration: ServiceWorkerRegistration | null = null;\n private config: AegisWebPushConfig;\n private initialized = false;\n\n constructor(\n apiClient: AegisAPIClient,\n config: AegisWebPushConfig,\n eventTracker?: PushEventTracker\n ) {\n this.apiClient = apiClient;\n this.config = {\n serviceWorkerPath: '/aegis-sw.js',\n serviceWorkerScope: '/',\n autoPrompt: false,\n promptDelay: 5000,\n ...config,\n };\n this.eventTracker = eventTracker ?? null;\n }\n\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n if (!('serviceWorker' in navigator) || !('PushManager' in window)) {\n console.warn('[AegisWebPush] Push notifications not supported in this browser');\n return;\n }\n\n try {\n this.swRegistration = await navigator.serviceWorker.register(\n this.config.serviceWorkerPath!,\n { scope: this.config.serviceWorkerScope }\n );\n await navigator.serviceWorker.ready;\n this.initialized = true;\n\n // Listen for messages from the service worker (click/dismiss tracking,\n // and pushsubscriptionchange relay — see handleSWMessage).\n navigator.serviceWorker.addEventListener('message', (event) => {\n this.handleSWMessage(event.data);\n });\n\n // Heal endpoint drift on every page load.\n //\n // Background: when the SW updates with skipWaiting+clients.claim\n // (added in 1.7.3), some browsers — Samsung Internet most\n // aggressively, occasionally Chrome too — silently regenerate the\n // push subscription mid-flight. The SW catches the\n // pushsubscriptionchange event and resubscribes locally, but\n // historically the new endpoint never made it back to the\n // backend. Cell-plane kept FCM-pushing to the dead endpoint;\n // FCM 201s on receipt regardless of whether the token is alive,\n // so the silent failure was invisible until users complained.\n //\n // Fix: whenever permission is already granted at init time,\n // re-run subscribeToPush. The pushManager.subscribe() call is\n // idempotent — it returns the existing subscription if the\n // endpoint is still valid, otherwise mints a new one — and we\n // POST the result to /v1/web_push/subscriptions which UPSERTs\n // on (property_id, device_fingerprint). One extra round-trip\n // per page load; cheap, deterministic, never need to debug\n // stale-endpoint mysteries again.\n if (typeof window !== 'undefined' && Notification.permission === 'granted') {\n this.subscribeToPush().catch((err) => {\n console.warn('[AegisWebPush] auto-resync subscribeToPush failed:', err);\n });\n }\n\n if (this.config.autoPrompt) {\n setTimeout(() => this.requestPermission(), this.config.promptDelay);\n }\n } catch (err) {\n console.error('[AegisWebPush] Service worker registration failed:', err);\n }\n }\n\n async requestPermission(): Promise<boolean> {\n const permission = await Notification.requestPermission();\n\n if (permission === 'granted') {\n await this.subscribeToPush();\n return true;\n }\n\n this.trackPushEvent('push.permission_denied', {});\n return false;\n }\n\n async identify(params: ContactIdentity): Promise<void> {\n // Persist the resolved contact_id so getStableFingerprint /\n // resolveContactId see it on the next subscribe call. Then re-run\n // the subscribe flow — the backend UPSERT on\n // (property_id, device_fingerprint) updates the existing row with\n // the new contact_id in place.\n try {\n if (params.contactId) {\n window.localStorage.setItem('aegis_contact_id', params.contactId);\n }\n } catch {\n // storage blocked — proceed anyway; subscribe() still works\n }\n\n const subscription = await this.swRegistration?.pushManager.getSubscription();\n if (!subscription) return;\n\n const subscriptionJSON = subscription.toJSON();\n const fingerprint = await this.getStableFingerprint();\n await this.retryApiCall(() =>\n this.apiClient.post(\n '/v1/web_push/subscriptions',\n {\n contact_id: params.contactId ?? this.resolveContactId(),\n endpoint: subscriptionJSON.endpoint!,\n p256dh: subscriptionJSON.keys!.p256dh!,\n auth: subscriptionJSON.keys!.auth!,\n device_fingerprint: fingerprint,\n user_agent: navigator.userAgent,\n },\n { 'X-Property-Id': this.config.propertyId }\n )\n );\n }\n\n async logout(): Promise<void> {\n const subscription = await this.swRegistration?.pushManager.getSubscription();\n if (!subscription) return;\n const subscriptionJSON = subscription.toJSON();\n const fingerprint = await this.getStableFingerprint();\n try {\n await this.apiClient.post(\n '/v1/web_push/unsubscribe',\n {\n contact_id: this.resolveContactId(),\n device_fingerprint: fingerprint,\n endpoint: subscriptionJSON.endpoint!,\n },\n { 'X-Property-Id': this.config.propertyId }\n );\n } finally {\n try {\n window.localStorage.removeItem('aegis_contact_id');\n } catch {\n // ignore\n }\n }\n }\n\n // ---------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------\n\n private async subscribeToPush(): Promise<void> {\n if (!this.swRegistration) return;\n\n const subscription = await this.swRegistration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: this.urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n\n const subscriptionJSON = subscription.toJSON();\n const fingerprint = await this.getStableFingerprint();\n const contactId = this.resolveContactId();\n\n await this.retryApiCall(() =>\n this.apiClient.post(\n '/v1/web_push/subscriptions',\n {\n contact_id: contactId,\n endpoint: subscriptionJSON.endpoint!,\n p256dh: subscriptionJSON.keys!.p256dh!,\n auth: subscriptionJSON.keys!.auth!,\n device_fingerprint: fingerprint,\n user_agent: navigator.userAgent,\n },\n { 'X-Property-Id': this.config.propertyId }\n )\n );\n\n this.trackPushEvent('push.subscribed', {\n browser: this.getBrowserInfo(),\n property_id: this.config.propertyId,\n });\n }\n\n /**\n * Stable SHA-256 device fingerprint over (first_party_cookie_id + UA stable\n * parts). Used as the UPSERT key on `(property_id, device_fingerprint)` —\n * re-subscribes after VAPID rotation or permission reset update the same\n * row instead of leaving dangling duplicates.\n */\n private async getStableFingerprint(): Promise<string> {\n const cookieId = this.config.firstPartyCookieId;\n const ua = navigator.userAgent || '';\n const platform = navigator.platform || '';\n const language = navigator.language || '';\n const input = `${cookieId}|${ua}|${platform}|${language}`;\n if (typeof crypto !== 'undefined' && crypto.subtle) {\n const bytes = new TextEncoder().encode(input);\n const hash = await crypto.subtle.digest('SHA-256', bytes);\n return Array.from(new Uint8Array(hash))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n }\n // Fallback only reachable in environments without Web Crypto —\n // push-capable browsers all have it, so this is defensive.\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n return `fnv-${h.toString(16)}`;\n }\n\n /**\n * Resolve the current contact_id. AegisWebPush doesn't own identity, so\n * we look in a few well-known localStorage keys that the identity layer\n * of the SDK writes. If no known contact is set, we use the first-party\n * cookie id — the backend upgrades the row when identify() is called.\n */\n private resolveContactId(): string {\n try {\n const fromStorage =\n window.localStorage.getItem('aegis_contact_id') ||\n window.localStorage.getItem('aegis_user_id');\n if (fromStorage) return fromStorage;\n } catch {\n // storage blocked — fall through\n }\n return this.config.firstPartyCookieId;\n }\n\n /**\n * Retry an API call with exponential backoff (max 3 retries: 1s, 2s, 4s).\n */\n private async retryApiCall(fn: () => Promise<unknown>, maxRetries = 3): Promise<void> {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n await fn();\n return;\n } catch (err) {\n if (attempt === maxRetries) {\n console.error('[AegisWebPush] API call failed after retries:', err);\n return;\n }\n const delay = 1000 * Math.pow(2, attempt);\n await new Promise((r) => setTimeout(r, delay));\n }\n }\n }\n\n /**\n * Handle messages forwarded from the service worker (push click, dismiss).\n */\n private handleSWMessage(data: Record<string, unknown>): void {\n if (!data || typeof data.type !== 'string') return;\n\n switch (data.type) {\n case 'push.clicked':\n this.trackPushEvent('push.clicked', {\n campaign_id: data.campaign_id,\n action_url: data.action_url,\n });\n break;\n case 'push.dismissed':\n this.trackPushEvent('push.dismissed', {\n campaign_id: data.campaign_id,\n });\n break;\n case 'push.delivered':\n this.trackPushEvent('push.delivered', {\n campaign_id: data.campaign_id,\n });\n break;\n case 'push.resubscribed':\n // SW caught a pushsubscriptionchange event and minted a new\n // push subscription. The browser side now has a fresh\n // endpoint; cell-plane still has the old one. Push the\n // updated payload up so the UPSERT keeps the row alive.\n // Without this, FCM pushes go to a dead endpoint with no\n // visible failure (FCM 201s regardless of token validity).\n void this.persistResubscription(\n data.subscription as PushSubscriptionJSON | undefined\n );\n break;\n }\n }\n\n private async persistResubscription(\n subscription: PushSubscriptionJSON | undefined\n ): Promise<void> {\n if (!subscription || !subscription.endpoint || !subscription.keys) return;\n try {\n const fingerprint = await this.getStableFingerprint();\n await this.retryApiCall(() =>\n this.apiClient.post(\n '/v1/web_push/subscriptions',\n {\n contact_id: this.resolveContactId(),\n endpoint: subscription.endpoint!,\n p256dh: subscription.keys!.p256dh!,\n auth: subscription.keys!.auth!,\n device_fingerprint: fingerprint,\n user_agent: navigator.userAgent,\n },\n { 'X-Property-Id': this.config.propertyId }\n )\n );\n this.trackPushEvent('push.resubscribed', {});\n } catch (err) {\n console.warn('[AegisWebPush] persistResubscription failed:', err);\n }\n }\n\n private trackPushEvent(eventName: string, properties: Record<string, unknown>): void {\n this.eventTracker?.track(eventName, {\n ...properties,\n platform: 'web',\n browser: this.getBrowserInfo(),\n });\n }\n\n private getBrowserInfo(): string {\n const ua = navigator.userAgent;\n if (ua.includes('Edg')) return 'edge';\n if (ua.includes('Chrome')) return 'chrome';\n if (ua.includes('Firefox')) return 'firefox';\n if (ua.includes('Safari')) return 'safari';\n return 'unknown';\n }\n\n private urlBase64ToUint8Array(base64String: string): Uint8Array {\n const padding = '='.repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');\n const rawData = atob(base64);\n const arr = new Uint8Array(rawData.length);\n for (let i = 0; i < rawData.length; ++i) {\n arr[i] = rawData.charCodeAt(i);\n }\n return arr;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AegisWebPush.js","sources":["../../src/push/AegisWebPush.ts"],"sourcesContent":["export interface AegisAPIClient {\n post(\n endpoint: string,\n payload: Record<string, unknown>,\n headers?: Record<string, string>\n ): Promise<unknown>;\n}\n\nexport interface ContactIdentity {\n contactId?: string;\n shopifyCustomerId?: string;\n email?: string;\n}\n\nexport interface AegisWebPushConfig {\n vapidPublicKey: string;\n // SubscriptionProperty binding — supplied by the embedding host (e.g.\n // the storefront SSR via the `sdk.property_id` field on /store/{slug}).\n // Required: every subscribe call sends it back as `X-Property-Id` so\n // the backend pins the row to the right origin.\n propertyId: string;\n // Stable per-browser cookie id (e.g. aegis_fpc). Used as the\n // device-fingerprint salt so a fresh subscribe after VAPID rotation\n // upserts onto the same `web_push_subscriptions` row.\n firstPartyCookieId: string;\n autoPrompt?: boolean;\n promptDelay?: number;\n serviceWorkerPath?: string;\n serviceWorkerScope?: string;\n}\n\nexport interface PushEventTracker {\n track(eventName: string, properties: Record<string, unknown>): void;\n}\n\n/**\n * Payload for the `push-tap` lifecycle event. CANCELLABLE — return `false`\n * (or call `evt.preventDefault()`) from a handler to suppress the SDK's\n * default `window.location.href` fallback navigation. Use this to dispatch\n * an in-place state change in a SPA host instead of a hard reload.\n *\n * `action_type` + `action_payload` are author-controlled fields baked into\n * the push template's `data` block:\n *\n * ```json\n * {\n * \"title\": \"Your cart is waiting\",\n * \"body\": \"...\",\n * \"action_url\": \"https://shop.actii.me/?step=cart\",\n * \"data\": {\n * \"action_type\": \"open_cart\",\n * \"action_payload\": { \"step\": \"cart\" }\n * }\n * }\n * ```\n */\nexport interface PushTapEvent {\n /** Author-controlled action discriminator (e.g. `open_cart`,\n * `open_payment_link`, `open_order`). When unset, the SDK runs its\n * default fallback navigation to `action_url`. */\n action_type?: string;\n /** Optional structured args the host SPA forwards to its handler. */\n action_payload?: Record<string, unknown>;\n /** Sanitized destination URL — used as the SDK's default fallback nav\n * target when no host handler cancels. */\n action_url: string;\n /** Action-button id when the user clicked a specific button on a\n * multi-button notification; `'default'` for body taps. */\n action: string;\n /** Source campaign id from the push payload — useful for SPA routing\n * (e.g. open_cart from a cart-recovery campaign vs from a generic\n * catch-all template). */\n campaign_id?: string;\n /** Mutate via `evt.preventDefault()` to signal the SDK to skip its\n * default hard-navigation. Identical semantics to DOM CustomEvent. */\n preventDefault: () => void;\n defaultPrevented: boolean;\n}\n\n/** Payload for `push-shown` — fires after `showNotification` resolves on\n * the SW side. The `degraded` flag is true when the SW had to fall back\n * to its minimal `{title, body}` retry path (broken icon URL etc.). */\nexport interface PushShownEvent {\n campaign_id?: string;\n degraded?: boolean;\n}\n\n/** Payload for `push-dismissed` — fires when the user closes the\n * notification without clicking. */\nexport interface PushDismissEvent {\n campaign_id?: string;\n}\n\n/** Payload for `error` — non-fatal SDK errors (subscribe failures,\n * resubscribe persist failures, host-handler throws). Wire into Sentry\n * / Datadog RUM. */\nexport interface PushErrorEvent {\n message: string;\n error: unknown;\n context: Record<string, unknown>;\n}\n\n/** Map of lifecycle event name → handler signature. Mirrors 1.8.0\n * in-app lifecycle for consistency. */\nexport interface WebPushLifecycleEventMap {\n 'push-tap': (evt: PushTapEvent) => void | false;\n 'push-shown': (evt: PushShownEvent) => void;\n 'push-dismissed': (evt: PushDismissEvent) => void;\n 'error': (evt: PushErrorEvent) => void;\n}\n\n/**\n * AegisWebPush — Web Push notification manager.\n *\n * Handles service worker registration, push subscription via VAPID,\n * token registration with the backend, and push event tracking.\n */\nexport class AegisWebPush {\n private apiClient: AegisAPIClient;\n private eventTracker: PushEventTracker | null;\n private swRegistration: ServiceWorkerRegistration | null = null;\n private config: AegisWebPushConfig;\n private initialized = false;\n\n // ── Lifecycle event bus (1.8.5) ────────────────────────────────────────\n // Typed handler registry, parity with AegisInAppManager (1.8.0).\n // Hosts register via the public `on*` methods below; calling the\n // returned unsubscribe fn removes the handler. Multiple subscribers per\n // event are supported. Cancellable events (`push-tap`) suppress the\n // SDK's default behaviour when a handler returns `false` or calls\n // `evt.preventDefault()`.\n //\n // Industry parity: matches CleverTap `notificationCallback`, OneSignal\n // `addEventListener('notificationOpened')`, MoEngage\n // `onSelfHandledClick`, WebEngage `notificationCallback`. The SW posts\n // a `push.clicked` message; this class translates that into a typed\n // PushTapEvent with cancellable default-nav behaviour.\n private hooks = new Map<string, Set<(...args: unknown[]) => void | false | undefined>>();\n\n constructor(\n apiClient: AegisAPIClient,\n config: AegisWebPushConfig,\n eventTracker?: PushEventTracker\n ) {\n this.apiClient = apiClient;\n this.config = {\n serviceWorkerPath: '/aegis-sw.js',\n serviceWorkerScope: '/',\n autoPrompt: false,\n promptDelay: 5000,\n ...config,\n };\n this.eventTracker = eventTracker ?? null;\n }\n\n // ── Public lifecycle hook API ──────────────────────────────────────────\n\n /** Generic typed subscribe — useful when the host wants to dispatch\n * multiple events through one wrapper. For single-event subscriptions\n * prefer the `on*` sugar methods below. */\n on<E extends keyof WebPushLifecycleEventMap>(\n event: E,\n handler: WebPushLifecycleEventMap[E],\n ): () => void {\n return this.register(event, handler as never);\n }\n\n /** Subscribe to push notification taps. CANCELLABLE — return `false`\n * (or call `evt.preventDefault()`) to suppress the SDK's default\n * `window.location.href = action_url` fallback. Use this to wire a\n * SPA router into the typed `action_type` payload (e.g. open the\n * cart drawer in-place instead of a full reload). */\n onPushTap(\n handler: (evt: PushTapEvent) => void | false,\n ): () => void {\n return this.register('push-tap', handler);\n }\n\n /** Subscribe to push impressions — fires after the OS notification\n * has actually rendered. `degraded=true` when the SW fell back to\n * the minimal `{title, body}` path (broken icon, etc.). */\n onPushShown(\n handler: (evt: PushShownEvent) => void,\n ): () => void {\n return this.register('push-shown', handler);\n }\n\n /** Subscribe to push dismissals (notification closed without a tap). */\n onPushDismiss(\n handler: (evt: PushDismissEvent) => void,\n ): () => void {\n return this.register('push-dismissed', handler);\n }\n\n /** Subscribe to non-fatal SDK errors. Wire into Sentry / Datadog. */\n onError(\n handler: (evt: PushErrorEvent) => void,\n ): () => void {\n return this.register('error', handler);\n }\n\n private register<T>(\n eventName: string,\n handler: (payload: T) => void | false | undefined,\n ): () => void {\n if (!this.hooks.has(eventName)) this.hooks.set(eventName, new Set());\n this.hooks.get(eventName)!.add(handler as (...args: unknown[]) => void | false | undefined);\n return () => {\n this.hooks.get(eventName)?.delete(handler as (...args: unknown[]) => void | false | undefined);\n };\n }\n\n private emit<T>(eventName: string, payload: T): boolean {\n const handlers = this.hooks.get(eventName);\n if (!handlers || handlers.size === 0) return true;\n let proceed = true;\n for (const handler of handlers) {\n try {\n const result = (handler as (p: T) => void | false | undefined)(payload);\n if (result === false) proceed = false;\n } catch (err) {\n this.emitError(err, { event: eventName });\n }\n }\n return proceed;\n }\n\n private emitError(err: unknown, context?: Record<string, unknown>): void {\n const handlers = this.hooks.get('error');\n if (!handlers || handlers.size === 0) {\n console.warn(\n '[AegisWebPush] error:',\n err instanceof Error ? err.message : String(err),\n context ?? {}\n );\n return;\n }\n for (const handler of handlers) {\n try {\n (handler as (e: PushErrorEvent) => void)({\n message: err instanceof Error ? err.message : String(err),\n error: err,\n context: context ?? {},\n });\n } catch {\n // already in error path — don't loop\n }\n }\n }\n\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n if (!('serviceWorker' in navigator) || !('PushManager' in window)) {\n console.warn('[AegisWebPush] Push notifications not supported in this browser');\n return;\n }\n\n try {\n this.swRegistration = await navigator.serviceWorker.register(\n this.config.serviceWorkerPath!,\n { scope: this.config.serviceWorkerScope }\n );\n await navigator.serviceWorker.ready;\n this.initialized = true;\n\n // Listen for messages from the service worker (click/dismiss tracking,\n // and pushsubscriptionchange relay — see handleSWMessage).\n navigator.serviceWorker.addEventListener('message', (event) => {\n this.handleSWMessage(event.data);\n });\n\n // Heal endpoint drift on every page load.\n //\n // Background: when the SW updates with skipWaiting+clients.claim\n // (added in 1.7.3), some browsers — Samsung Internet most\n // aggressively, occasionally Chrome too — silently regenerate the\n // push subscription mid-flight. The SW catches the\n // pushsubscriptionchange event and resubscribes locally, but\n // historically the new endpoint never made it back to the\n // backend. Cell-plane kept FCM-pushing to the dead endpoint;\n // FCM 201s on receipt regardless of whether the token is alive,\n // so the silent failure was invisible until users complained.\n //\n // Fix: whenever permission is already granted at init time,\n // re-run subscribeToPush. The pushManager.subscribe() call is\n // idempotent — it returns the existing subscription if the\n // endpoint is still valid, otherwise mints a new one — and we\n // POST the result to /v1/web_push/subscriptions which UPSERTs\n // on (property_id, device_fingerprint). One extra round-trip\n // per page load; cheap, deterministic, never need to debug\n // stale-endpoint mysteries again.\n if (typeof window !== 'undefined' && Notification.permission === 'granted') {\n this.subscribeToPush().catch((err) => {\n console.warn('[AegisWebPush] auto-resync subscribeToPush failed:', err);\n });\n }\n\n if (this.config.autoPrompt) {\n setTimeout(() => this.requestPermission(), this.config.promptDelay);\n }\n } catch (err) {\n console.error('[AegisWebPush] Service worker registration failed:', err);\n }\n }\n\n async requestPermission(): Promise<boolean> {\n const permission = await Notification.requestPermission();\n\n if (permission === 'granted') {\n await this.subscribeToPush();\n return true;\n }\n\n this.trackPushEvent('push.permission_denied', {});\n return false;\n }\n\n async identify(params: ContactIdentity): Promise<void> {\n // Persist the resolved contact_id so getStableFingerprint /\n // resolveContactId see it on the next subscribe call. Then re-run\n // the subscribe flow — the backend UPSERT on\n // (property_id, device_fingerprint) updates the existing row with\n // the new contact_id in place.\n try {\n if (params.contactId) {\n window.localStorage.setItem('aegis_contact_id', params.contactId);\n }\n } catch {\n // storage blocked — proceed anyway; subscribe() still works\n }\n\n const subscription = await this.swRegistration?.pushManager.getSubscription();\n if (!subscription) return;\n\n const subscriptionJSON = subscription.toJSON();\n const fingerprint = await this.getStableFingerprint();\n await this.retryApiCall(() =>\n this.apiClient.post(\n '/v1/web_push/subscriptions',\n {\n contact_id: params.contactId ?? this.resolveContactId(),\n endpoint: subscriptionJSON.endpoint!,\n p256dh: subscriptionJSON.keys!.p256dh!,\n auth: subscriptionJSON.keys!.auth!,\n device_fingerprint: fingerprint,\n user_agent: navigator.userAgent,\n },\n { 'X-Property-Id': this.config.propertyId }\n )\n );\n }\n\n async logout(): Promise<void> {\n const subscription = await this.swRegistration?.pushManager.getSubscription();\n if (!subscription) return;\n const subscriptionJSON = subscription.toJSON();\n const fingerprint = await this.getStableFingerprint();\n try {\n await this.apiClient.post(\n '/v1/web_push/unsubscribe',\n {\n contact_id: this.resolveContactId(),\n device_fingerprint: fingerprint,\n endpoint: subscriptionJSON.endpoint!,\n },\n { 'X-Property-Id': this.config.propertyId }\n );\n } finally {\n try {\n window.localStorage.removeItem('aegis_contact_id');\n } catch {\n // ignore\n }\n }\n }\n\n // ---------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------\n\n private async subscribeToPush(): Promise<void> {\n if (!this.swRegistration) return;\n\n const subscription = await this.swRegistration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: this.urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n\n const subscriptionJSON = subscription.toJSON();\n const fingerprint = await this.getStableFingerprint();\n const contactId = this.resolveContactId();\n\n await this.retryApiCall(() =>\n this.apiClient.post(\n '/v1/web_push/subscriptions',\n {\n contact_id: contactId,\n endpoint: subscriptionJSON.endpoint!,\n p256dh: subscriptionJSON.keys!.p256dh!,\n auth: subscriptionJSON.keys!.auth!,\n device_fingerprint: fingerprint,\n user_agent: navigator.userAgent,\n },\n { 'X-Property-Id': this.config.propertyId }\n )\n );\n\n this.trackPushEvent('push.subscribed', {\n browser: this.getBrowserInfo(),\n property_id: this.config.propertyId,\n });\n }\n\n /**\n * Stable SHA-256 device fingerprint over (first_party_cookie_id + UA stable\n * parts). Used as the UPSERT key on `(property_id, device_fingerprint)` —\n * re-subscribes after VAPID rotation or permission reset update the same\n * row instead of leaving dangling duplicates.\n */\n private async getStableFingerprint(): Promise<string> {\n const cookieId = this.config.firstPartyCookieId;\n const ua = navigator.userAgent || '';\n const platform = navigator.platform || '';\n const language = navigator.language || '';\n const input = `${cookieId}|${ua}|${platform}|${language}`;\n if (typeof crypto !== 'undefined' && crypto.subtle) {\n const bytes = new TextEncoder().encode(input);\n const hash = await crypto.subtle.digest('SHA-256', bytes);\n return Array.from(new Uint8Array(hash))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n }\n // Fallback only reachable in environments without Web Crypto —\n // push-capable browsers all have it, so this is defensive.\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n return `fnv-${h.toString(16)}`;\n }\n\n /**\n * Resolve the current contact_id. AegisWebPush doesn't own identity, so\n * we look in a few well-known localStorage keys that the identity layer\n * of the SDK writes. If no known contact is set, we use the first-party\n * cookie id — the backend upgrades the row when identify() is called.\n */\n private resolveContactId(): string {\n try {\n const fromStorage =\n window.localStorage.getItem('aegis_contact_id') ||\n window.localStorage.getItem('aegis_user_id');\n if (fromStorage) return fromStorage;\n } catch {\n // storage blocked — fall through\n }\n return this.config.firstPartyCookieId;\n }\n\n /**\n * Retry an API call with exponential backoff (max 3 retries: 1s, 2s, 4s).\n */\n private async retryApiCall(fn: () => Promise<unknown>, maxRetries = 3): Promise<void> {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n await fn();\n return;\n } catch (err) {\n if (attempt === maxRetries) {\n console.error('[AegisWebPush] API call failed after retries:', err);\n return;\n }\n const delay = 1000 * Math.pow(2, attempt);\n await new Promise((r) => setTimeout(r, delay));\n }\n }\n }\n\n /**\n * Handle messages forwarded from the service worker (push click, dismiss).\n */\n private handleSWMessage(data: Record<string, unknown>): void {\n if (!data || typeof data.type !== 'string') return;\n\n switch (data.type) {\n case 'push.clicked': {\n this.trackPushEvent('push.clicked', {\n campaign_id: data.campaign_id,\n action_url: data.action_url,\n action_type: data.action_type,\n });\n this.dispatchPushTap(data);\n break;\n }\n case 'push.dismissed':\n this.trackPushEvent('push.dismissed', {\n campaign_id: data.campaign_id,\n });\n this.emit('push-dismissed', {\n campaign_id: data.campaign_id as string | undefined,\n });\n break;\n case 'push.delivered':\n this.trackPushEvent('push.delivered', {\n campaign_id: data.campaign_id,\n });\n this.emit('push-shown', {\n campaign_id: data.campaign_id as string | undefined,\n degraded: data.degraded === true ? true : undefined,\n });\n break;\n case 'push.resubscribed':\n // SW caught a pushsubscriptionchange event and minted a new\n // push subscription. The browser side now has a fresh\n // endpoint; cell-plane still has the old one. Push the\n // updated payload up so the UPSERT keeps the row alive.\n // Without this, FCM pushes go to a dead endpoint with no\n // visible failure (FCM 201s regardless of token validity).\n void this.persistResubscription(\n data.subscription as PushSubscriptionJSON | undefined\n );\n break;\n }\n }\n\n /**\n * Translate a SW `push.clicked` postMessage into a typed PushTapEvent,\n * dispatch through the lifecycle hook bus, and fall back to a hard\n * navigation if no host handler claimed it.\n *\n * The fallback path matters: if a tenant ships a push template with\n * an unknown `action_type` (no SPA handler registered) OR pushes the\n * SDK before the host wires up its `onPushTap` subscriber, we still\n * want the user to land somewhere — `action_url` is always set by the\n * SW's notificationclick handler (it falls back to '/').\n */\n private dispatchPushTap(data: Record<string, unknown>): void {\n if (typeof window === 'undefined') return;\n const actionUrl = (data.action_url as string) || '/';\n let defaultPrevented = false;\n const evt: PushTapEvent = {\n action_type: data.action_type as string | undefined,\n action_payload: data.action_payload as Record<string, unknown> | undefined,\n action_url: actionUrl,\n action: (data.action as string) || 'default',\n campaign_id: data.campaign_id as string | undefined,\n preventDefault: () => {\n defaultPrevented = true;\n evt.defaultPrevented = true;\n },\n defaultPrevented: false,\n };\n const proceed = this.emit('push-tap', evt);\n if (proceed && !defaultPrevented) {\n // No subscriber claimed this tap (or the subscriber returned\n // void / didn't preventDefault). Default to a hard nav so the\n // user always lands somewhere actionable. SPAs that want\n // in-place handling MUST return false from their handler.\n try {\n window.location.href = actionUrl;\n } catch {\n // navigation blocked (sandboxed iframe, etc.) — host can\n // still handle via the hook\n }\n }\n }\n\n private async persistResubscription(\n subscription: PushSubscriptionJSON | undefined\n ): Promise<void> {\n if (!subscription || !subscription.endpoint || !subscription.keys) return;\n try {\n const fingerprint = await this.getStableFingerprint();\n await this.retryApiCall(() =>\n this.apiClient.post(\n '/v1/web_push/subscriptions',\n {\n contact_id: this.resolveContactId(),\n endpoint: subscription.endpoint!,\n p256dh: subscription.keys!.p256dh!,\n auth: subscription.keys!.auth!,\n device_fingerprint: fingerprint,\n user_agent: navigator.userAgent,\n },\n { 'X-Property-Id': this.config.propertyId }\n )\n );\n this.trackPushEvent('push.resubscribed', {});\n } catch (err) {\n console.warn('[AegisWebPush] persistResubscription failed:', err);\n }\n }\n\n private trackPushEvent(eventName: string, properties: Record<string, unknown>): void {\n this.eventTracker?.track(eventName, {\n ...properties,\n platform: 'web',\n browser: this.getBrowserInfo(),\n });\n }\n\n private getBrowserInfo(): string {\n const ua = navigator.userAgent;\n if (ua.includes('Edg')) return 'edge';\n if (ua.includes('Chrome')) return 'chrome';\n if (ua.includes('Firefox')) return 'firefox';\n if (ua.includes('Safari')) return 'safari';\n return 'unknown';\n }\n\n private urlBase64ToUint8Array(base64String: string): Uint8Array {\n const padding = '='.repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');\n const rawData = atob(base64);\n const arr = new Uint8Array(rawData.length);\n for (let i = 0; i < rawData.length; ++i) {\n arr[i] = rawData.charCodeAt(i);\n }\n return arr;\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@active-reach/web-sdk",
3
- "version": "1.8.0",
3
+ "version": "1.8.5",
4
4
  "description": "Web SDK for Active Reach Intelligence — event tracking, identity resolution, in-app messaging, web push, and placements",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",