@active-reach/web-sdk 1.14.1 → 1.19.0
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/README.md +35 -54
- package/dist/aegis-sw.js +1 -1
- package/dist/aegis.min.js +1 -1
- package/dist/aegis.min.js.map +1 -1
- package/dist/{analytics-CjLItVo2.mjs → analytics-DblhjFhs.mjs} +66 -3
- package/dist/analytics-DblhjFhs.mjs.map +1 -0
- package/dist/cdn.d.ts +5 -2
- package/dist/cdn.d.ts.map +1 -1
- package/dist/chat/AegisChat.d.ts +138 -0
- package/dist/chat/AegisChat.d.ts.map +1 -0
- package/dist/chat/index.d.ts +3 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/core/analytics.d.ts +37 -2
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/ecommerce/index.d.ts +32 -14
- package/dist/ecommerce/index.d.ts.map +1 -1
- package/dist/inapp/AegisInAppManager.d.ts +349 -1
- package/dist/inapp/AegisInAppManager.d.ts.map +1 -1
- package/dist/inapp/devicePairing.d.ts +70 -0
- package/dist/inapp/devicePairing.d.ts.map +1 -0
- package/dist/inapp/index.d.ts +2 -0
- package/dist/inapp/index.d.ts.map +1 -1
- package/dist/inapp/renderPreview.d.ts.map +1 -1
- package/dist/inapp/renderers/active-web-chat.d.ts +38 -0
- package/dist/inapp/renderers/active-web-chat.d.ts.map +1 -0
- package/dist/inapp/renderers/carousel-cards.d.ts.map +1 -1
- package/dist/inapp/renderers/coachmark-tour.d.ts.map +1 -1
- package/dist/inapp/renderers/games.d.ts +27 -0
- package/dist/inapp/renderers/games.d.ts.map +1 -0
- package/dist/inapp/renderers/index.d.ts +8 -3
- package/dist/inapp/renderers/index.d.ts.map +1 -1
- package/dist/inapp/renderers/product-recommendation.d.ts +4 -4
- package/dist/inapp/renderers/product-recommendation.d.ts.map +1 -1
- package/dist/inapp/renderers/progress-bar.d.ts +14 -0
- package/dist/inapp/renderers/progress-bar.d.ts.map +1 -1
- package/dist/inapp/renderers/sticky-bar.d.ts.map +1 -1
- package/dist/inapp/renderers/stories.d.ts +31 -0
- package/dist/inapp/renderers/stories.d.ts.map +1 -0
- package/dist/inapp/renderers/types.d.ts +73 -2
- package/dist/inapp/renderers/types.d.ts.map +1 -1
- package/dist/inapp/renderers/video.d.ts +20 -0
- package/dist/inapp/renderers/video.d.ts.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8696 -3093
- package/dist/index.js.map +1 -1
- package/dist/loyalty/AegisLoyaltyManager.d.ts +120 -0
- package/dist/loyalty/AegisLoyaltyManager.d.ts.map +1 -0
- package/dist/loyalty/index.d.ts +3 -0
- package/dist/loyalty/index.d.ts.map +1 -0
- package/dist/push/AegisWebPush.d.ts.map +1 -1
- package/dist/push/AegisWebPush.js +13 -2
- package/dist/push/AegisWebPush.js.map +1 -1
- package/dist/react.js +1 -1
- package/dist/runtime/AegisMessageRuntime.d.ts +96 -1
- package/dist/runtime/AegisMessageRuntime.d.ts.map +1 -1
- package/dist/triggers/ContactScoresFetcher.d.ts +105 -0
- package/dist/triggers/ContactScoresFetcher.d.ts.map +1 -0
- package/dist/triggers/IntentRuleEvaluator.d.ts +1 -1
- package/dist/triggers/IntentRuleEvaluator.d.ts.map +1 -1
- package/dist/triggers/index.d.ts +2 -0
- package/dist/triggers/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/analytics-CjLItVo2.mjs.map +0 -1
- package/dist/placements/AegisPlacementManager.d.ts +0 -97
- package/dist/placements/AegisPlacementManager.d.ts.map +0 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AegisLoyaltyManager — public SDK surface for the closed-loop store
|
|
3
|
+
* credit + gift cards feature.
|
|
4
|
+
*
|
|
5
|
+
* Phase 7 of the Active Loyalty — Store Credit + Gift Cards rollout.
|
|
6
|
+
* Tracker: docs/architecture/ACTIVE_LOYALTY_STORE_CREDIT_GIFT_CARDS_TRACKER.md
|
|
7
|
+
*
|
|
8
|
+
* Public API (call via `runtime.loyalty.storeCredit.*` / `runtime.loyalty.giftCards.*`):
|
|
9
|
+
*
|
|
10
|
+
* await runtime.loyalty.storeCredit.listActive(phone)
|
|
11
|
+
* await runtime.loyalty.storeCredit.startReload({ phone, workspaceId, amountPaise })
|
|
12
|
+
* await runtime.loyalty.giftCards.startPurchase({ ... })
|
|
13
|
+
* await runtime.loyalty.giftCards.claim({ giftCardId, phone })
|
|
14
|
+
* await runtime.loyalty.giftCards.listReceived(phone)
|
|
15
|
+
*
|
|
16
|
+
* Closed-loop constraints surfaced through the API:
|
|
17
|
+
* - workspaceId is required on every reload / gift card purchase
|
|
18
|
+
* (C1 — single-workspace redemption).
|
|
19
|
+
* - listActive returns ONE row per workspace; clients render one
|
|
20
|
+
* card per workspace, never pooled.
|
|
21
|
+
* - claim() requires the recipient phone to authenticate as the
|
|
22
|
+
* directed recipient (C3 — no bearer codes).
|
|
23
|
+
*
|
|
24
|
+
* Network shape — all calls hit the storefront-facing commerce router
|
|
25
|
+
* via the configured apiHost / same-origin.
|
|
26
|
+
*/
|
|
27
|
+
export interface StoreCreditBalance {
|
|
28
|
+
workspaceId: string;
|
|
29
|
+
balancePaise: number;
|
|
30
|
+
expiresAt: string | null;
|
|
31
|
+
lastActivityAt: string | null;
|
|
32
|
+
}
|
|
33
|
+
export interface ReloadCheckoutResult {
|
|
34
|
+
contactId: string;
|
|
35
|
+
checkoutUrl: string;
|
|
36
|
+
provider: string;
|
|
37
|
+
paymentLinkId: string;
|
|
38
|
+
amountPaise: number;
|
|
39
|
+
}
|
|
40
|
+
export interface GiftCardPurchaseResult {
|
|
41
|
+
senderContactId: string;
|
|
42
|
+
giftCardId: string;
|
|
43
|
+
checkoutUrl: string;
|
|
44
|
+
provider: string;
|
|
45
|
+
paymentLinkId: string;
|
|
46
|
+
amountPaise: number;
|
|
47
|
+
}
|
|
48
|
+
export interface GiftCard {
|
|
49
|
+
id: string;
|
|
50
|
+
workspaceId: string;
|
|
51
|
+
senderContactId: string;
|
|
52
|
+
amountPaise: number;
|
|
53
|
+
status: string;
|
|
54
|
+
personalMessage: string | null;
|
|
55
|
+
deliveredAt: string | null;
|
|
56
|
+
claimedAt: string | null;
|
|
57
|
+
expiresAt: string | null;
|
|
58
|
+
createdAt: string | null;
|
|
59
|
+
}
|
|
60
|
+
export interface GiftCardClaimResult {
|
|
61
|
+
recipientContactId: string;
|
|
62
|
+
giftCardId: string;
|
|
63
|
+
amountCreditedPaise: number;
|
|
64
|
+
newBalancePaise: number;
|
|
65
|
+
}
|
|
66
|
+
export interface AegisLoyaltyManagerConfig {
|
|
67
|
+
/** Storefront slug — `{slug}.actii.me/...` */
|
|
68
|
+
slug: string;
|
|
69
|
+
/** Optional explicit API host. When null, uses same-origin. */
|
|
70
|
+
apiHost?: string;
|
|
71
|
+
/** When true, attaches credentials so cookies flow (anon-id, csrf). */
|
|
72
|
+
credentials?: 'include' | 'same-origin' | 'omit';
|
|
73
|
+
/** Optional debug logger. */
|
|
74
|
+
debugMode?: boolean;
|
|
75
|
+
}
|
|
76
|
+
declare class HttpClient {
|
|
77
|
+
private config;
|
|
78
|
+
constructor(config: AegisLoyaltyManagerConfig);
|
|
79
|
+
private url;
|
|
80
|
+
get<T>(path: string): Promise<T>;
|
|
81
|
+
post<T>(path: string, body: Record<string, unknown>): Promise<T>;
|
|
82
|
+
}
|
|
83
|
+
declare class StoreCreditClient {
|
|
84
|
+
private http;
|
|
85
|
+
constructor(http: HttpClient);
|
|
86
|
+
listActive(phone: string): Promise<StoreCreditBalance[]>;
|
|
87
|
+
startReload(opts: {
|
|
88
|
+
phone: string;
|
|
89
|
+
workspaceId: string;
|
|
90
|
+
amountPaise: number;
|
|
91
|
+
firstName?: string;
|
|
92
|
+
email?: string;
|
|
93
|
+
callbackUrl?: string;
|
|
94
|
+
}): Promise<ReloadCheckoutResult>;
|
|
95
|
+
}
|
|
96
|
+
declare class GiftCardsClient {
|
|
97
|
+
private http;
|
|
98
|
+
constructor(http: HttpClient);
|
|
99
|
+
startPurchase(opts: {
|
|
100
|
+
senderPhone: string;
|
|
101
|
+
workspaceId: string;
|
|
102
|
+
recipientPhone: string;
|
|
103
|
+
recipientName?: string;
|
|
104
|
+
amountPaise: number;
|
|
105
|
+
personalMessage?: string;
|
|
106
|
+
callbackUrl?: string;
|
|
107
|
+
}): Promise<GiftCardPurchaseResult>;
|
|
108
|
+
claim(opts: {
|
|
109
|
+
giftCardId: string;
|
|
110
|
+
phone: string;
|
|
111
|
+
}): Promise<GiftCardClaimResult>;
|
|
112
|
+
listReceived(phone: string): Promise<GiftCard[]>;
|
|
113
|
+
}
|
|
114
|
+
export declare class AegisLoyaltyManager {
|
|
115
|
+
readonly storeCredit: StoreCreditClient;
|
|
116
|
+
readonly giftCards: GiftCardsClient;
|
|
117
|
+
constructor(config: AegisLoyaltyManagerConfig);
|
|
118
|
+
}
|
|
119
|
+
export {};
|
|
120
|
+
//# sourceMappingURL=AegisLoyaltyManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AegisLoyaltyManager.d.ts","sourceRoot":"","sources":["../../src/loyalty/AegisLoyaltyManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/loyalty/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EACV,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,QAAQ,EACR,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AegisWebPush.d.ts","sourceRoot":"","sources":["../../src/push/AegisWebPush.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"AegisWebPush.d.ts","sourceRoot":"","sources":["../../src/push/AegisWebPush.ts"],"names":[],"mappings":"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"}
|
|
@@ -319,6 +319,11 @@ class AegisWebPush {
|
|
|
319
319
|
case "push.clicked": {
|
|
320
320
|
this.trackPushEvent("push.clicked", {
|
|
321
321
|
campaign_id: data.campaign_id,
|
|
322
|
+
// Attribution echoed by the SW → forwarded to the engagement
|
|
323
|
+
// event so it rolls up per-journey / per-template / per-outlet.
|
|
324
|
+
journey_execution_id: data.journey_execution_id,
|
|
325
|
+
lifecycle_fork_id: data.lifecycle_fork_id,
|
|
326
|
+
location_id: data.location_id,
|
|
322
327
|
action_url: data.action_url,
|
|
323
328
|
action_type: data.action_type
|
|
324
329
|
});
|
|
@@ -327,7 +332,10 @@ class AegisWebPush {
|
|
|
327
332
|
}
|
|
328
333
|
case "push.dismissed":
|
|
329
334
|
this.trackPushEvent("push.dismissed", {
|
|
330
|
-
campaign_id: data.campaign_id
|
|
335
|
+
campaign_id: data.campaign_id,
|
|
336
|
+
journey_execution_id: data.journey_execution_id,
|
|
337
|
+
lifecycle_fork_id: data.lifecycle_fork_id,
|
|
338
|
+
location_id: data.location_id
|
|
331
339
|
});
|
|
332
340
|
this.emit("push-dismissed", {
|
|
333
341
|
campaign_id: data.campaign_id
|
|
@@ -335,7 +343,10 @@ class AegisWebPush {
|
|
|
335
343
|
break;
|
|
336
344
|
case "push.delivered":
|
|
337
345
|
this.trackPushEvent("push.delivered", {
|
|
338
|
-
campaign_id: data.campaign_id
|
|
346
|
+
campaign_id: data.campaign_id,
|
|
347
|
+
journey_execution_id: data.journey_execution_id,
|
|
348
|
+
lifecycle_fork_id: data.lifecycle_fork_id,
|
|
349
|
+
location_id: data.location_id
|
|
339
350
|
});
|
|
340
351
|
this.emit("push-shown", {
|
|
341
352
|
campaign_id: data.campaign_id,
|
|
@@ -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 // Engagement-event reporting credentials. When BOTH `writeKey` and\n // `organizationId` are provided AND the caller does NOT pass an\n // explicit `eventTracker` to the constructor, AegisWebPush auto-wires\n // a built-in tracker that POSTs push lifecycle events\n // (push.delivered / push.dismissed / push.clicked) to\n // `/v1/push/engagement` — closing the long-standing gap where shown\n // + dismissed signals (only the device-side SDK can report them)\n // never reached `aegis.delivery_events`. See backend dual-route at\n // `apps/event-ingress/src/services/event-switchboard.ts`\n // (SDK_ENGAGEMENT_EVENT_TO_DELIVERY_MAPPING).\n //\n // Callers using `AegisMessageRuntime` should keep passing the\n // runtime's analytics adapter as `eventTracker` — these fields are\n // only the standalone-init fallback.\n writeKey?: string;\n organizationId?: 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 // Explicit eventTracker wins (typical AegisMessageRuntime flow).\n // Otherwise, if both `writeKey` and `organizationId` are configured,\n // wire the built-in tracker that POSTs to `/v1/push/engagement` so\n // standalone push installs don't silently drop lifecycle events.\n if (eventTracker) {\n this.eventTracker = eventTracker;\n } else if (this.config.writeKey && this.config.organizationId) {\n this.eventTracker = this.createBuiltinEventTracker(\n this.config.writeKey,\n this.config.organizationId,\n );\n } else {\n this.eventTracker = null;\n }\n }\n\n /** Built-in fallback that POSTs push lifecycle events to\n * `/v1/push/engagement`. The gateway forwards to event-ingress with\n * the canonical `push.<event>` event_type, which the\n * event-switchboard's SDK_ENGAGEMENT_EVENT_TO_DELIVERY_MAPPING\n * dual-routes onto `aegis.delivery_events` as\n * `channel='push', status='shown'|'dismissed'|...`. Best-effort —\n * failures are warned but never thrown (push SW callbacks shouldn't\n * reject because the analytics POST failed). */\n private createBuiltinEventTracker(\n writeKey: string,\n organizationId: string,\n ): PushEventTracker {\n const apiClient = this.apiClient;\n const propertyId = this.config.propertyId;\n const resolveContactId = () => this.resolveContactId();\n return {\n track(eventName: string, properties: Record<string, unknown>): void {\n const contactId =\n (properties.contact_id as string | undefined) ?? resolveContactId();\n const body = {\n campaign_id: (properties.campaign_id as string | undefined) || '',\n message_id: (properties.message_id as string | undefined) || '',\n event_type: eventName, // e.g. 'push.delivered' — gateway tolerates either form\n user_id: (properties.user_id as string | undefined) || contactId,\n contact_id: contactId,\n anonymous_id: (properties.anonymous_id as string | undefined) || '',\n platform: (properties.platform as string | undefined) || 'web',\n property_id: propertyId,\n metadata: (properties.metadata as Record<string, unknown> | undefined) ?? {},\n };\n const headers = {\n 'X-Aegis-Write-Key': writeKey,\n 'X-Organization-ID': organizationId,\n 'X-Property-Id': propertyId,\n };\n void Promise.resolve(\n apiClient.post('/v1/push/engagement', body, headers),\n ).catch((err) => {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn('[Active Reach WebPush] engagement report failed:', err);\n }\n });\n },\n };\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 '[Active Reach WebPush] 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('[Active Reach WebPush] 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('[Active Reach WebPush] 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('[Active Reach WebPush] 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('[Active Reach WebPush] 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('[Active Reach WebPush] 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 // Engagement-event reporting credentials. When BOTH `writeKey` and\n // `organizationId` are provided AND the caller does NOT pass an\n // explicit `eventTracker` to the constructor, AegisWebPush auto-wires\n // a built-in tracker that POSTs push lifecycle events\n // (push.delivered / push.dismissed / push.clicked) to\n // `/v1/push/engagement` — closing the long-standing gap where shown\n // + dismissed signals (only the device-side SDK can report them)\n // never reached `aegis.delivery_events`. See backend dual-route at\n // `apps/event-ingress/src/services/event-switchboard.ts`\n // (SDK_ENGAGEMENT_EVENT_TO_DELIVERY_MAPPING).\n //\n // Callers using `AegisMessageRuntime` should keep passing the\n // runtime's analytics adapter as `eventTracker` — these fields are\n // only the standalone-init fallback.\n writeKey?: string;\n organizationId?: 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 // Explicit eventTracker wins (typical AegisMessageRuntime flow).\n // Otherwise, if both `writeKey` and `organizationId` are configured,\n // wire the built-in tracker that POSTs to `/v1/push/engagement` so\n // standalone push installs don't silently drop lifecycle events.\n if (eventTracker) {\n this.eventTracker = eventTracker;\n } else if (this.config.writeKey && this.config.organizationId) {\n this.eventTracker = this.createBuiltinEventTracker(\n this.config.writeKey,\n this.config.organizationId,\n );\n } else {\n this.eventTracker = null;\n }\n }\n\n /** Built-in fallback that POSTs push lifecycle events to\n * `/v1/push/engagement`. The gateway forwards to event-ingress with\n * the canonical `push.<event>` event_type, which the\n * event-switchboard's SDK_ENGAGEMENT_EVENT_TO_DELIVERY_MAPPING\n * dual-routes onto `aegis.delivery_events` as\n * `channel='push', status='shown'|'dismissed'|...`. Best-effort —\n * failures are warned but never thrown (push SW callbacks shouldn't\n * reject because the analytics POST failed). */\n private createBuiltinEventTracker(\n writeKey: string,\n organizationId: string,\n ): PushEventTracker {\n const apiClient = this.apiClient;\n const propertyId = this.config.propertyId;\n const resolveContactId = () => this.resolveContactId();\n return {\n track(eventName: string, properties: Record<string, unknown>): void {\n const contactId =\n (properties.contact_id as string | undefined) ?? resolveContactId();\n const body = {\n campaign_id: (properties.campaign_id as string | undefined) || '',\n message_id: (properties.message_id as string | undefined) || '',\n event_type: eventName, // e.g. 'push.delivered' — gateway tolerates either form\n user_id: (properties.user_id as string | undefined) || contactId,\n contact_id: contactId,\n anonymous_id: (properties.anonymous_id as string | undefined) || '',\n platform: (properties.platform as string | undefined) || 'web',\n property_id: propertyId,\n metadata: (properties.metadata as Record<string, unknown> | undefined) ?? {},\n };\n const headers = {\n 'X-Aegis-Write-Key': writeKey,\n 'X-Organization-ID': organizationId,\n 'X-Property-Id': propertyId,\n };\n void Promise.resolve(\n apiClient.post('/v1/push/engagement', body, headers),\n ).catch((err) => {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn('[Active Reach WebPush] engagement report failed:', err);\n }\n });\n },\n };\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 '[Active Reach WebPush] 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('[Active Reach WebPush] 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('[Active Reach WebPush] 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('[Active Reach WebPush] 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('[Active Reach WebPush] 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 // Attribution echoed by the SW → forwarded to the engagement\n // event so it rolls up per-journey / per-template / per-outlet.\n journey_execution_id: data.journey_execution_id,\n lifecycle_fork_id: data.lifecycle_fork_id,\n location_id: data.location_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 journey_execution_id: data.journey_execution_id,\n lifecycle_fork_id: data.lifecycle_fork_id,\n location_id: data.location_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 journey_execution_id: data.journey_execution_id,\n lifecycle_fork_id: data.lifecycle_fork_id,\n location_id: data.location_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('[Active Reach WebPush] 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/dist/react.js
CHANGED
|
@@ -22,8 +22,12 @@
|
|
|
22
22
|
* `widgets.renderInteractiveCampaign(campaign)` — no CustomEvent bus,
|
|
23
23
|
* no listeners, no registration-order bugs.
|
|
24
24
|
*/
|
|
25
|
-
import { AegisInAppManager, type AegisInAppConfig, type InAppCampaign } from '../inapp';
|
|
25
|
+
import { AegisInAppManager, type AegisInAppConfig, type InAppCampaign, type CartState } from '../inapp';
|
|
26
26
|
import { AegisWidgetManager, type AegisWidgetConfig } from '../widgets';
|
|
27
|
+
import { IntentRuleEvaluator } from '../triggers/IntentRuleEvaluator';
|
|
28
|
+
import { ContactScoresFetcher } from '../triggers/ContactScoresFetcher';
|
|
29
|
+
import { AegisLoyaltyManager } from '../loyalty';
|
|
30
|
+
import { AegisChat, type AegisChatConfig } from '../chat';
|
|
27
31
|
export interface AegisMessageRuntimeConfig extends AegisInAppConfig {
|
|
28
32
|
/** If set, a `TriggerEngine` from the SDK is wired to the WidgetManager
|
|
29
33
|
* for its exit-intent / scroll-velocity / inactivity handlers. Leave
|
|
@@ -36,6 +40,27 @@ export interface AegisMessageRuntimeConfig extends AegisInAppConfig {
|
|
|
36
40
|
/** Source platform for gamification attribution (shopify / woocommerce
|
|
37
41
|
* / magento / mobile_sdk / web). Passed through to WidgetManager. */
|
|
38
42
|
sourcePlatform?: AegisWidgetConfig['sourcePlatform'];
|
|
43
|
+
/** Storefront slug for the loyalty subsystem — needed when the host
|
|
44
|
+
* app calls `runtime.loyalty.storeCredit.*` / `runtime.loyalty.giftCards.*`.
|
|
45
|
+
* When omitted, the loyalty manager is constructed but every call
|
|
46
|
+
* will throw until configure() is invoked. */
|
|
47
|
+
storefrontSlug?: string;
|
|
48
|
+
/** Mount the customer chat launcher ("Shopping Assistant") — a floating
|
|
49
|
+
* bubble that talks to the channel-neutral chat agent (Track A). Off by
|
|
50
|
+
* default; opt in per host page. */
|
|
51
|
+
enableChat?: boolean;
|
|
52
|
+
/** Presentation overrides for the chat launcher (title / greeting / accent),
|
|
53
|
+
* plus proactive auto-open. writeKey / apiHost / org / contact are inherited
|
|
54
|
+
* from the runtime. */
|
|
55
|
+
chat?: Pick<AegisChatConfig, 'title' | 'greeting' | 'accentColor' | 'channel' | 'icon' | 'logoUrl' | 'position'> & {
|
|
56
|
+
/** Client-side TriggerEngine events that auto-open the launcher (e.g.
|
|
57
|
+
* ['exit_intent', 'inactivity_90']). The trigger ONLY opens the panel —
|
|
58
|
+
* it never creates an offer. The proactive *opener* is owned server-side
|
|
59
|
+
* by the journey→warm-offer-queue bridge (Track B B4); the bot surfaces it
|
|
60
|
+
* on the customer's first turn. This keeps a single offer source — no
|
|
61
|
+
* parallel client-side offer infra. */
|
|
62
|
+
proactiveTriggers?: string[];
|
|
63
|
+
};
|
|
39
64
|
}
|
|
40
65
|
/**
|
|
41
66
|
* One runtime, all in-app types. Consumers should prefer this over
|
|
@@ -45,7 +70,34 @@ export interface AegisMessageRuntimeConfig extends AegisInAppConfig {
|
|
|
45
70
|
export declare class AegisMessageRuntime {
|
|
46
71
|
readonly inApp: AegisInAppManager;
|
|
47
72
|
readonly widgets: AegisWidgetManager;
|
|
73
|
+
/** Shared IntentRuleEvaluator for identified-contact micro-intent
|
|
74
|
+
* patterns. The ContactScoresFetcher writes Layer-1 signals into its
|
|
75
|
+
* snapshot; IntentSnapshotCollector (if wired by the consumer) writes
|
|
76
|
+
* Layer-2 signals. Exposed so apps that pre-existed P4a can swap in
|
|
77
|
+
* their own evaluator if they need to. */
|
|
78
|
+
readonly intentRuleEvaluator: IntentRuleEvaluator;
|
|
79
|
+
/** Bridges GET /v1/sdk/contact-scores into the evaluator snapshot.
|
|
80
|
+
* Triggered automatically on initialize() and updateContactId(). */
|
|
81
|
+
readonly contactScores: ContactScoresFetcher;
|
|
82
|
+
/** Closed-loop store credit + directed gift cards (Phase 7).
|
|
83
|
+
* Usage:
|
|
84
|
+
* await runtime.loyalty.storeCredit.listActive(phone)
|
|
85
|
+
* await runtime.loyalty.storeCredit.startReload({ phone, workspaceId, amountPaise })
|
|
86
|
+
* await runtime.loyalty.giftCards.startPurchase({ ... })
|
|
87
|
+
* await runtime.loyalty.giftCards.claim({ giftCardId, phone })
|
|
88
|
+
* Tracker: docs/architecture/ACTIVE_LOYALTY_STORE_CREDIT_GIFT_CARDS_TRACKER.md */
|
|
89
|
+
readonly loyalty: AegisLoyaltyManager;
|
|
90
|
+
/** Customer chat launcher ("Shopping Assistant"). Constructed only when
|
|
91
|
+
* `enableChat` is set; undefined otherwise. (Track A) */
|
|
92
|
+
readonly chat?: AegisChat;
|
|
93
|
+
private contactId?;
|
|
48
94
|
private initialized;
|
|
95
|
+
/** apiHost captured for the device-pairing entry (`?aegis_pair=CODE`). */
|
|
96
|
+
private readonly pairingApiHost;
|
|
97
|
+
/** Live cart snapshot, set imperatively via setCartState(). Held in
|
|
98
|
+
* private state (NOT a window global) and surfaced to the InAppManager's
|
|
99
|
+
* renderers via the getCartState reader passed into its constructor. */
|
|
100
|
+
private cartState?;
|
|
49
101
|
constructor(config: AegisMessageRuntimeConfig);
|
|
50
102
|
/**
|
|
51
103
|
* Boots both managers in parallel. Safe to call multiple times — the
|
|
@@ -69,6 +121,49 @@ export declare class AegisMessageRuntime {
|
|
|
69
121
|
* channel already.
|
|
70
122
|
*/
|
|
71
123
|
onClientEvent(eventName: string, eventData?: Record<string, unknown>): void;
|
|
124
|
+
/**
|
|
125
|
+
* Feed live cart state into the runtime — the imperative, encapsulated
|
|
126
|
+
* equivalent of the `window.aegis_cart` global the SDK reads for
|
|
127
|
+
* third-party Shopify/Woo/Magento embeds. Headless storefronts (e.g. the
|
|
128
|
+
* Active Commerce Store) call this on every cart mutation.
|
|
129
|
+
*
|
|
130
|
+
* The state is held in PRIVATE runtime state (no window pollution, not
|
|
131
|
+
* readable/writable by third-party scripts) and:
|
|
132
|
+
* 1. surfaces to renderers (progress-bar free-delivery / cart-total goals)
|
|
133
|
+
* via the getCartState reader passed into the InAppManager;
|
|
134
|
+
* 2. feeds the `cart_value` micro-intent signal so `CART_VALUE`-gated
|
|
135
|
+
* campaigns (e.g. a cart-idle nudge) can evaluate — this signal was
|
|
136
|
+
* previously never populated client-side;
|
|
137
|
+
* 3. taps a debounced in-app refresh so a newly eligible cart-gated
|
|
138
|
+
* campaign arms without waiting for the next identity flip.
|
|
139
|
+
*
|
|
140
|
+
* This is a visual/targeting hint only. Reward GATING that must agree with
|
|
141
|
+
* the server-side checkout guard should use the trusted SSE progress source.
|
|
142
|
+
*/
|
|
143
|
+
setCartState(state: CartState): void;
|
|
144
|
+
/**
|
|
145
|
+
* Notify the in-app subsystem that a tracked event occurred. Schedules
|
|
146
|
+
* a debounced refresh of armed campaigns so newly eligible in-app
|
|
147
|
+
* messages (e.g., trigger-gated on `cart_item_added`) become visible
|
|
148
|
+
* within ~300ms — bursts coalesce. See
|
|
149
|
+
* `AegisInAppManager.refreshOnEvent` for debounce semantics and why
|
|
150
|
+
* this matters under storefront polling.
|
|
151
|
+
*/
|
|
152
|
+
track(eventName: string): void;
|
|
153
|
+
/**
|
|
154
|
+
* Notify the in-app subsystem that a URL-bound page view occurred.
|
|
155
|
+
* Mirrors `aegis.page()` semantics — fires `'page_view'` as the
|
|
156
|
+
* qualifying event so the trailing debounce covers SPA route bursts
|
|
157
|
+
* (history.pushState rapid-fire) without spamming the endpoint.
|
|
158
|
+
*/
|
|
159
|
+
page(name?: string): void;
|
|
160
|
+
/**
|
|
161
|
+
* Notify the in-app subsystem that a SPA-logical screen view occurred
|
|
162
|
+
* (cart drawer open, checkout step transition — anywhere `aegis.screen()`
|
|
163
|
+
* is called). Refresh-on-screen lets per-screen-targeted campaigns
|
|
164
|
+
* arm without a URL change.
|
|
165
|
+
*/
|
|
166
|
+
screen(name: string): void;
|
|
72
167
|
/**
|
|
73
168
|
* Conversion-aware suppression — call when the host app observes a
|
|
74
169
|
* goal event (purchase / order_completed / checkout_completed / custom).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AegisMessageRuntime.d.ts","sourceRoot":"","sources":["../../src/runtime/AegisMessageRuntime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"AegisMessageRuntime.d.ts","sourceRoot":"","sources":["../../src/runtime/AegisMessageRuntime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,iBAAiB,EAAqB,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAC3H,OAAO,EAAE,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAExE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1D,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE;;kEAE8D;IAC9D,aAAa,CAAC,EAAE,iBAAiB,CAAC,eAAe,CAAC,CAAC;IACnD;;sDAEkD;IAClD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;0EACsE;IACtE,cAAc,CAAC,EAAE,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;IACrD;;;mDAG+C;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;yCAEqC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;4BAEwB;IACxB,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC,GAAG;QACjH;;;;;gDAKwC;QACxC,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC9B,CAAC;CACH;AAED;;;;GAIG;AACH,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC;;;;+CAI2C;IAC3C,QAAQ,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;IAClD;yEACqE;IACrE,QAAQ,CAAC,aAAa,EAAE,oBAAoB,CAAC;IAC7C;;;;;;uFAMmF;IACnF,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC;8DAC0D;IAC1D,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC;IAC1B,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAS;IAC5B,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC;;6EAEyE;IACzE,OAAO,CAAC,SAAS,CAAC,CAAY;gBAElB,MAAM,EAAE,yBAAyB;IAwF7C;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBjC;;;;;;;OAOG;IACG,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD;;;;;;OAMG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI;IAI/E;;;;;;;;;;;;;;;;;;OAkBG;IACH,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IAwBpC;;;;;;;OAOG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAK9B;;;;;OAKG;IACH,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAOzB;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAO1B;;;;;;;;;;;;;;;;OAgBG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIxC;;;;;OAKG;IACH,OAAO,IAAI,IAAI;IAOf;;;;;OAKG;IACH,YAAY,IAAI,aAAa,EAAE;CAMhC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Micro-Intent Engine — contact-scores fetcher (P4a bridge).
|
|
3
|
+
*
|
|
4
|
+
* Bridges `GET /v1/sdk/contact-scores` into the SDK's
|
|
5
|
+
* `IntentRuleEvaluator` snapshot. Without this, identified-contact
|
|
6
|
+
* micro-intent patterns (VIP Whisper, Win-back At-Risk, Price-Sensitive
|
|
7
|
+
* Nudge, etc.) cannot fire because their AND-compound rules evaluate
|
|
8
|
+
* `rfm_segment` / `lifecycle_stage` / `churn_risk_tier` /
|
|
9
|
+
* `predicted_ltv_tier` / `price_sensitivity_score` against an empty
|
|
10
|
+
* snapshot and always return false.
|
|
11
|
+
*
|
|
12
|
+
* Lifecycle
|
|
13
|
+
* ---------
|
|
14
|
+
* AegisMessageRuntime.initialize() → fetch if contactId known
|
|
15
|
+
* AegisMessageRuntime.updateContactId → fetch for the new contact
|
|
16
|
+
*
|
|
17
|
+
* The fetcher is session-deduped: a second `fetchForContact(sameId)`
|
|
18
|
+
* call within `dedupWindowMs` is a no-op. This stops a hot navigation
|
|
19
|
+
* loop from hammering the endpoint when SPA route changes re-call
|
|
20
|
+
* identify() with the same id.
|
|
21
|
+
*
|
|
22
|
+
* Empty state
|
|
23
|
+
* -----------
|
|
24
|
+
* The endpoint returns 204 for unknown contacts AND for contacts whose
|
|
25
|
+
* `scoring_tier='none'`. Either way the fetcher leaves the snapshot
|
|
26
|
+
* unchanged — the evaluator's leaf eval treats absent Layer-1 keys as
|
|
27
|
+
* null, which fail-closes any AND clause involving them. Anonymous
|
|
28
|
+
* patterns continue to work normally.
|
|
29
|
+
*
|
|
30
|
+
* Privacy posture
|
|
31
|
+
* ---------------
|
|
32
|
+
* The endpoint's default response is the tiered form (`predicted_ltv_tier`).
|
|
33
|
+
* Tenants who opt into raw `predicted_ltv` via
|
|
34
|
+
* `organizations.settings.score_serving_raw_ltv = true` will see the
|
|
35
|
+
* float on the snapshot too. Both are valid IntentSignals; rule
|
|
36
|
+
* authors pick whichever matches their tenant config.
|
|
37
|
+
*
|
|
38
|
+
* Plan reference: docs/architecture/MICRO_INTENT_ENGINE.md §P4a.
|
|
39
|
+
*/
|
|
40
|
+
import type { IntentRuleEvaluator, IntentSnapshot } from './IntentRuleEvaluator';
|
|
41
|
+
/** Shape of the Layer-1 score response from /v1/sdk/contact-scores. */
|
|
42
|
+
export interface ContactScoresResponse {
|
|
43
|
+
rfm_segment?: string;
|
|
44
|
+
lifecycle_stage?: string;
|
|
45
|
+
churn_risk_tier?: 'low' | 'medium' | 'high';
|
|
46
|
+
predicted_ltv_tier?: 'low' | 'medium' | 'high';
|
|
47
|
+
predicted_ltv?: number;
|
|
48
|
+
price_sensitivity_score?: number;
|
|
49
|
+
scoring_tier?: string;
|
|
50
|
+
scores_updated_at?: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface ContactScoresFetcherConfig {
|
|
53
|
+
apiHost: string;
|
|
54
|
+
writeKey: string;
|
|
55
|
+
organizationId?: string;
|
|
56
|
+
/** Optional. When provided, the fetcher pushes the response into the
|
|
57
|
+
* evaluator's snapshot automatically on every successful fetch. */
|
|
58
|
+
evaluator?: IntentRuleEvaluator;
|
|
59
|
+
/** Minimum time between repeat fetches for the same contactId.
|
|
60
|
+
* Default 60 seconds. */
|
|
61
|
+
dedupWindowMs?: number;
|
|
62
|
+
/** Override for tests / non-browser environments. */
|
|
63
|
+
fetchImpl?: typeof fetch;
|
|
64
|
+
/** Console.warn channel. */
|
|
65
|
+
onError?: (err: unknown, contactId: string) => void;
|
|
66
|
+
}
|
|
67
|
+
export declare class ContactScoresFetcher {
|
|
68
|
+
private readonly apiHost;
|
|
69
|
+
private readonly writeKey;
|
|
70
|
+
private organizationId?;
|
|
71
|
+
private readonly evaluator?;
|
|
72
|
+
private readonly dedupWindowMs;
|
|
73
|
+
private readonly fetchImpl;
|
|
74
|
+
private readonly onError?;
|
|
75
|
+
private lastFetchedContactId?;
|
|
76
|
+
private lastFetchedAt;
|
|
77
|
+
private latestSnapshot;
|
|
78
|
+
constructor(cfg: ContactScoresFetcherConfig);
|
|
79
|
+
/** Update the org so subsequent fetches scope correctly (e.g. when
|
|
80
|
+
* the SDK resolves an org id after init). */
|
|
81
|
+
updateOrganizationId(organizationId: string | undefined): void;
|
|
82
|
+
/** Most recent successful response. `null` when no fetch has succeeded
|
|
83
|
+
* yet, OR when the endpoint returned 204 (unscored). Consumers can
|
|
84
|
+
* poll this for diagnostic UIs; campaign evaluation does not need to
|
|
85
|
+
* read it directly because the evaluator (if configured) already has
|
|
86
|
+
* the snapshot applied. */
|
|
87
|
+
getSnapshot(): Readonly<ContactScoresResponse> | null;
|
|
88
|
+
/** Fetch scores for `contactId` and apply to the configured evaluator
|
|
89
|
+
* (if any). Returns the response on success, null on 204 / error.
|
|
90
|
+
*
|
|
91
|
+
* Session-dedup: repeat calls for the same contactId within
|
|
92
|
+
* `dedupWindowMs` are no-ops. Pass `force: true` to override. */
|
|
93
|
+
fetchForContact(contactId: string, opts?: {
|
|
94
|
+
force?: boolean;
|
|
95
|
+
}): Promise<ContactScoresResponse | null>;
|
|
96
|
+
}
|
|
97
|
+
/** Project the wire response onto the evaluator's IntentSnapshot shape.
|
|
98
|
+
* Strings (rfm_segment / lifecycle_stage / *_tier) map straight across;
|
|
99
|
+
* numerics (price_sensitivity_score / predicted_ltv) are passed through
|
|
100
|
+
* as numbers. Missing keys are omitted so the evaluator's leaf eval
|
|
101
|
+
* treats them as null (per IntentRuleEvaluator semantics).
|
|
102
|
+
*
|
|
103
|
+
* Exported for the smoke test — drift guard pins the mapping. */
|
|
104
|
+
export declare function toIntentSnapshot(r: ContactScoresResponse): IntentSnapshot;
|
|
105
|
+
//# sourceMappingURL=ContactScoresFetcher.d.ts.map
|