@amboras-dev/snapchat-ads 2.0.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/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ deriveMinuteBucket: () => deriveMinuteBucket,
24
+ getSnapchatAdsConfig: () => getSnapchatAdsConfig,
25
+ makeEventId: () => makeEventId,
26
+ minuteBucketNow: () => minuteBucketNow,
27
+ sendSnapchatCapiEvent: () => sendSnapchatCapiEvent,
28
+ trackAddCart: () => trackAddCart,
29
+ trackPageView: () => trackPageView,
30
+ trackPurchase: () => trackPurchase,
31
+ trackStartCheckout: () => trackStartCheckout,
32
+ trackViewContent: () => trackViewContent
33
+ });
34
+ module.exports = __toCommonJS(src_exports);
35
+
36
+ // src/lib/snap-pixel-loader.ts
37
+ function safeSnaptr(...args) {
38
+ if (typeof window === "undefined") return false;
39
+ const fn = window.snaptr;
40
+ if (typeof fn !== "function") return false;
41
+ try {
42
+ fn(...args);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ // src/lib/dedup.ts
50
+ var MS_PER_MINUTE = 60 * 1e3;
51
+ function deriveMinuteBucket(nowMs) {
52
+ if (!Number.isFinite(nowMs)) return 0;
53
+ return Math.floor(nowMs / MS_PER_MINUTE);
54
+ }
55
+ function minuteBucketNow() {
56
+ return deriveMinuteBucket(Date.now());
57
+ }
58
+ function safeKey(primaryKey) {
59
+ if (!primaryKey) return "unknown";
60
+ return primaryKey.replace(/[^a-zA-Z0-9_-]/g, "_");
61
+ }
62
+ function makeEventId(eventName, primaryKey, minuteBucket = minuteBucketNow()) {
63
+ return `${eventName}_${safeKey(primaryKey)}_${minuteBucket}`;
64
+ }
65
+ function deriveOrderIdForPurchase(eventName, orderId) {
66
+ if (eventName !== "PURCHASE") return void 0;
67
+ if (typeof orderId !== "string") return void 0;
68
+ const trimmed = orderId.trim();
69
+ return trimmed ? trimmed : void 0;
70
+ }
71
+
72
+ // src/hooks/useSnapchatEvents.ts
73
+ var CAPI_PROXY_ENDPOINT = "/api/snapchat-ads/events";
74
+ function currentUrl() {
75
+ return typeof window !== "undefined" ? window.location.href : "";
76
+ }
77
+ async function sendSnapchatCapiEvent(payload) {
78
+ try {
79
+ await fetch(CAPI_PROXY_ENDPOINT, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify(payload)
83
+ });
84
+ } catch {
85
+ }
86
+ }
87
+ function trackPageView(primaryKey, userData = {}, eventId) {
88
+ const eid = eventId ?? makeEventId("PAGE_VIEW", primaryKey, minuteBucketNow());
89
+ safeSnaptr("track", "PAGE_VIEW", { client_dedup_id: eid });
90
+ void sendSnapchatCapiEvent({
91
+ eventName: "PAGE_VIEW",
92
+ eventId: eid,
93
+ eventSourceUrl: currentUrl(),
94
+ userData
95
+ });
96
+ }
97
+ function trackViewContent(productId, options = {}) {
98
+ const eid = options.eventId ?? makeEventId("VIEW_CONTENT", productId, minuteBucketNow());
99
+ const contentIds = options.contentIds ?? [productId];
100
+ const customData = {
101
+ ...options.currency ? { currency: options.currency } : {},
102
+ ...options.value !== void 0 ? { value: options.value } : {},
103
+ ...contentIds.length ? { contentIds } : {},
104
+ ...options.contents?.length ? { contents: options.contents } : {},
105
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {}
106
+ };
107
+ safeSnaptr("track", "VIEW_CONTENT", {
108
+ ...contentIds.length ? { item_ids: contentIds } : {},
109
+ ...options.currency ? { currency: options.currency } : {},
110
+ ...options.value !== void 0 ? { value: options.value } : {},
111
+ client_dedup_id: eid
112
+ });
113
+ void sendSnapchatCapiEvent({
114
+ eventName: "VIEW_CONTENT",
115
+ eventId: eid,
116
+ eventSourceUrl: currentUrl(),
117
+ userData: options.userData ?? {},
118
+ customData
119
+ });
120
+ }
121
+ function trackAddCart(cartId, options = {}) {
122
+ const eid = options.eventId ?? makeEventId("ADD_CART", cartId, minuteBucketNow());
123
+ const customData = {
124
+ ...options.currency ? { currency: options.currency } : {},
125
+ ...options.value !== void 0 ? { value: options.value } : {},
126
+ ...options.contentIds?.length ? { contentIds: options.contentIds } : {},
127
+ ...options.contents?.length ? { contents: options.contents } : {},
128
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {}
129
+ };
130
+ safeSnaptr("track", "ADD_CART", {
131
+ ...options.contentIds?.length ? { item_ids: options.contentIds } : {},
132
+ ...options.currency ? { currency: options.currency } : {},
133
+ ...options.value !== void 0 ? { value: options.value } : {},
134
+ ...typeof options.numItems === "number" ? { number_items: options.numItems } : {},
135
+ client_dedup_id: eid
136
+ });
137
+ void sendSnapchatCapiEvent({
138
+ eventName: "ADD_CART",
139
+ eventId: eid,
140
+ eventSourceUrl: currentUrl(),
141
+ userData: options.userData ?? {},
142
+ customData
143
+ });
144
+ }
145
+ function trackStartCheckout(cartId, options = {}) {
146
+ const eid = options.eventId ?? makeEventId("START_CHECKOUT", cartId, minuteBucketNow());
147
+ const customData = {
148
+ ...options.currency ? { currency: options.currency } : {},
149
+ ...options.value !== void 0 ? { value: options.value } : {},
150
+ ...options.contentIds?.length ? { contentIds: options.contentIds } : {},
151
+ ...options.contents?.length ? { contents: options.contents } : {},
152
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {}
153
+ };
154
+ safeSnaptr("track", "START_CHECKOUT", {
155
+ ...options.contentIds?.length ? { item_ids: options.contentIds } : {},
156
+ ...options.currency ? { currency: options.currency } : {},
157
+ ...options.value !== void 0 ? { value: options.value } : {},
158
+ ...typeof options.numItems === "number" ? { number_items: options.numItems } : {},
159
+ client_dedup_id: eid
160
+ });
161
+ void sendSnapchatCapiEvent({
162
+ eventName: "START_CHECKOUT",
163
+ eventId: eid,
164
+ eventSourceUrl: currentUrl(),
165
+ userData: options.userData ?? {},
166
+ customData
167
+ });
168
+ }
169
+ function trackPurchase(orderId, options = {}) {
170
+ const eid = options.eventId ?? makeEventId("PURCHASE", orderId, 0);
171
+ const derivedOrderId = deriveOrderIdForPurchase("PURCHASE", orderId);
172
+ const customData = {
173
+ ...options.currency ? { currency: options.currency } : {},
174
+ ...options.value !== void 0 ? { value: options.value } : {},
175
+ ...options.contentIds?.length ? { contentIds: options.contentIds } : {},
176
+ ...options.contents?.length ? { contents: options.contents } : {},
177
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {},
178
+ ...derivedOrderId ? { orderId: derivedOrderId } : {}
179
+ };
180
+ safeSnaptr("track", "PURCHASE", {
181
+ ...options.contentIds?.length ? { item_ids: options.contentIds } : {},
182
+ ...options.currency ? { currency: options.currency } : {},
183
+ ...options.value !== void 0 ? { value: options.value } : {},
184
+ ...typeof options.numItems === "number" ? { number_items: options.numItems } : {},
185
+ ...derivedOrderId ? { transaction_id: derivedOrderId } : {},
186
+ client_dedup_id: eid
187
+ });
188
+ void sendSnapchatCapiEvent({
189
+ eventName: "PURCHASE",
190
+ eventId: eid,
191
+ eventSourceUrl: currentUrl(),
192
+ userData: options.userData ?? {},
193
+ customData
194
+ });
195
+ }
196
+
197
+ // src/get-config.ts
198
+ function resolveEnv(key) {
199
+ if (key === "NEXT_PUBLIC_MEDUSA_BACKEND_URL") return process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL;
200
+ if (key === "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY") return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY;
201
+ if (key === "NEXT_PUBLIC_STORE_ID") return process.env.NEXT_PUBLIC_STORE_ID;
202
+ return void 0;
203
+ }
204
+ async function getSnapchatAdsConfig(options = {}) {
205
+ const baseUrl = options.baseUrl ?? resolveEnv("NEXT_PUBLIC_MEDUSA_BACKEND_URL") ?? "http://localhost:9000";
206
+ const publishableKey = options.publishableApiKey ?? resolveEnv("NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY");
207
+ const storeId = options.storeId ?? resolveEnv("NEXT_PUBLIC_STORE_ID");
208
+ try {
209
+ const res = await fetch(`${baseUrl}/store/snapchat-ads/config`, {
210
+ headers: {
211
+ ...storeId ? { "X-Store-Environment-ID": storeId } : {},
212
+ ...publishableKey ? { "x-publishable-api-key": publishableKey } : {}
213
+ },
214
+ next: { revalidate: 60 }
215
+ });
216
+ if (!res.ok) return null;
217
+ return await res.json();
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+ // Annotate the CommonJS export names for ESM import in node:
223
+ 0 && (module.exports = {
224
+ deriveMinuteBucket,
225
+ getSnapchatAdsConfig,
226
+ makeEventId,
227
+ minuteBucketNow,
228
+ sendSnapchatCapiEvent,
229
+ trackAddCart,
230
+ trackPageView,
231
+ trackPurchase,
232
+ trackStartCheckout,
233
+ trackViewContent
234
+ });
235
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/lib/snap-pixel-loader.ts","../src/lib/dedup.ts","../src/hooks/useSnapchatEvents.ts","../src/get-config.ts"],"sourcesContent":["export {\n trackPageView,\n trackViewContent,\n trackAddCart,\n trackStartCheckout,\n trackPurchase,\n sendSnapchatCapiEvent,\n} from './hooks/useSnapchatEvents'\n\nexport { getSnapchatAdsConfig } from './get-config'\n\nexport { makeEventId, deriveMinuteBucket, minuteBucketNow } from './lib/dedup'\n\n// `SnapchatAdsConfig` (the secret-bearing full shape with\n// `snap_conversion_api_token` / `snap_ad_account_id`) is intentionally NOT\n// exported: those fields never leave the Medusa side, and re-exporting the\n// type from a browser package invites callers to write handling that\n// assumes a secret is present. Storefront code consumes\n// `SnapchatAdsPublicConfig` only.\nexport type {\n SnapchatAdsPublicConfig,\n SnapEventName,\n SnapUserData,\n SnapCustomData,\n SnapEventPayload,\n} from './types'\n","/**\n * Snap Pixel loader - installs the `snaptr` shim and injects\n * `https://sc-static.net/scevent.min.js`. Snap does NOT ship an\n * npm-installable browser SDK (research memo Section 10), so we inline\n * the same IIFE shape the official Snap install snippet emits. Once the\n * script loads, Snap reconciles the shim queue and subsequent calls\n * flow directly through.\n *\n * The shim mirrors Snap's canonical shape so their loaded pixel finds\n * the expected `handleRequest` / `queue` fields and picks up buffered\n * calls made before the network load completes.\n *\n * Idempotent: subsequent calls no-op if `window.snaptr` is already set,\n * so consent flips or SPA nav doesn't stack multiple scripts.\n */\n\ndeclare global {\n interface Window {\n snaptr?: SnaptrFunction\n }\n}\n\nexport type SnaptrFunction = ((...args: unknown[]) => void) & {\n handleRequest?: (...args: unknown[]) => void\n push?: (...args: unknown[]) => void\n queue?: unknown[][]\n version?: string\n loaded?: boolean\n}\n\nconst SCRIPT_SRC = 'https://sc-static.net/scevent.min.js'\n\n/**\n * Install the snaptr shim + inject `sc-static.net/scevent.min.js`.\n * Idempotent - safe to call multiple times.\n */\nexport function installSnapPixelShim(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n if (window.snaptr) return\n\n const queue: unknown[][] = []\n const shim: SnaptrFunction = function (...args: unknown[]): void {\n if (typeof shim.handleRequest === 'function') {\n shim.handleRequest(...args)\n return\n }\n queue.push(args)\n } as SnaptrFunction\n shim.push = function (...args: unknown[]): void {\n queue.push(args)\n }\n shim.queue = queue\n shim.version = '1.0'\n shim.loaded = true\n window.snaptr = shim\n\n const script = document.createElement('script')\n script.async = true\n script.src = SCRIPT_SRC\n document.head.appendChild(script)\n}\n\n/**\n * Best-effort synchronous call. Returns whether snaptr was ready. Slot\n * components use this so events queued before `scevent.min.js` finishes\n * loading still land - Snap's script drains the shim queue on load.\n */\nexport function safeSnaptr(...args: unknown[]): boolean {\n if (typeof window === 'undefined') return false\n const fn = window.snaptr\n if (typeof fn !== 'function') return false\n try {\n fn(...args)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Poll `window.snaptr` on `requestAnimationFrame` up to `maxAttempts`\n * times, invoking the callback on the first tick where snaptr exists.\n * Escape hatch for callers that must run the moment snaptr becomes\n * ready. The standard slot components rely on the shim queue via\n * `safeSnaptr()`.\n */\nexport function whenSnaptrReady(cb: (fn: SnaptrFunction) => void, maxAttempts = 20): () => void {\n if (typeof window === 'undefined') return () => {}\n let cancelled = false\n let attempts = 0\n\n const tick = (): void => {\n if (cancelled) return\n attempts += 1\n const fn = window.snaptr\n if (typeof fn === 'function') {\n cb(fn)\n return\n }\n if (attempts >= maxAttempts) return\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(tick)\n } else {\n setTimeout(tick, 50)\n }\n }\n\n tick()\n return () => {\n cancelled = true\n }\n}\n","import type { SnapEventName } from '../types'\n\n/**\n * Deterministic event-ID builder for Snap browser <-> CAPI dedup.\n *\n * Event ID contract (research memo Section 5, encoded verbatim):\n *\n * > For the 5 events (PAGE_VIEW, VIEW_CONTENT, ADD_CART, START_CHECKOUT,\n * > PURCHASE), the browser Pixel call and the CAPI server call MUST share a\n * > top-level `event_id` (Pixel side: `client_dedup_id`), matched by Snap\n * > within a 48-hour window. For PURCHASE ONLY, they MUST additionally share\n * > `custom_data.order_id` (CAPI) / `transaction_id` (Pixel), matched within\n * > a 30-day window. Reuse the Amboras cart/order id as both `order_id` and\n * > `transaction_id`; mint a deterministic UUID from `(event_name,\n * > cart_id_or_session_id, minute_bucket)` for `event_id`/`client_dedup_id`.\n * > Never let the Pixel SDK auto-mint `client_dedup_id` - it prefixes with\n * > `@-` and never matches CAPI (research memo Section 5).\n *\n * Determinism matters because the browser Pixel call and the CAPI relay\n * call happen in parallel from the same user action - if the IDs don't\n * match, Snap counts the event twice.\n *\n * Contract:\n * - Same `(eventName, primaryKey, minuteBucket)` always produces the\n * same ID.\n * - Different event names with the same primary key produce different\n * IDs (a `VIEW_CONTENT` and `ADD_CART` for the same product shouldn't\n * collide).\n * - The minute-bucket component keeps browser retries within the same\n * minute stable (both sides land on the same ID), but rotates the ID\n * for user actions in a subsequent minute so we don't accidentally\n * collapse two genuine events.\n */\n\nconst MS_PER_MINUTE = 60 * 1000\n\n/** Bucket a Unix millisecond timestamp into whole minutes. */\nexport function deriveMinuteBucket(nowMs: number): number {\n if (!Number.isFinite(nowMs)) return 0\n return Math.floor(nowMs / MS_PER_MINUTE)\n}\n\n/** Convenience wrapper for `deriveMinuteBucket(Date.now())`. */\nexport function minuteBucketNow(): number {\n return deriveMinuteBucket(Date.now())\n}\n\n/**\n * Sanitize a primary key for inclusion in an event id. Snap accepts\n * alphanumerics + `_` + `-`; anything else gets folded to `_` so a\n * merchant using URLs with query strings as page-view keys doesn't\n * produce malformed ids.\n */\nfunction safeKey(primaryKey: string): string {\n if (!primaryKey) return 'unknown'\n return primaryKey.replace(/[^a-zA-Z0-9_-]/g, '_')\n}\n\n/**\n * Mint a deterministic event id for `event_id` (CAPI) and\n * `client_dedup_id` (Pixel). See contract above.\n *\n * Callers that want a stable id across a longer window (e.g. tying two\n * page-view fires back to back to the same URL) can pin `minuteBucket` to\n * a well-known value; the default is `minuteBucketNow()`. For PURCHASE,\n * pin `minuteBucket` to a constant per order (recommend `0`) so a\n * failing browser fire that retries in a later minute still dedups\n * against the server event.\n */\nexport function makeEventId(\n eventName: SnapEventName,\n primaryKey: string,\n minuteBucket: number = minuteBucketNow(),\n): string {\n return `${eventName}_${safeKey(primaryKey)}_${minuteBucket}`\n}\n\n/**\n * Derivation helper for the PURCHASE event's 30-day dedup pair. Reuses\n * the Amboras order id as both `custom_data.order_id` (CAPI) and\n * `transaction_id` (Pixel) so Snap can dedup on the 30-day window even\n * if the 48h `event_id` window has expired.\n *\n * Returns `undefined` for non-PURCHASE events (encodes the \"PURCHASE\n * only\" leg of the dual-dedup scheme).\n */\nexport function deriveOrderIdForPurchase(\n eventName: SnapEventName,\n orderId: string | undefined,\n): string | undefined {\n if (eventName !== 'PURCHASE') return undefined\n if (typeof orderId !== 'string') return undefined\n const trimmed = orderId.trim()\n return trimmed ? trimmed : undefined\n}\n","import type { SnapEventName, SnapEventPayload, SnapUserData, SnapCustomData } from '../types'\nimport { safeSnaptr } from '../lib/snap-pixel-loader'\nimport { makeEventId, minuteBucketNow, deriveOrderIdForPurchase } from '../lib/dedup'\n\n/**\n * Client-side dual-fire hooks for Snap. Every event flows two ways from\n * the same user action:\n *\n * 1. Browser pixel: `snaptr('track', <EVENT_NAME>, { ..., client_dedup_id })`\n * via the loaded `scevent.min.js`.\n * 2. Server-side CAPI: POST to the storefront proxy at\n * `/api/snapchat-ads/events` which enriches with\n * `client_ip_address` + `client_user_agent` and forwards to Medusa\n * `/store/snapchat-ads/capi`.\n *\n * Both sides carry the SAME `event_id` / `client_dedup_id` so Snap can\n * dedup within 48h. For PURCHASE only, both sides also share\n * `custom_data.order_id` / `transaction_id` for the 30-day dedup window\n * (research memo Section 5).\n *\n * Snap v3 wire caveats encoded here:\n * - Event names are UPPER_SNAKE_CASE (PAGE_VIEW / VIEW_CONTENT /\n * ADD_CART / START_CHECKOUT / PURCHASE).\n * - `value`, `item_price`, `quantity` are STRINGS in the CAPI payload.\n * The browser pixel is more lenient but we mirror the same string\n * serialization so the two sides stay symmetric and dedup diffs read\n * cleanly in logs.\n * - PII (`em`, `ph`, `madid`, `external_id`) is passed RAW from the\n * browser and hashed on the Medusa side by `sendSnapchatCapiEvent`.\n * The proxy strips raw PII before forwarding to Snap.\n *\n * All fires are non-throwing - a failed CAPI relay is logged, never\n * bubbled, so a merchant's storefront cannot break because Snap's edge\n * had a hiccup.\n */\n\nconst CAPI_PROXY_ENDPOINT = '/api/snapchat-ads/events'\n\nfunction currentUrl(): string {\n return typeof window !== 'undefined' ? window.location.href : ''\n}\n\n/**\n * Send a server-side Snap CAPI event via the storefront's proxy. Fire-\n * and-forget from the caller's perspective; internal errors are swallowed\n * because analytics loss is preferable to storefront breakage.\n *\n * Intentionally NOT gated on analytics consent - Snap CAPI operates on a\n * different legal basis (order-tracking legitimate interest) than the\n * browser pixel. The `SnapPixelProvider` handles browser-side consent\n * gating separately.\n */\nexport async function sendSnapchatCapiEvent(payload: SnapEventPayload): Promise<void> {\n try {\n await fetch(CAPI_PROXY_ENDPOINT, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch {\n // non-fatal\n }\n}\n\n/**\n * Fire PAGE_VIEW on both sides. The `SnapPixelProvider` already fires\n * this on mount + route change; this hook is exposed for callers that\n * want to trigger a page-view manually (e.g. modal opens counted as\n * page views).\n */\nexport function trackPageView(\n primaryKey: string,\n userData: SnapUserData = {},\n eventId?: string,\n): void {\n const eid = eventId ?? makeEventId('PAGE_VIEW', primaryKey, minuteBucketNow())\n safeSnaptr('track', 'PAGE_VIEW', { client_dedup_id: eid })\n void sendSnapchatCapiEvent({\n eventName: 'PAGE_VIEW',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData,\n })\n}\n\nexport interface TrackViewContentOptions {\n currency?: string\n /** Major-unit value STRING (e.g. `\"10.50\"`). */\n value?: string\n /** Product / variant id passed to Snap as `content_ids`. */\n contentIds?: string[]\n /** Optional per-line breakdown. `item_price` + `quantity` are STRINGS. */\n contents?: Array<{ id: string; quantity: string; item_price?: string }>\n numItems?: number\n userData?: SnapUserData\n eventId?: string\n}\n\nexport function trackViewContent(productId: string, options: TrackViewContentOptions = {}): void {\n const eid = options.eventId ?? makeEventId('VIEW_CONTENT', productId, minuteBucketNow())\n const contentIds = options.contentIds ?? [productId]\n const customData: SnapCustomData = {\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(contentIds.length ? { contentIds } : {}),\n ...(options.contents?.length ? { contents: options.contents } : {}),\n ...(typeof options.numItems === 'number' ? { numItems: options.numItems } : {}),\n }\n safeSnaptr('track', 'VIEW_CONTENT', {\n ...(contentIds.length ? { item_ids: contentIds } : {}),\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n client_dedup_id: eid,\n })\n void sendSnapchatCapiEvent({\n eventName: 'VIEW_CONTENT',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData: options.userData ?? {},\n customData,\n })\n}\n\nexport interface TrackAddCartOptions {\n currency?: string\n /** Major-unit value STRING. */\n value?: string\n /** Product / variant ids. */\n contentIds?: string[]\n contents?: Array<{ id: string; quantity: string; item_price?: string }>\n /** Top-level `num_items` - number-typed per Snap docs. */\n numItems?: number\n userData?: SnapUserData\n eventId?: string\n}\n\nexport function trackAddCart(cartId: string, options: TrackAddCartOptions = {}): void {\n const eid = options.eventId ?? makeEventId('ADD_CART', cartId, minuteBucketNow())\n const customData: SnapCustomData = {\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(options.contentIds?.length ? { contentIds: options.contentIds } : {}),\n ...(options.contents?.length ? { contents: options.contents } : {}),\n ...(typeof options.numItems === 'number' ? { numItems: options.numItems } : {}),\n }\n safeSnaptr('track', 'ADD_CART', {\n ...(options.contentIds?.length ? { item_ids: options.contentIds } : {}),\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(typeof options.numItems === 'number' ? { number_items: options.numItems } : {}),\n client_dedup_id: eid,\n })\n void sendSnapchatCapiEvent({\n eventName: 'ADD_CART',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData: options.userData ?? {},\n customData,\n })\n}\n\nexport interface TrackStartCheckoutOptions {\n currency?: string\n value?: string\n contentIds?: string[]\n contents?: Array<{ id: string; quantity: string; item_price?: string }>\n numItems?: number\n userData?: SnapUserData\n eventId?: string\n}\n\nexport function trackStartCheckout(cartId: string, options: TrackStartCheckoutOptions = {}): void {\n const eid = options.eventId ?? makeEventId('START_CHECKOUT', cartId, minuteBucketNow())\n const customData: SnapCustomData = {\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(options.contentIds?.length ? { contentIds: options.contentIds } : {}),\n ...(options.contents?.length ? { contents: options.contents } : {}),\n ...(typeof options.numItems === 'number' ? { numItems: options.numItems } : {}),\n }\n safeSnaptr('track', 'START_CHECKOUT', {\n ...(options.contentIds?.length ? { item_ids: options.contentIds } : {}),\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(typeof options.numItems === 'number' ? { number_items: options.numItems } : {}),\n client_dedup_id: eid,\n })\n void sendSnapchatCapiEvent({\n eventName: 'START_CHECKOUT',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData: options.userData ?? {},\n customData,\n })\n}\n\nexport interface TrackPurchaseOptions {\n currency?: string\n value?: string\n contentIds?: string[]\n contents?: Array<{ id: string; quantity: string; item_price?: string }>\n numItems?: number\n userData?: SnapUserData\n /**\n * When present, pins `event_id` / `client_dedup_id` explicitly; otherwise\n * a stable id is derived from `(PURCHASE, orderId, 0)` - minute-bucket 0\n * so retries within a longer window still dedup against the server event.\n */\n eventId?: string\n}\n\n/**\n * PURCHASE gets the dual-dedup treatment:\n * - Top-level `event_id` / Pixel `client_dedup_id` shared for 48h window.\n * - `custom_data.order_id` / Pixel `transaction_id` shared for 30-day\n * window. Reuses the Amboras order id as both.\n */\nexport function trackPurchase(orderId: string, options: TrackPurchaseOptions = {}): void {\n // Pin minute-bucket to 0 for PURCHASE so a browser retry in a later\n // minute still lands on the same event_id as the initial server fire.\n const eid = options.eventId ?? makeEventId('PURCHASE', orderId, 0)\n const derivedOrderId = deriveOrderIdForPurchase('PURCHASE', orderId)\n const customData: SnapCustomData = {\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(options.contentIds?.length ? { contentIds: options.contentIds } : {}),\n ...(options.contents?.length ? { contents: options.contents } : {}),\n ...(typeof options.numItems === 'number' ? { numItems: options.numItems } : {}),\n ...(derivedOrderId ? { orderId: derivedOrderId } : {}),\n }\n safeSnaptr('track', 'PURCHASE', {\n ...(options.contentIds?.length ? { item_ids: options.contentIds } : {}),\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(typeof options.numItems === 'number' ? { number_items: options.numItems } : {}),\n ...(derivedOrderId ? { transaction_id: derivedOrderId } : {}),\n client_dedup_id: eid,\n })\n void sendSnapchatCapiEvent({\n eventName: 'PURCHASE',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData: options.userData ?? {},\n customData,\n })\n}\n","import type { SnapchatAdsPublicConfig } from './types'\n\nfunction resolveEnv(key: string): string | undefined {\n if (key === 'NEXT_PUBLIC_MEDUSA_BACKEND_URL') return process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL\n if (key === 'NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY') return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY\n if (key === 'NEXT_PUBLIC_STORE_ID') return process.env.NEXT_PUBLIC_STORE_ID\n return undefined\n}\n\ninterface GetSnapchatAdsConfigOptions {\n baseUrl?: string\n publishableApiKey?: string\n storeId?: string\n}\n\n/**\n * Fetch the non-secret Snapchat Ads config from the Medusa Store API. Returns\n * `null` when the store isn't wired up, when the publishable key is missing,\n * or on any transport error - callers should treat `null` as \"plugin\n * off\" and skip event firing.\n *\n * NEVER surfaces `snap_conversion_api_token` or `snap_ad_account_id` -\n * those live only on the Medusa side and never leave the server.\n */\nexport async function getSnapchatAdsConfig(\n options: GetSnapchatAdsConfigOptions = {},\n): Promise<SnapchatAdsPublicConfig | null> {\n const baseUrl = options.baseUrl ?? resolveEnv('NEXT_PUBLIC_MEDUSA_BACKEND_URL') ?? 'http://localhost:9000'\n const publishableKey = options.publishableApiKey ?? resolveEnv('NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY')\n const storeId = options.storeId ?? resolveEnv('NEXT_PUBLIC_STORE_ID')\n\n try {\n // `next` is a Next.js fetch extension for cache revalidation - ignored outside Next.js\n const res = await fetch(`${baseUrl}/store/snapchat-ads/config`, {\n headers: {\n ...(storeId ? { 'X-Store-Environment-ID': storeId } : {}),\n ...(publishableKey ? { 'x-publishable-api-key': publishableKey } : {}),\n },\n next: { revalidate: 60 },\n } as RequestInit)\n if (!res.ok) return null\n return (await res.json()) as SnapchatAdsPublicConfig\n } catch {\n return null\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,177 @@
1
+ import {
2
+ getSnapchatAdsConfig
3
+ } from "./chunk-PK23QMZT.mjs";
4
+
5
+ // src/lib/snap-pixel-loader.ts
6
+ function safeSnaptr(...args) {
7
+ if (typeof window === "undefined") return false;
8
+ const fn = window.snaptr;
9
+ if (typeof fn !== "function") return false;
10
+ try {
11
+ fn(...args);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ // src/lib/dedup.ts
19
+ var MS_PER_MINUTE = 60 * 1e3;
20
+ function deriveMinuteBucket(nowMs) {
21
+ if (!Number.isFinite(nowMs)) return 0;
22
+ return Math.floor(nowMs / MS_PER_MINUTE);
23
+ }
24
+ function minuteBucketNow() {
25
+ return deriveMinuteBucket(Date.now());
26
+ }
27
+ function safeKey(primaryKey) {
28
+ if (!primaryKey) return "unknown";
29
+ return primaryKey.replace(/[^a-zA-Z0-9_-]/g, "_");
30
+ }
31
+ function makeEventId(eventName, primaryKey, minuteBucket = minuteBucketNow()) {
32
+ return `${eventName}_${safeKey(primaryKey)}_${minuteBucket}`;
33
+ }
34
+ function deriveOrderIdForPurchase(eventName, orderId) {
35
+ if (eventName !== "PURCHASE") return void 0;
36
+ if (typeof orderId !== "string") return void 0;
37
+ const trimmed = orderId.trim();
38
+ return trimmed ? trimmed : void 0;
39
+ }
40
+
41
+ // src/hooks/useSnapchatEvents.ts
42
+ var CAPI_PROXY_ENDPOINT = "/api/snapchat-ads/events";
43
+ function currentUrl() {
44
+ return typeof window !== "undefined" ? window.location.href : "";
45
+ }
46
+ async function sendSnapchatCapiEvent(payload) {
47
+ try {
48
+ await fetch(CAPI_PROXY_ENDPOINT, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify(payload)
52
+ });
53
+ } catch {
54
+ }
55
+ }
56
+ function trackPageView(primaryKey, userData = {}, eventId) {
57
+ const eid = eventId ?? makeEventId("PAGE_VIEW", primaryKey, minuteBucketNow());
58
+ safeSnaptr("track", "PAGE_VIEW", { client_dedup_id: eid });
59
+ void sendSnapchatCapiEvent({
60
+ eventName: "PAGE_VIEW",
61
+ eventId: eid,
62
+ eventSourceUrl: currentUrl(),
63
+ userData
64
+ });
65
+ }
66
+ function trackViewContent(productId, options = {}) {
67
+ const eid = options.eventId ?? makeEventId("VIEW_CONTENT", productId, minuteBucketNow());
68
+ const contentIds = options.contentIds ?? [productId];
69
+ const customData = {
70
+ ...options.currency ? { currency: options.currency } : {},
71
+ ...options.value !== void 0 ? { value: options.value } : {},
72
+ ...contentIds.length ? { contentIds } : {},
73
+ ...options.contents?.length ? { contents: options.contents } : {},
74
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {}
75
+ };
76
+ safeSnaptr("track", "VIEW_CONTENT", {
77
+ ...contentIds.length ? { item_ids: contentIds } : {},
78
+ ...options.currency ? { currency: options.currency } : {},
79
+ ...options.value !== void 0 ? { value: options.value } : {},
80
+ client_dedup_id: eid
81
+ });
82
+ void sendSnapchatCapiEvent({
83
+ eventName: "VIEW_CONTENT",
84
+ eventId: eid,
85
+ eventSourceUrl: currentUrl(),
86
+ userData: options.userData ?? {},
87
+ customData
88
+ });
89
+ }
90
+ function trackAddCart(cartId, options = {}) {
91
+ const eid = options.eventId ?? makeEventId("ADD_CART", cartId, minuteBucketNow());
92
+ const customData = {
93
+ ...options.currency ? { currency: options.currency } : {},
94
+ ...options.value !== void 0 ? { value: options.value } : {},
95
+ ...options.contentIds?.length ? { contentIds: options.contentIds } : {},
96
+ ...options.contents?.length ? { contents: options.contents } : {},
97
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {}
98
+ };
99
+ safeSnaptr("track", "ADD_CART", {
100
+ ...options.contentIds?.length ? { item_ids: options.contentIds } : {},
101
+ ...options.currency ? { currency: options.currency } : {},
102
+ ...options.value !== void 0 ? { value: options.value } : {},
103
+ ...typeof options.numItems === "number" ? { number_items: options.numItems } : {},
104
+ client_dedup_id: eid
105
+ });
106
+ void sendSnapchatCapiEvent({
107
+ eventName: "ADD_CART",
108
+ eventId: eid,
109
+ eventSourceUrl: currentUrl(),
110
+ userData: options.userData ?? {},
111
+ customData
112
+ });
113
+ }
114
+ function trackStartCheckout(cartId, options = {}) {
115
+ const eid = options.eventId ?? makeEventId("START_CHECKOUT", cartId, minuteBucketNow());
116
+ const customData = {
117
+ ...options.currency ? { currency: options.currency } : {},
118
+ ...options.value !== void 0 ? { value: options.value } : {},
119
+ ...options.contentIds?.length ? { contentIds: options.contentIds } : {},
120
+ ...options.contents?.length ? { contents: options.contents } : {},
121
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {}
122
+ };
123
+ safeSnaptr("track", "START_CHECKOUT", {
124
+ ...options.contentIds?.length ? { item_ids: options.contentIds } : {},
125
+ ...options.currency ? { currency: options.currency } : {},
126
+ ...options.value !== void 0 ? { value: options.value } : {},
127
+ ...typeof options.numItems === "number" ? { number_items: options.numItems } : {},
128
+ client_dedup_id: eid
129
+ });
130
+ void sendSnapchatCapiEvent({
131
+ eventName: "START_CHECKOUT",
132
+ eventId: eid,
133
+ eventSourceUrl: currentUrl(),
134
+ userData: options.userData ?? {},
135
+ customData
136
+ });
137
+ }
138
+ function trackPurchase(orderId, options = {}) {
139
+ const eid = options.eventId ?? makeEventId("PURCHASE", orderId, 0);
140
+ const derivedOrderId = deriveOrderIdForPurchase("PURCHASE", orderId);
141
+ const customData = {
142
+ ...options.currency ? { currency: options.currency } : {},
143
+ ...options.value !== void 0 ? { value: options.value } : {},
144
+ ...options.contentIds?.length ? { contentIds: options.contentIds } : {},
145
+ ...options.contents?.length ? { contents: options.contents } : {},
146
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {},
147
+ ...derivedOrderId ? { orderId: derivedOrderId } : {}
148
+ };
149
+ safeSnaptr("track", "PURCHASE", {
150
+ ...options.contentIds?.length ? { item_ids: options.contentIds } : {},
151
+ ...options.currency ? { currency: options.currency } : {},
152
+ ...options.value !== void 0 ? { value: options.value } : {},
153
+ ...typeof options.numItems === "number" ? { number_items: options.numItems } : {},
154
+ ...derivedOrderId ? { transaction_id: derivedOrderId } : {},
155
+ client_dedup_id: eid
156
+ });
157
+ void sendSnapchatCapiEvent({
158
+ eventName: "PURCHASE",
159
+ eventId: eid,
160
+ eventSourceUrl: currentUrl(),
161
+ userData: options.userData ?? {},
162
+ customData
163
+ });
164
+ }
165
+ export {
166
+ deriveMinuteBucket,
167
+ getSnapchatAdsConfig,
168
+ makeEventId,
169
+ minuteBucketNow,
170
+ sendSnapchatCapiEvent,
171
+ trackAddCart,
172
+ trackPageView,
173
+ trackPurchase,
174
+ trackStartCheckout,
175
+ trackViewContent
176
+ };
177
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/snap-pixel-loader.ts","../src/lib/dedup.ts","../src/hooks/useSnapchatEvents.ts"],"sourcesContent":["/**\n * Snap Pixel loader - installs the `snaptr` shim and injects\n * `https://sc-static.net/scevent.min.js`. Snap does NOT ship an\n * npm-installable browser SDK (research memo Section 10), so we inline\n * the same IIFE shape the official Snap install snippet emits. Once the\n * script loads, Snap reconciles the shim queue and subsequent calls\n * flow directly through.\n *\n * The shim mirrors Snap's canonical shape so their loaded pixel finds\n * the expected `handleRequest` / `queue` fields and picks up buffered\n * calls made before the network load completes.\n *\n * Idempotent: subsequent calls no-op if `window.snaptr` is already set,\n * so consent flips or SPA nav doesn't stack multiple scripts.\n */\n\ndeclare global {\n interface Window {\n snaptr?: SnaptrFunction\n }\n}\n\nexport type SnaptrFunction = ((...args: unknown[]) => void) & {\n handleRequest?: (...args: unknown[]) => void\n push?: (...args: unknown[]) => void\n queue?: unknown[][]\n version?: string\n loaded?: boolean\n}\n\nconst SCRIPT_SRC = 'https://sc-static.net/scevent.min.js'\n\n/**\n * Install the snaptr shim + inject `sc-static.net/scevent.min.js`.\n * Idempotent - safe to call multiple times.\n */\nexport function installSnapPixelShim(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n if (window.snaptr) return\n\n const queue: unknown[][] = []\n const shim: SnaptrFunction = function (...args: unknown[]): void {\n if (typeof shim.handleRequest === 'function') {\n shim.handleRequest(...args)\n return\n }\n queue.push(args)\n } as SnaptrFunction\n shim.push = function (...args: unknown[]): void {\n queue.push(args)\n }\n shim.queue = queue\n shim.version = '1.0'\n shim.loaded = true\n window.snaptr = shim\n\n const script = document.createElement('script')\n script.async = true\n script.src = SCRIPT_SRC\n document.head.appendChild(script)\n}\n\n/**\n * Best-effort synchronous call. Returns whether snaptr was ready. Slot\n * components use this so events queued before `scevent.min.js` finishes\n * loading still land - Snap's script drains the shim queue on load.\n */\nexport function safeSnaptr(...args: unknown[]): boolean {\n if (typeof window === 'undefined') return false\n const fn = window.snaptr\n if (typeof fn !== 'function') return false\n try {\n fn(...args)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Poll `window.snaptr` on `requestAnimationFrame` up to `maxAttempts`\n * times, invoking the callback on the first tick where snaptr exists.\n * Escape hatch for callers that must run the moment snaptr becomes\n * ready. The standard slot components rely on the shim queue via\n * `safeSnaptr()`.\n */\nexport function whenSnaptrReady(cb: (fn: SnaptrFunction) => void, maxAttempts = 20): () => void {\n if (typeof window === 'undefined') return () => {}\n let cancelled = false\n let attempts = 0\n\n const tick = (): void => {\n if (cancelled) return\n attempts += 1\n const fn = window.snaptr\n if (typeof fn === 'function') {\n cb(fn)\n return\n }\n if (attempts >= maxAttempts) return\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(tick)\n } else {\n setTimeout(tick, 50)\n }\n }\n\n tick()\n return () => {\n cancelled = true\n }\n}\n","import type { SnapEventName } from '../types'\n\n/**\n * Deterministic event-ID builder for Snap browser <-> CAPI dedup.\n *\n * Event ID contract (research memo Section 5, encoded verbatim):\n *\n * > For the 5 events (PAGE_VIEW, VIEW_CONTENT, ADD_CART, START_CHECKOUT,\n * > PURCHASE), the browser Pixel call and the CAPI server call MUST share a\n * > top-level `event_id` (Pixel side: `client_dedup_id`), matched by Snap\n * > within a 48-hour window. For PURCHASE ONLY, they MUST additionally share\n * > `custom_data.order_id` (CAPI) / `transaction_id` (Pixel), matched within\n * > a 30-day window. Reuse the Amboras cart/order id as both `order_id` and\n * > `transaction_id`; mint a deterministic UUID from `(event_name,\n * > cart_id_or_session_id, minute_bucket)` for `event_id`/`client_dedup_id`.\n * > Never let the Pixel SDK auto-mint `client_dedup_id` - it prefixes with\n * > `@-` and never matches CAPI (research memo Section 5).\n *\n * Determinism matters because the browser Pixel call and the CAPI relay\n * call happen in parallel from the same user action - if the IDs don't\n * match, Snap counts the event twice.\n *\n * Contract:\n * - Same `(eventName, primaryKey, minuteBucket)` always produces the\n * same ID.\n * - Different event names with the same primary key produce different\n * IDs (a `VIEW_CONTENT` and `ADD_CART` for the same product shouldn't\n * collide).\n * - The minute-bucket component keeps browser retries within the same\n * minute stable (both sides land on the same ID), but rotates the ID\n * for user actions in a subsequent minute so we don't accidentally\n * collapse two genuine events.\n */\n\nconst MS_PER_MINUTE = 60 * 1000\n\n/** Bucket a Unix millisecond timestamp into whole minutes. */\nexport function deriveMinuteBucket(nowMs: number): number {\n if (!Number.isFinite(nowMs)) return 0\n return Math.floor(nowMs / MS_PER_MINUTE)\n}\n\n/** Convenience wrapper for `deriveMinuteBucket(Date.now())`. */\nexport function minuteBucketNow(): number {\n return deriveMinuteBucket(Date.now())\n}\n\n/**\n * Sanitize a primary key for inclusion in an event id. Snap accepts\n * alphanumerics + `_` + `-`; anything else gets folded to `_` so a\n * merchant using URLs with query strings as page-view keys doesn't\n * produce malformed ids.\n */\nfunction safeKey(primaryKey: string): string {\n if (!primaryKey) return 'unknown'\n return primaryKey.replace(/[^a-zA-Z0-9_-]/g, '_')\n}\n\n/**\n * Mint a deterministic event id for `event_id` (CAPI) and\n * `client_dedup_id` (Pixel). See contract above.\n *\n * Callers that want a stable id across a longer window (e.g. tying two\n * page-view fires back to back to the same URL) can pin `minuteBucket` to\n * a well-known value; the default is `minuteBucketNow()`. For PURCHASE,\n * pin `minuteBucket` to a constant per order (recommend `0`) so a\n * failing browser fire that retries in a later minute still dedups\n * against the server event.\n */\nexport function makeEventId(\n eventName: SnapEventName,\n primaryKey: string,\n minuteBucket: number = minuteBucketNow(),\n): string {\n return `${eventName}_${safeKey(primaryKey)}_${minuteBucket}`\n}\n\n/**\n * Derivation helper for the PURCHASE event's 30-day dedup pair. Reuses\n * the Amboras order id as both `custom_data.order_id` (CAPI) and\n * `transaction_id` (Pixel) so Snap can dedup on the 30-day window even\n * if the 48h `event_id` window has expired.\n *\n * Returns `undefined` for non-PURCHASE events (encodes the \"PURCHASE\n * only\" leg of the dual-dedup scheme).\n */\nexport function deriveOrderIdForPurchase(\n eventName: SnapEventName,\n orderId: string | undefined,\n): string | undefined {\n if (eventName !== 'PURCHASE') return undefined\n if (typeof orderId !== 'string') return undefined\n const trimmed = orderId.trim()\n return trimmed ? trimmed : undefined\n}\n","import type { SnapEventName, SnapEventPayload, SnapUserData, SnapCustomData } from '../types'\nimport { safeSnaptr } from '../lib/snap-pixel-loader'\nimport { makeEventId, minuteBucketNow, deriveOrderIdForPurchase } from '../lib/dedup'\n\n/**\n * Client-side dual-fire hooks for Snap. Every event flows two ways from\n * the same user action:\n *\n * 1. Browser pixel: `snaptr('track', <EVENT_NAME>, { ..., client_dedup_id })`\n * via the loaded `scevent.min.js`.\n * 2. Server-side CAPI: POST to the storefront proxy at\n * `/api/snapchat-ads/events` which enriches with\n * `client_ip_address` + `client_user_agent` and forwards to Medusa\n * `/store/snapchat-ads/capi`.\n *\n * Both sides carry the SAME `event_id` / `client_dedup_id` so Snap can\n * dedup within 48h. For PURCHASE only, both sides also share\n * `custom_data.order_id` / `transaction_id` for the 30-day dedup window\n * (research memo Section 5).\n *\n * Snap v3 wire caveats encoded here:\n * - Event names are UPPER_SNAKE_CASE (PAGE_VIEW / VIEW_CONTENT /\n * ADD_CART / START_CHECKOUT / PURCHASE).\n * - `value`, `item_price`, `quantity` are STRINGS in the CAPI payload.\n * The browser pixel is more lenient but we mirror the same string\n * serialization so the two sides stay symmetric and dedup diffs read\n * cleanly in logs.\n * - PII (`em`, `ph`, `madid`, `external_id`) is passed RAW from the\n * browser and hashed on the Medusa side by `sendSnapchatCapiEvent`.\n * The proxy strips raw PII before forwarding to Snap.\n *\n * All fires are non-throwing - a failed CAPI relay is logged, never\n * bubbled, so a merchant's storefront cannot break because Snap's edge\n * had a hiccup.\n */\n\nconst CAPI_PROXY_ENDPOINT = '/api/snapchat-ads/events'\n\nfunction currentUrl(): string {\n return typeof window !== 'undefined' ? window.location.href : ''\n}\n\n/**\n * Send a server-side Snap CAPI event via the storefront's proxy. Fire-\n * and-forget from the caller's perspective; internal errors are swallowed\n * because analytics loss is preferable to storefront breakage.\n *\n * Intentionally NOT gated on analytics consent - Snap CAPI operates on a\n * different legal basis (order-tracking legitimate interest) than the\n * browser pixel. The `SnapPixelProvider` handles browser-side consent\n * gating separately.\n */\nexport async function sendSnapchatCapiEvent(payload: SnapEventPayload): Promise<void> {\n try {\n await fetch(CAPI_PROXY_ENDPOINT, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch {\n // non-fatal\n }\n}\n\n/**\n * Fire PAGE_VIEW on both sides. The `SnapPixelProvider` already fires\n * this on mount + route change; this hook is exposed for callers that\n * want to trigger a page-view manually (e.g. modal opens counted as\n * page views).\n */\nexport function trackPageView(\n primaryKey: string,\n userData: SnapUserData = {},\n eventId?: string,\n): void {\n const eid = eventId ?? makeEventId('PAGE_VIEW', primaryKey, minuteBucketNow())\n safeSnaptr('track', 'PAGE_VIEW', { client_dedup_id: eid })\n void sendSnapchatCapiEvent({\n eventName: 'PAGE_VIEW',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData,\n })\n}\n\nexport interface TrackViewContentOptions {\n currency?: string\n /** Major-unit value STRING (e.g. `\"10.50\"`). */\n value?: string\n /** Product / variant id passed to Snap as `content_ids`. */\n contentIds?: string[]\n /** Optional per-line breakdown. `item_price` + `quantity` are STRINGS. */\n contents?: Array<{ id: string; quantity: string; item_price?: string }>\n numItems?: number\n userData?: SnapUserData\n eventId?: string\n}\n\nexport function trackViewContent(productId: string, options: TrackViewContentOptions = {}): void {\n const eid = options.eventId ?? makeEventId('VIEW_CONTENT', productId, minuteBucketNow())\n const contentIds = options.contentIds ?? [productId]\n const customData: SnapCustomData = {\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(contentIds.length ? { contentIds } : {}),\n ...(options.contents?.length ? { contents: options.contents } : {}),\n ...(typeof options.numItems === 'number' ? { numItems: options.numItems } : {}),\n }\n safeSnaptr('track', 'VIEW_CONTENT', {\n ...(contentIds.length ? { item_ids: contentIds } : {}),\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n client_dedup_id: eid,\n })\n void sendSnapchatCapiEvent({\n eventName: 'VIEW_CONTENT',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData: options.userData ?? {},\n customData,\n })\n}\n\nexport interface TrackAddCartOptions {\n currency?: string\n /** Major-unit value STRING. */\n value?: string\n /** Product / variant ids. */\n contentIds?: string[]\n contents?: Array<{ id: string; quantity: string; item_price?: string }>\n /** Top-level `num_items` - number-typed per Snap docs. */\n numItems?: number\n userData?: SnapUserData\n eventId?: string\n}\n\nexport function trackAddCart(cartId: string, options: TrackAddCartOptions = {}): void {\n const eid = options.eventId ?? makeEventId('ADD_CART', cartId, minuteBucketNow())\n const customData: SnapCustomData = {\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(options.contentIds?.length ? { contentIds: options.contentIds } : {}),\n ...(options.contents?.length ? { contents: options.contents } : {}),\n ...(typeof options.numItems === 'number' ? { numItems: options.numItems } : {}),\n }\n safeSnaptr('track', 'ADD_CART', {\n ...(options.contentIds?.length ? { item_ids: options.contentIds } : {}),\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(typeof options.numItems === 'number' ? { number_items: options.numItems } : {}),\n client_dedup_id: eid,\n })\n void sendSnapchatCapiEvent({\n eventName: 'ADD_CART',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData: options.userData ?? {},\n customData,\n })\n}\n\nexport interface TrackStartCheckoutOptions {\n currency?: string\n value?: string\n contentIds?: string[]\n contents?: Array<{ id: string; quantity: string; item_price?: string }>\n numItems?: number\n userData?: SnapUserData\n eventId?: string\n}\n\nexport function trackStartCheckout(cartId: string, options: TrackStartCheckoutOptions = {}): void {\n const eid = options.eventId ?? makeEventId('START_CHECKOUT', cartId, minuteBucketNow())\n const customData: SnapCustomData = {\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(options.contentIds?.length ? { contentIds: options.contentIds } : {}),\n ...(options.contents?.length ? { contents: options.contents } : {}),\n ...(typeof options.numItems === 'number' ? { numItems: options.numItems } : {}),\n }\n safeSnaptr('track', 'START_CHECKOUT', {\n ...(options.contentIds?.length ? { item_ids: options.contentIds } : {}),\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(typeof options.numItems === 'number' ? { number_items: options.numItems } : {}),\n client_dedup_id: eid,\n })\n void sendSnapchatCapiEvent({\n eventName: 'START_CHECKOUT',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData: options.userData ?? {},\n customData,\n })\n}\n\nexport interface TrackPurchaseOptions {\n currency?: string\n value?: string\n contentIds?: string[]\n contents?: Array<{ id: string; quantity: string; item_price?: string }>\n numItems?: number\n userData?: SnapUserData\n /**\n * When present, pins `event_id` / `client_dedup_id` explicitly; otherwise\n * a stable id is derived from `(PURCHASE, orderId, 0)` - minute-bucket 0\n * so retries within a longer window still dedup against the server event.\n */\n eventId?: string\n}\n\n/**\n * PURCHASE gets the dual-dedup treatment:\n * - Top-level `event_id` / Pixel `client_dedup_id` shared for 48h window.\n * - `custom_data.order_id` / Pixel `transaction_id` shared for 30-day\n * window. Reuses the Amboras order id as both.\n */\nexport function trackPurchase(orderId: string, options: TrackPurchaseOptions = {}): void {\n // Pin minute-bucket to 0 for PURCHASE so a browser retry in a later\n // minute still lands on the same event_id as the initial server fire.\n const eid = options.eventId ?? makeEventId('PURCHASE', orderId, 0)\n const derivedOrderId = deriveOrderIdForPurchase('PURCHASE', orderId)\n const customData: SnapCustomData = {\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(options.contentIds?.length ? { contentIds: options.contentIds } : {}),\n ...(options.contents?.length ? { contents: options.contents } : {}),\n ...(typeof options.numItems === 'number' ? { numItems: options.numItems } : {}),\n ...(derivedOrderId ? { orderId: derivedOrderId } : {}),\n }\n safeSnaptr('track', 'PURCHASE', {\n ...(options.contentIds?.length ? { item_ids: options.contentIds } : {}),\n ...(options.currency ? { currency: options.currency } : {}),\n ...(options.value !== undefined ? { value: options.value } : {}),\n ...(typeof options.numItems === 'number' ? { number_items: options.numItems } : {}),\n ...(derivedOrderId ? { transaction_id: derivedOrderId } : {}),\n client_dedup_id: eid,\n })\n void sendSnapchatCapiEvent({\n eventName: 'PURCHASE',\n eventId: eid,\n eventSourceUrl: currentUrl(),\n userData: options.userData ?? {},\n customData,\n })\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,128 @@
1
+ interface SnapPixelProviderProps {
2
+ /** Snap Pixel ID (`snap_pixel_id`). When absent, this component is a no-op. */
3
+ pixelId?: string;
4
+ }
5
+ /**
6
+ * Null-rendering side-effect component for the `rootProviders` plugin
7
+ * slot. Responsibilities:
8
+ *
9
+ * 1. Consent-gated install of the `snaptr` shim + injection of
10
+ * `https://sc-static.net/scevent.min.js`. Snap does NOT ship an
11
+ * npm-installable browser SDK (research memo Section 10) so we inline
12
+ * the canonical IIFE via `installSnapPixelShim`.
13
+ * 2. Consent-gated `snaptr('init', pixelId)` - runs at most once per
14
+ * pixel id even across SPA nav (guarded by `initedForPixelId`).
15
+ * 3. SPA PAGE_VIEW on every route change. `scevent.min.js` only fires
16
+ * PAGE_VIEW automatically on full page loads; SPA navs need an
17
+ * explicit `snaptr('track', 'PAGE_VIEW')`. Fires with an explicit
18
+ * `client_dedup_id` derived from the pathname so a browser retry in
19
+ * the same minute doesn't get counted twice.
20
+ *
21
+ * Everything is gated on `hasAnalyticsConsent()` and re-checked on the
22
+ * `amboras-consent-changed` event.
23
+ *
24
+ * Deliberately does NOT relay PAGE_VIEW to CAPI - the storefront has no
25
+ * server counterpart for a page view (no route handler dispatches it,
26
+ * unlike PURCHASE / ADD_CART which the checkout / cart flow triggers).
27
+ * Merchants who want server-side PAGE_VIEW attribution can call
28
+ * `trackPageView` from their app; the default here is browser-only for
29
+ * PAGE_VIEW parity with Snap's canonical install snippet.
30
+ */
31
+ declare function SnapPixelProvider({ pixelId }: SnapPixelProviderProps): null;
32
+
33
+ interface SnapViewContentProps {
34
+ productId?: string;
35
+ productName?: string;
36
+ sku?: string;
37
+ /** Major-unit numeric price (already converted from Medusa's minor units by the slot host). */
38
+ price?: number;
39
+ currency?: string;
40
+ }
41
+ /**
42
+ * Null-rendering component for the `pdpAnalytics` plugin slot.
43
+ * Fires Snap `VIEW_CONTENT` on both browser (Pixel) and server (CAPI)
44
+ * on PDP mount.
45
+ *
46
+ * Guarded by a productId ref so PDPs that swap variants without a full
47
+ * remount don't fire once per prop tick - Snap counts double fires as
48
+ * two events even when `client_dedup_id` matches (dedup is only across
49
+ * browser vs server, not within the same channel).
50
+ */
51
+ declare function SnapViewContent({ productId, price, currency }: SnapViewContentProps): null;
52
+
53
+ interface SnapAddCartProps {
54
+ cartId?: string;
55
+ itemCount?: number;
56
+ /** Kept for prop-shape compatibility with the plugin slot - not used directly. */
57
+ total?: number;
58
+ currency?: string;
59
+ }
60
+ /**
61
+ * Null-rendering component for the `cartUpdate` plugin slot.
62
+ * Fires Snap `ADD_CART` per newly-added variant, self-hydrating from
63
+ * Medusa's `/store/carts/:id` so `value`/`currency` are always
64
+ * authoritative. Per-variant sessionStorage baseline persists across
65
+ * cart-drawer mount cycles; a 1.5s window dedup absorbs TanStack Query
66
+ * refetch races.
67
+ *
68
+ * First-mount seed: on a fresh session with a hydrated cart (page
69
+ * refresh loads a cart with N items already in it), the baseline is
70
+ * seeded to the current cart state and NO events fire. Only subsequent
71
+ * quantity increases fire events.
72
+ *
73
+ * Values are serialised to STRINGS per Snap v3 wire spec (research memo
74
+ * caveat #3).
75
+ */
76
+ declare function SnapAddCart({ cartId, itemCount: _itemCount }: SnapAddCartProps): null;
77
+
78
+ interface SnapStartCheckoutProps {
79
+ cartId?: string;
80
+ itemCount?: number;
81
+ total?: number;
82
+ currency?: string;
83
+ }
84
+ /**
85
+ * Null-rendering component for the `checkoutStart` plugin slot.
86
+ * Fires Snap `START_CHECKOUT` exactly once per cartId. SessionStorage
87
+ * dedup so back-nav to the checkout page doesn't double-fire.
88
+ *
89
+ * Self-fetches the cart from Medusa so `value`/`currency` are correct
90
+ * even when the merchant slot context is thin. The ref is set only
91
+ * AFTER a successful dispatch so a StrictMode cleanup between effect-
92
+ * run and fetch-complete does NOT poison the ref.
93
+ */
94
+ declare function SnapStartCheckout({ cartId, itemCount, total, currency }: SnapStartCheckoutProps): null;
95
+
96
+ interface SnapPurchaseProps {
97
+ orderId?: string;
98
+ total?: number;
99
+ currency?: string;
100
+ itemCount?: number;
101
+ }
102
+ /**
103
+ * Null-rendering component for the `checkoutComplete` plugin slot.
104
+ * Fires Snap `PURCHASE` exactly once per orderId. Dual-dedup semantics
105
+ * per research memo Section 5:
106
+ *
107
+ * - Top-level `event_id` / Pixel `client_dedup_id` shared for 48h.
108
+ * - `custom_data.order_id` / Pixel `transaction_id` shared for 30d.
109
+ *
110
+ * Both derive from the orderId directly so browser retries in a later
111
+ * minute still line up with the server event.
112
+ *
113
+ * Primary path: self-fetch from `/store/orders/:id` and derive value +
114
+ * currency + contentIds from the authoritative Medusa response
115
+ * (currency-aware `toMajorUnits`, so JPY/KWD are correct).
116
+ *
117
+ * Fallback path (fetch failed): fire a MINIMAL browser PURCHASE using
118
+ * the incoming `total` + `currency` props, log a dev warning, and rely
119
+ * on Snap's dedup with the server-side `sendSnapchatCapiEvent` to
120
+ * reconcile.
121
+ *
122
+ * Two-layer dedup mirrors meta-ads: `useRef` guards StrictMode double-
123
+ * mount within the same instance; `sessionStorage` guards page refresh
124
+ * + remount. Both are set only AFTER successful dispatch.
125
+ */
126
+ declare function SnapPurchase({ orderId, total, currency, itemCount: _itemCount }: SnapPurchaseProps): null;
127
+
128
+ export { SnapAddCart, type SnapAddCartProps, SnapPixelProvider, type SnapPixelProviderProps, SnapPurchase, type SnapPurchaseProps, SnapStartCheckout, type SnapStartCheckoutProps, SnapViewContent, type SnapViewContentProps };