@amboras-dev/snapchat-ads 0.1.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.
@@ -0,0 +1,468 @@
1
+ "use client";
2
+
3
+ // src/slot-components/SnapPixelProvider.tsx
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { usePathname, useSearchParams } from "next/navigation";
6
+ import { fetchCurrentCustomer, hasAnalyticsConsent, piiPayloadFromCustomer } from "@amboras-dev/pixel-core";
7
+
8
+ // src/constants/index.ts
9
+ var SNAP_EVENTS = {
10
+ PAGE_VIEW: "PAGE_VIEW",
11
+ VIEW_CONTENT: "VIEW_CONTENT",
12
+ ADD_CART: "ADD_CART",
13
+ START_CHECKOUT: "START_CHECKOUT",
14
+ PURCHASE: "PURCHASE"
15
+ };
16
+ var EVENT_TOGGLES = {
17
+ PAGE_VIEW: "track_page_view",
18
+ VIEW_CONTENT: "track_view_content",
19
+ ADD_CART: "track_add_cart",
20
+ START_CHECKOUT: "track_start_checkout",
21
+ PURCHASE: "track_purchase"
22
+ };
23
+ var CONFIG_REFRESH_MS = 6e4;
24
+ var CAPI_RELAY_ENDPOINT = "/api/snapchat/events";
25
+ var SNAP_SCRIPT_SRC = "https://sc-static.net/scevent.min.js";
26
+ var CLICK_ID_QUERY_PARAM = "ScCid";
27
+ var COOKIE_NAMES = {
28
+ /** Plugin-owned raw PII for the relay; Medusa hashes it. */
29
+ PII: "amboras_snap_pii",
30
+ /** Plugin-owned copy of `ScCid`. */
31
+ CLICK_ID: "amboras_snap_clid",
32
+ /** Snap's own first-party cookie (forwarded as `sc_cookie1`). */
33
+ SCID: "_scid"
34
+ };
35
+ var COOKIE_MAX_AGE_SECONDS = {
36
+ PII: 30 * 24 * 60 * 60,
37
+ // Matches Snap's default 28-day click attribution window.
38
+ CLICK_ID: 28 * 24 * 60 * 60
39
+ };
40
+ var STORAGE_KEYS = {
41
+ CART_STATE: "amboras_snap_cart_state_v1",
42
+ CART_RECENT_FIRES: "__amboras_snap_recent_fires__",
43
+ CHECKOUT_SEEN: "amboras_snap_checkout_seen",
44
+ PURCHASE_PREFIX: "amboras_snap_purchase_"
45
+ };
46
+ var WINDOW_EVENTS = {
47
+ CONSENT_CHANGED: "amboras-consent-changed",
48
+ AUTH_CHANGED: "amboras-auth-changed"
49
+ };
50
+
51
+ // src/lib/cookies.ts
52
+ import { deleteCookie, readCookie, writeCookie, writePiiCookie } from "@amboras-dev/pixel-core";
53
+ function readScid() {
54
+ return readCookie(COOKIE_NAMES.SCID);
55
+ }
56
+ function readClickId() {
57
+ return readCookie(COOKIE_NAMES.CLICK_ID);
58
+ }
59
+ function captureClickIdFromQuery() {
60
+ if (typeof window === "undefined" || typeof document === "undefined") return;
61
+ let clickId = null;
62
+ try {
63
+ clickId = new URL(window.location.href).searchParams.get(CLICK_ID_QUERY_PARAM);
64
+ } catch {
65
+ return;
66
+ }
67
+ if (!clickId || clickId === readClickId()) return;
68
+ writeCookie(COOKIE_NAMES.CLICK_ID, clickId, COOKIE_MAX_AGE_SECONDS.CLICK_ID);
69
+ }
70
+ function writeSnapchatPiiCookie(payload) {
71
+ writePiiCookie(COOKIE_NAMES.PII, payload, COOKIE_MAX_AGE_SECONDS.PII);
72
+ }
73
+ function clearSnapchatPiiCookie() {
74
+ deleteCookie(COOKIE_NAMES.PII);
75
+ }
76
+
77
+ // src/lib/snaptr.ts
78
+ function safeSnaptr(...args) {
79
+ if (typeof window === "undefined") return false;
80
+ const fn = window.snaptr;
81
+ if (typeof fn !== "function") return false;
82
+ try {
83
+ fn(...args);
84
+ return true;
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+ function installSnaptrShim() {
90
+ if (typeof window === "undefined" || typeof document === "undefined") return;
91
+ if (window.snaptr) return;
92
+ const shim = function(...args) {
93
+ const self = shim;
94
+ if (self.handleRequest) self.handleRequest(...args);
95
+ else self.queue.push(args);
96
+ };
97
+ shim.queue = [];
98
+ window.snaptr = shim;
99
+ const script = document.createElement("script");
100
+ script.async = true;
101
+ script.src = SNAP_SCRIPT_SRC;
102
+ document.head.appendChild(script);
103
+ }
104
+
105
+ // src/get-config.ts
106
+ import { resolveBackendUrl, resolvePublishableKey, resolveStoreEnvironmentId } from "@amboras-dev/pixel-core";
107
+ async function getSnapchatConfig(options = {}) {
108
+ const baseUrl = options.baseUrl ?? resolveBackendUrl();
109
+ if (!baseUrl) return null;
110
+ const publishableKey = options.publishableApiKey ?? resolvePublishableKey();
111
+ const storeId = options.storeId ?? resolveStoreEnvironmentId();
112
+ const doFetch = options.fetchImpl ?? fetch;
113
+ try {
114
+ const res = await doFetch(`${baseUrl}/store/snapchat-ads/config`, {
115
+ headers: {
116
+ ...storeId ? { "X-Store-Environment-ID": storeId } : {},
117
+ ...publishableKey ? { "x-publishable-api-key": publishableKey } : {}
118
+ },
119
+ next: { revalidate: 60 }
120
+ });
121
+ if (!res.ok) return null;
122
+ return await res.json();
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ // src/lib/runtime-config.ts
129
+ var cached = null;
130
+ function loadSnapchatConfig() {
131
+ const now = Date.now();
132
+ if (cached && now - cached.at < CONFIG_REFRESH_MS) return cached.read;
133
+ const read = getSnapchatConfig().then((config) => {
134
+ if (!config) cached = null;
135
+ return config;
136
+ });
137
+ cached = { at: now, read };
138
+ return read;
139
+ }
140
+ function activePixelId(config) {
141
+ if (!config || config.enabled !== true) return null;
142
+ return config.pixel_id || null;
143
+ }
144
+ function isEventEnabled(config, eventName) {
145
+ if (!activePixelId(config)) return false;
146
+ return config[EVENT_TOGGLES[eventName]] !== false;
147
+ }
148
+
149
+ // src/lib/event-id.ts
150
+ function makeEventId(eventName, primaryKey) {
151
+ const safeKey = primaryKey.replace(/[^a-zA-Z0-9_-]/g, "_");
152
+ return `${eventName.toLowerCase()}_${safeKey}`;
153
+ }
154
+ function randomSuffix() {
155
+ const c = globalThis.crypto;
156
+ if (c?.randomUUID) return c.randomUUID().replace(/-/g, "").slice(0, 16);
157
+ return Math.random().toString(36).slice(2, 12) + Date.now().toString(36);
158
+ }
159
+ function makeUniqueEventId(eventName, primaryKey) {
160
+ return `${makeEventId(eventName, primaryKey)}_${randomSuffix()}`;
161
+ }
162
+
163
+ // src/hooks/useSnapchatEvents.ts
164
+ function currentUrl() {
165
+ return typeof window !== "undefined" ? window.location.href : "";
166
+ }
167
+ function baseUserData() {
168
+ const out = {};
169
+ const clickId = readClickId();
170
+ const cookie1 = readScid();
171
+ if (clickId) out.clickId = clickId;
172
+ if (cookie1) out.cookie1 = cookie1;
173
+ return out;
174
+ }
175
+ function compact(input) {
176
+ const out = {};
177
+ for (const [k, v] of Object.entries(input)) {
178
+ if (v !== void 0) out[k] = v;
179
+ }
180
+ return out;
181
+ }
182
+ async function sendSnapchatCapiEvent(payload) {
183
+ try {
184
+ await fetch(CAPI_RELAY_ENDPOINT, {
185
+ method: "POST",
186
+ headers: { "Content-Type": "application/json" },
187
+ body: JSON.stringify(payload),
188
+ keepalive: true
189
+ });
190
+ } catch {
191
+ }
192
+ }
193
+ function fire(eventName, eventId, pixelParams, customData) {
194
+ const payload = {
195
+ eventName,
196
+ eventId,
197
+ eventSourceUrl: currentUrl(),
198
+ userData: baseUserData(),
199
+ ...customData ? { customData } : {}
200
+ };
201
+ const params = compact({ ...pixelParams, client_dedup_id: eventId });
202
+ void loadSnapchatConfig().then((config) => {
203
+ if (!isEventEnabled(config, eventName)) return;
204
+ safeSnaptr("track", eventName, params);
205
+ void sendSnapchatCapiEvent(payload);
206
+ });
207
+ }
208
+ function trackPageView() {
209
+ fire(SNAP_EVENTS.PAGE_VIEW, makeUniqueEventId(SNAP_EVENTS.PAGE_VIEW, "page"), {});
210
+ }
211
+ function trackViewContent(productId, value, currency) {
212
+ fire(
213
+ SNAP_EVENTS.VIEW_CONTENT,
214
+ makeUniqueEventId(SNAP_EVENTS.VIEW_CONTENT, productId),
215
+ { price: value, currency, item_ids: [productId] },
216
+ { value, currency, contentIds: [productId] }
217
+ );
218
+ }
219
+ function trackAddCart(variantId, quantity, value, currency, contentIds) {
220
+ const ids = contentIds?.length ? contentIds : [variantId];
221
+ fire(
222
+ SNAP_EVENTS.ADD_CART,
223
+ makeUniqueEventId(SNAP_EVENTS.ADD_CART, variantId),
224
+ { price: value, currency, item_ids: ids, number_items: quantity },
225
+ { value, currency, contentIds: ids, numItems: quantity }
226
+ );
227
+ }
228
+ function trackStartCheckout(cartId, itemCount, value, currency, contentIds) {
229
+ fire(
230
+ SNAP_EVENTS.START_CHECKOUT,
231
+ makeEventId(SNAP_EVENTS.START_CHECKOUT, cartId),
232
+ { price: value, currency, item_ids: contentIds?.length ? contentIds : void 0, number_items: itemCount },
233
+ { value, currency, contentIds, numItems: itemCount }
234
+ );
235
+ }
236
+ function trackPurchase(orderId, value, currency, itemCount, contentIds) {
237
+ fire(
238
+ SNAP_EVENTS.PURCHASE,
239
+ makeEventId(SNAP_EVENTS.PURCHASE, orderId),
240
+ {
241
+ price: value,
242
+ currency,
243
+ transaction_id: orderId,
244
+ item_ids: contentIds?.length ? contentIds : void 0,
245
+ number_items: itemCount
246
+ },
247
+ { value, currency, orderId, contentIds, numItems: itemCount }
248
+ );
249
+ }
250
+
251
+ // src/slot-components/SnapPixelProvider.tsx
252
+ function SnapPixelProvider(_props) {
253
+ const [consented, setConsented] = useState(false);
254
+ const loadedRef = useRef(false);
255
+ const pathname = usePathname();
256
+ const searchParams = useSearchParams();
257
+ useEffect(() => {
258
+ setConsented(hasAnalyticsConsent());
259
+ const handler = () => setConsented(hasAnalyticsConsent());
260
+ window.addEventListener(WINDOW_EVENTS.CONSENT_CHANGED, handler);
261
+ return () => window.removeEventListener(WINDOW_EVENTS.CONSENT_CHANGED, handler);
262
+ }, []);
263
+ useEffect(() => {
264
+ if (!consented) return;
265
+ let cancelled = false;
266
+ let ticket = 0;
267
+ const syncPiiCookie = async () => {
268
+ const localTicket = ++ticket;
269
+ const customer = await fetchCurrentCustomer();
270
+ if (cancelled || localTicket !== ticket) return;
271
+ const pii = piiPayloadFromCustomer(customer);
272
+ if (pii) writeSnapchatPiiCookie(pii);
273
+ else clearSnapchatPiiCookie();
274
+ };
275
+ void syncPiiCookie();
276
+ const handler = () => {
277
+ void syncPiiCookie();
278
+ };
279
+ window.addEventListener(WINDOW_EVENTS.AUTH_CHANGED, handler);
280
+ return () => {
281
+ cancelled = true;
282
+ window.removeEventListener(WINDOW_EVENTS.AUTH_CHANGED, handler);
283
+ };
284
+ }, [consented]);
285
+ useEffect(() => {
286
+ if (!consented) return;
287
+ captureClickIdFromQuery();
288
+ let cancelled = false;
289
+ void loadSnapchatConfig().then((config) => {
290
+ if (cancelled) return;
291
+ const id = activePixelId(config);
292
+ if (!id) return;
293
+ if (!loadedRef.current) {
294
+ installSnaptrShim();
295
+ safeSnaptr("init", id, {});
296
+ loadedRef.current = true;
297
+ }
298
+ trackPageView();
299
+ });
300
+ return () => {
301
+ cancelled = true;
302
+ };
303
+ }, [pathname, searchParams, consented]);
304
+ return null;
305
+ }
306
+
307
+ // src/slot-components/SnapViewContent.tsx
308
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
309
+ import { toMajorUnits } from "@amboras-dev/pixel-core";
310
+ function SnapViewContent({ productId, price, currency }) {
311
+ const lastProductId = useRef2(null);
312
+ useEffect2(() => {
313
+ if (!productId || lastProductId.current === productId) return;
314
+ lastProductId.current = productId;
315
+ trackViewContent(productId, toMajorUnits(price, currency), currency);
316
+ }, [productId, price, currency]);
317
+ return null;
318
+ }
319
+
320
+ // src/slot-components/SnapAddCart.tsx
321
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
322
+ import { createCartAddTracker } from "@amboras-dev/pixel-core";
323
+ function SnapAddCart({ cartId, itemCount }) {
324
+ const tracker = useRef3(createCartAddTracker({ namespace: STORAGE_KEYS.CART_STATE }));
325
+ useEffect3(() => {
326
+ if (!cartId) return;
327
+ let cancelled = false;
328
+ void (async () => {
329
+ const adds = await tracker.current.check(cartId, () => cancelled);
330
+ if (!adds) return;
331
+ for (const add of adds) {
332
+ const contentIds = add.productId ? [add.productId, add.variantId] : [add.variantId];
333
+ trackAddCart(add.variantId, add.quantity, add.value, add.currency, contentIds);
334
+ }
335
+ })();
336
+ return () => {
337
+ cancelled = true;
338
+ };
339
+ }, [cartId, itemCount]);
340
+ return null;
341
+ }
342
+
343
+ // src/slot-components/SnapStartCheckout.tsx
344
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
345
+ import { fetchCart, toMajorUnits as toMajorUnits2 } from "@amboras-dev/pixel-core";
346
+ var STORAGE_KEY = STORAGE_KEYS.CHECKOUT_SEEN;
347
+ function readSeen() {
348
+ if (typeof window === "undefined") return /* @__PURE__ */ new Set();
349
+ try {
350
+ const raw = window.sessionStorage.getItem(STORAGE_KEY);
351
+ if (!raw) return /* @__PURE__ */ new Set();
352
+ const parsed = JSON.parse(raw);
353
+ return Array.isArray(parsed) ? new Set(parsed.filter((v) => typeof v === "string")) : /* @__PURE__ */ new Set();
354
+ } catch {
355
+ return /* @__PURE__ */ new Set();
356
+ }
357
+ }
358
+ function writeSeen(set) {
359
+ if (typeof window === "undefined") return;
360
+ try {
361
+ window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(set).slice(-50)));
362
+ } catch {
363
+ }
364
+ }
365
+ function SnapStartCheckout({
366
+ cartId,
367
+ itemCount,
368
+ total,
369
+ currency
370
+ }) {
371
+ const fired = useRef4(null);
372
+ useEffect4(() => {
373
+ if (!cartId) return;
374
+ if (fired.current === cartId) return;
375
+ const seen = readSeen();
376
+ if (seen.has(cartId)) {
377
+ fired.current = cartId;
378
+ return;
379
+ }
380
+ let cancelled = false;
381
+ void (async () => {
382
+ const cart = await fetchCart(cartId);
383
+ if (cancelled) return;
384
+ let resolvedItems = [];
385
+ let resolvedTotal = total;
386
+ let resolvedCurrency = currency;
387
+ let resolvedNumItems = itemCount ?? 0;
388
+ if (cart) {
389
+ if (Array.isArray(cart.items)) resolvedItems = cart.items;
390
+ if (typeof cart.total === "number") resolvedTotal = cart.total;
391
+ if (typeof cart.currency_code === "string") resolvedCurrency = cart.currency_code;
392
+ resolvedNumItems = resolvedItems.reduce((sum, it) => sum + (it.quantity ?? 0), 0) || resolvedNumItems;
393
+ }
394
+ const contentIds = resolvedItems.map((it) => it.product_id).filter((id) => typeof id === "string" && id.length > 0);
395
+ const majorValue = toMajorUnits2(resolvedTotal, resolvedCurrency);
396
+ if (resolvedNumItems <= 0 && contentIds.length === 0) return;
397
+ trackStartCheckout(cartId, resolvedNumItems, majorValue, resolvedCurrency, contentIds);
398
+ fired.current = cartId;
399
+ seen.add(cartId);
400
+ writeSeen(seen);
401
+ })();
402
+ return () => {
403
+ cancelled = true;
404
+ };
405
+ }, [cartId, itemCount, total, currency]);
406
+ return null;
407
+ }
408
+
409
+ // src/slot-components/SnapPurchase.tsx
410
+ import { useEffect as useEffect5, useRef as useRef5 } from "react";
411
+ import { fetchOrder, toMajorUnits as toMajorUnits3 } from "@amboras-dev/pixel-core";
412
+ function SnapPurchase({
413
+ orderId,
414
+ total,
415
+ currency,
416
+ itemCount
417
+ }) {
418
+ const fired = useRef5(null);
419
+ useEffect5(() => {
420
+ if (!orderId) return;
421
+ if (fired.current === orderId) return;
422
+ const dedupeKey = `${STORAGE_KEYS.PURCHASE_PREFIX}${orderId}`;
423
+ try {
424
+ if (typeof sessionStorage !== "undefined" && sessionStorage.getItem(dedupeKey)) {
425
+ fired.current = orderId;
426
+ return;
427
+ }
428
+ } catch {
429
+ }
430
+ let cancelled = false;
431
+ void (async () => {
432
+ const order = await fetchOrder(orderId);
433
+ if (cancelled) return;
434
+ let resolvedItems = [];
435
+ let resolvedTotal = total;
436
+ let resolvedCurrency = currency;
437
+ let resolvedNumItems = itemCount;
438
+ if (order) {
439
+ if (Array.isArray(order.items)) resolvedItems = order.items;
440
+ if (typeof order.total === "number") resolvedTotal = order.total;
441
+ if (typeof order.currency_code === "string") resolvedCurrency = order.currency_code;
442
+ const summed = resolvedItems.reduce((sum, it) => sum + (it.quantity ?? 0), 0);
443
+ if (summed > 0) resolvedNumItems = summed;
444
+ }
445
+ const majorValue = toMajorUnits3(resolvedTotal, resolvedCurrency);
446
+ if (majorValue === void 0 || !resolvedCurrency) return;
447
+ const contentIds = resolvedItems.map((it) => it.product_id).filter((id) => typeof id === "string" && id.length > 0);
448
+ trackPurchase(orderId, majorValue, resolvedCurrency, resolvedNumItems, contentIds);
449
+ fired.current = orderId;
450
+ try {
451
+ if (typeof sessionStorage !== "undefined") sessionStorage.setItem(dedupeKey, "1");
452
+ } catch {
453
+ }
454
+ })();
455
+ return () => {
456
+ cancelled = true;
457
+ };
458
+ }, [orderId, total, currency, itemCount]);
459
+ return null;
460
+ }
461
+ export {
462
+ SnapAddCart,
463
+ SnapPixelProvider,
464
+ SnapPurchase,
465
+ SnapStartCheckout,
466
+ SnapViewContent
467
+ };
468
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/slot-components/SnapPixelProvider.tsx","../../src/constants/index.ts","../../src/lib/cookies.ts","../../src/lib/snaptr.ts","../../src/get-config.ts","../../src/lib/runtime-config.ts","../../src/lib/event-id.ts","../../src/hooks/useSnapchatEvents.ts","../../src/slot-components/SnapViewContent.tsx","../../src/slot-components/SnapAddCart.tsx","../../src/slot-components/SnapStartCheckout.tsx","../../src/slot-components/SnapPurchase.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef, useState } from 'react'\nimport { usePathname, useSearchParams } from 'next/navigation'\nimport { fetchCurrentCustomer, hasAnalyticsConsent, piiPayloadFromCustomer } from '@amboras-dev/pixel-core'\nimport { WINDOW_EVENTS } from '../constants'\nimport {\n captureClickIdFromQuery,\n writeSnapchatPiiCookie,\n clearSnapchatPiiCookie,\n} from '../lib/cookies'\nimport { installSnaptrShim, safeSnaptr } from '../lib/snaptr'\nimport { activePixelId, loadSnapchatConfig } from '../lib/runtime-config'\nimport { trackPageView } from '../hooks/useSnapchatEvents'\n\nexport interface SnapPixelProviderProps {\n /**\n * Bound by the manifest's `propsFromConfig` (migration 475) and kept for\n * that contract. The pixel actually loaded is the live config's\n * `pixel_id`, so a Pixel ID change in the admin applies without a\n * storefront redeploy.\n */\n pixelId?: string\n}\n\n/**\n * Root providers slot component for Snapchat. Three effects, one job each:\n * 1. Consent: read `amboras_consent` and follow `amboras-consent-changed`\n * so a banner can enable the pixel mid-session. Nothing below runs\n * without consent.\n * 2. Identity: fetch the logged-in customer on mount and on\n * `amboras-auth-changed` (not per route - identity almost never changes\n * mid-session) and write or clear the `amboras_snap_pii` cookie the CAPI\n * relay reads.\n * 3. Navigation: on every route change capture `?ScCid=`, read the\n * merchant's live config, install the pixel the first time\n * (`snaptr('init', pixelId)`), then fire the single `PAGE_VIEW`\n * (browser + CAPI, shared event id). The click id is captured first so\n * the landing page's PAGE_VIEW already carries it. With the plugin\n * switched off (or no config) the pixel is never loaded.\n */\nexport function SnapPixelProvider(_props: SnapPixelProviderProps): null {\n const [consented, setConsented] = useState(false)\n const loadedRef = useRef(false)\n const pathname = usePathname()\n const searchParams = useSearchParams()\n\n // 1. Consent.\n useEffect(() => {\n setConsented(hasAnalyticsConsent())\n const handler = (): void => setConsented(hasAnalyticsConsent())\n window.addEventListener(WINDOW_EVENTS.CONSENT_CHANGED, handler)\n return () => window.removeEventListener(WINDOW_EVENTS.CONSENT_CHANGED, handler)\n }, [])\n\n // 2. Identity -> PII cookie. The cookie is written straight from the fetch\n // result, so no customer state (and no extra render) is kept. `ticket`\n // drops a slow response that an auth change has already superseded.\n useEffect(() => {\n if (!consented) return\n let cancelled = false\n let ticket = 0\n\n const syncPiiCookie = async (): Promise<void> => {\n const localTicket = ++ticket\n const customer = await fetchCurrentCustomer()\n if (cancelled || localTicket !== ticket) return\n const pii = piiPayloadFromCustomer(customer)\n if (pii) writeSnapchatPiiCookie(pii)\n else clearSnapchatPiiCookie()\n }\n\n void syncPiiCookie()\n const handler = (): void => {\n void syncPiiCookie()\n }\n window.addEventListener(WINDOW_EVENTS.AUTH_CHANGED, handler)\n return () => {\n cancelled = true\n window.removeEventListener(WINDOW_EVENTS.AUTH_CHANGED, handler)\n }\n }, [consented])\n\n // 3. Navigation. The single source of PAGE_VIEW: Snap's bare\n // `snaptr('track', 'PAGE_VIEW')` is never called, because it would fire a\n // browser event without `client_dedup_id` that never dedups against CAPI.\n // `loadedRef` makes the install run once per session even if consent flips.\n useEffect(() => {\n if (!consented) return\n captureClickIdFromQuery()\n let cancelled = false\n void loadSnapchatConfig().then((config) => {\n if (cancelled) return\n const id = activePixelId(config)\n if (!id) return\n if (!loadedRef.current) {\n installSnaptrShim()\n safeSnaptr('init', id, {})\n loadedRef.current = true\n }\n trackPageView()\n })\n return () => {\n cancelled = true\n }\n }, [pathname, searchParams, consented])\n\n return null\n}\n","/**\n * Every fixed string the package puts on the wire or in browser storage.\n * Wire names are a cross-repo contract: Medusa's strict Zod schema and the\n * orchestrator's storefront relay template expect exactly these values.\n */\n\n/**\n * Snap standard event names. The pixel (`snaptr('track', <name>)`) and\n * Conversions API v3 (`event_name`) use the SAME uppercase strings.\n * https://developers.snap.com/api/marketing-api/Conversions-API/Parameters\n */\nexport const SNAP_EVENTS = {\n PAGE_VIEW: 'PAGE_VIEW',\n VIEW_CONTENT: 'VIEW_CONTENT',\n ADD_CART: 'ADD_CART',\n START_CHECKOUT: 'START_CHECKOUT',\n PURCHASE: 'PURCHASE',\n} as const\n\n/**\n * Merchant setting (from `/store/snapchat-ads/config`) that switches each\n * event off. Medusa enforces the same map server-side before enqueueing.\n */\nexport const EVENT_TOGGLES = {\n PAGE_VIEW: 'track_page_view',\n VIEW_CONTENT: 'track_view_content',\n ADD_CART: 'track_add_cart',\n START_CHECKOUT: 'track_start_checkout',\n PURCHASE: 'track_purchase',\n} as const satisfies Record<keyof typeof SNAP_EVENTS, string>\n\n/** How long the browser reuses the merchant config before re-reading it. */\nexport const CONFIG_REFRESH_MS = 60_000\n\n/** Storefront relay route written by the orchestrator's template. */\nexport const CAPI_RELAY_ENDPOINT = '/api/snapchat/events'\n\n/** Snap's official pixel script. */\nexport const SNAP_SCRIPT_SRC = 'https://sc-static.net/scevent.min.js'\n\n/** URL param Snap appends to ad landing URLs (forwarded as `sc_click_id`). */\nexport const CLICK_ID_QUERY_PARAM = 'ScCid'\n\nexport const COOKIE_NAMES = {\n /** Plugin-owned raw PII for the relay; Medusa hashes it. */\n PII: 'amboras_snap_pii',\n /** Plugin-owned copy of `ScCid`. */\n CLICK_ID: 'amboras_snap_clid',\n /** Snap's own first-party cookie (forwarded as `sc_cookie1`). */\n SCID: '_scid',\n} as const\n\nexport const COOKIE_MAX_AGE_SECONDS = {\n PII: 30 * 24 * 60 * 60,\n // Matches Snap's default 28-day click attribution window.\n CLICK_ID: 28 * 24 * 60 * 60,\n} as const\n\n/** sessionStorage / globalThis keys the slot components use for dedup. */\nexport const STORAGE_KEYS = {\n CART_STATE: 'amboras_snap_cart_state_v1',\n CART_RECENT_FIRES: '__amboras_snap_recent_fires__',\n CHECKOUT_SEEN: 'amboras_snap_checkout_seen',\n PURCHASE_PREFIX: 'amboras_snap_purchase_',\n} as const\n\n/** Storefront window events the provider listens to. */\nexport const WINDOW_EVENTS = {\n CONSENT_CHANGED: 'amboras-consent-changed',\n AUTH_CHANGED: 'amboras-auth-changed',\n} as const\n","/**\n * Snap-specific cookies, on top of `@amboras-dev/pixel-core`'s cookie helpers:\n * - `_scid`: Snap's first-party cookie set by the pixel. Forwarded raw to\n * CAPI as `sc_cookie1`.\n * - `amboras_snap_clid`: plugin-owned copy of the `ScCid` URL param Snap\n * appends to ad landing URLs. Forwarded raw to CAPI as `sc_click_id`.\n * Captured on landing because a later SPA navigation drops the param.\n * - `amboras_snap_pii`: plugin-owned raw (unhashed) customer PII for the\n * storefront's `/api/snapchat/events` relay; Medusa normalizes + hashes\n * it before shipping to Snap. Separate from the meta/pinterest PII\n * cookies so the plugins stay decoupled.\n */\n\nimport { deleteCookie, readCookie, writeCookie, writePiiCookie, type PiiPayload } from '@amboras-dev/pixel-core'\nimport { CLICK_ID_QUERY_PARAM, COOKIE_MAX_AGE_SECONDS, COOKIE_NAMES } from '../constants'\n\nexport function readScid(): string | undefined {\n return readCookie(COOKIE_NAMES.SCID)\n}\n\nexport function readClickId(): string | undefined {\n return readCookie(COOKIE_NAMES.CLICK_ID)\n}\n\n/**\n * Store `?ScCid=` from the landing URL. A newer click id replaces an older\n * one so the most recent ad click wins attribution.\n */\nexport function captureClickIdFromQuery(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n let clickId: string | null = null\n try {\n clickId = new URL(window.location.href).searchParams.get(CLICK_ID_QUERY_PARAM)\n } catch {\n return\n }\n if (!clickId || clickId === readClickId()) return\n writeCookie(COOKIE_NAMES.CLICK_ID, clickId, COOKIE_MAX_AGE_SECONDS.CLICK_ID)\n}\n\nexport function writeSnapchatPiiCookie(payload: PiiPayload): void {\n writePiiCookie(COOKIE_NAMES.PII, payload, COOKIE_MAX_AGE_SECONDS.PII)\n}\n\nexport function clearSnapchatPiiCookie(): void {\n deleteCookie(COOKIE_NAMES.PII)\n}\n","/**\n * Typed wrapper around Snap's global `snaptr`. The shim is installed by\n * `SnapPixelProvider` behind consent - every other slot component calls\n * `safeSnaptr()`, which is a no-op when snaptr is absent (consent denied,\n * plugin uninstalled, or SSR).\n */\n\nimport { SNAP_SCRIPT_SRC } from '../constants'\n\ndeclare global {\n interface Window {\n snaptr?: SnaptrFunction\n }\n}\n\nexport type SnaptrFunction = (...args: unknown[]) => void\n\n/** Best-effort synchronous call. Returns whether snaptr was present. */\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 * Install the snaptr queue shim + inject `scevent.min.js`. Idempotent.\n * Mirrors the shape of Snap's official install snippet\n * (https://developers.snap.com/api/marketing-api/Ads-API/snap-pixel) so the\n * loaded script drains the queue via `handleRequest`.\n */\nexport function installSnaptrShim(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n if (window.snaptr) return\n\n const shim = function (...args: unknown[]): void {\n const self = shim as unknown as { handleRequest?: (...a: unknown[]) => void; queue: unknown[] }\n if (self.handleRequest) self.handleRequest(...args)\n else self.queue.push(args)\n } as SnaptrFunction\n ;(shim as unknown as { queue: unknown[] }).queue = []\n window.snaptr = shim\n\n const script = document.createElement('script')\n script.async = true\n script.src = SNAP_SCRIPT_SRC\n document.head.appendChild(script)\n}\n","import { resolveBackendUrl, resolvePublishableKey, resolveStoreEnvironmentId } from '@amboras-dev/pixel-core'\nimport type { SnapchatPublicConfig } from './types'\n\ninterface GetSnapchatConfigOptions {\n baseUrl?: string\n publishableApiKey?: string\n storeId?: string\n /** Injected `fetch` for testing. Defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Fetches the non-secret Snapchat config from Medusa. Returns `null` when\n * the backend URL is unknown or the endpoint is missing, unauthorized, or\n * errors. The Conversions API token is never returned by the server route,\n * so it never surfaces here.\n */\nexport async function getSnapchatConfig(\n options: GetSnapchatConfigOptions = {},\n): Promise<SnapchatPublicConfig | null> {\n const baseUrl = options.baseUrl ?? resolveBackendUrl()\n if (!baseUrl) return null\n const publishableKey = options.publishableApiKey ?? resolvePublishableKey()\n const storeId = options.storeId ?? resolveStoreEnvironmentId()\n const doFetch = options.fetchImpl ?? fetch\n\n try {\n const res = await doFetch(`${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 SnapchatPublicConfig\n } catch {\n return null\n }\n}\n","/**\n * The merchant's live Snapchat settings, read once per page (refreshed\n * every CONFIG_REFRESH_MS) and shared by the provider and every event.\n *\n * Fail-closed: no config (network error, plugin disconnected, Medusa answers\n * `{ enabled: false }`) means no pixel and no events. The browser then\n * honours \"Plugin enabled\" and each \"Track ...\" toggle the same way Medusa's\n * events route does, and a Pixel ID change applies without a redeploy.\n */\n\nimport { getSnapchatConfig } from '../get-config'\nimport { CONFIG_REFRESH_MS, EVENT_TOGGLES } from '../constants'\nimport type { SnapchatEventName, SnapchatPublicConfig } from '../types'\n\nlet cached: { at: number; read: Promise<SnapchatPublicConfig | null> } | null = null\n\nexport function loadSnapchatConfig(): Promise<SnapchatPublicConfig | null> {\n const now = Date.now()\n if (cached && now - cached.at < CONFIG_REFRESH_MS) return cached.read\n const read = getSnapchatConfig().then((config) => {\n // A failed read is not reused, so the next event tries again.\n if (!config) cached = null\n return config\n })\n cached = { at: now, read }\n return read\n}\n\n/** The pixel ID to load, or null when the plugin is off / not configured. */\nexport function activePixelId(config: SnapchatPublicConfig | null): string | null {\n if (!config || config.enabled !== true) return null\n return config.pixel_id || null\n}\n\nexport function isEventEnabled(config: SnapchatPublicConfig | null, eventName: SnapchatEventName): boolean {\n if (!activePixelId(config)) return false\n // A toggle the config omits counts as on, matching Medusa's default.\n return config![EVENT_TOGGLES[eventName]] !== false\n}\n\n/** Test-only: forget the cached config. */\nexport function resetSnapchatConfigCache(): void {\n cached = null\n}\n","import type { SnapchatEventName } from '../types'\n\n/**\n * Event-ID builders for browser <-> CAPI dedup. Snap matches the pixel's\n * `client_dedup_id` against the CAPI `event_id` within 48h, so the browser\n * call and the CAPI relay for one user action must carry the same ID. Both\n * are fired from the same function call, so the ID only has to be stable\n * within that call - not across calls.\n *\n * Two flavours:\n * - `makeEventId`: deterministic. Used where a repeat fire of the SAME\n * action must collapse into one (START_CHECKOUT per cart, PURCHASE per\n * order), e.g. a refresh on the thank-you page.\n * - `makeUniqueEventId`: deterministic prefix + random suffix. Used for\n * actions a shopper legitimately repeats (viewing a product again,\n * adding the same variant twice, revisiting a page). A pure\n * deterministic ID would make Snap drop every repeat within 48h.\n */\nexport function makeEventId(eventName: SnapchatEventName, primaryKey: string): string {\n const safeKey = primaryKey.replace(/[^a-zA-Z0-9_-]/g, '_')\n return `${eventName.toLowerCase()}_${safeKey}`\n}\n\nfunction randomSuffix(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto\n if (c?.randomUUID) return c.randomUUID().replace(/-/g, '').slice(0, 16)\n return Math.random().toString(36).slice(2, 12) + Date.now().toString(36)\n}\n\nexport function makeUniqueEventId(eventName: SnapchatEventName, primaryKey: string): string {\n return `${makeEventId(eventName, primaryKey)}_${randomSuffix()}`\n}\n","import { safeSnaptr } from '../lib/snaptr'\nimport { makeEventId, makeUniqueEventId } from '../lib/event-id'\nimport { readClickId, readScid } from '../lib/cookies'\nimport { CAPI_RELAY_ENDPOINT, SNAP_EVENTS } from '../constants'\nimport { isEventEnabled, loadSnapchatConfig } from '../lib/runtime-config'\nimport type { SnapchatCapiEventPayload, SnapchatEventName } from '../types'\n\nfunction currentUrl(): string {\n return typeof window !== 'undefined' ? window.location.href : ''\n}\n\n/**\n * Match keys shared by every CAPI event: the `ScCid` click id captured on\n * ad landing and Snap's `_scid` cookie. Both are sent RAW - Snap matches\n * them against its own tables, they must never be hashed.\n */\nfunction baseUserData(): SnapchatCapiEventPayload['userData'] {\n const out: SnapchatCapiEventPayload['userData'] = {}\n const clickId = readClickId()\n const cookie1 = readScid()\n if (clickId) out.clickId = clickId\n if (cookie1) out.cookie1 = cookie1\n return out\n}\n\n/** Drop undefined keys so the pixel never receives `price: undefined`. */\nfunction compact(input: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(input)) {\n if (v !== undefined) out[k] = v\n }\n return out\n}\n\n/**\n * POST the event to the storefront's `/api/snapchat/events` proxy. The\n * proxy enriches with the PII cookie + IP + User-Agent and forwards to\n * Medusa's `/store/snapchat-ads/events` route, which queues it on the shared\n * outbox. Intentionally UNGATED on analytics consent - CAPI operates on a\n * different legal basis than the browser pixel. Merchant owns that call.\n */\nexport async function sendSnapchatCapiEvent(payload: SnapchatCapiEventPayload): Promise<void> {\n try {\n await fetch(CAPI_RELAY_ENDPOINT, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true,\n })\n } catch {\n // non-fatal - CAPI is best-effort from the browser's perspective\n }\n}\n\n/**\n * Fire one event on both channels with a shared ID. The pixel gets\n * `client_dedup_id` (Snap's browser-side dedup key); the relay gets the\n * same value as `eventId`, which Medusa sends as CAPI `event_id`.\n *\n * Gated on the merchant's live settings first: with the plugin off or this\n * event's \"Track ...\" toggle off, nothing leaves the browser on either\n * channel. URL and match keys are read before the (cached) config lookup,\n * so an SPA navigation during it cannot attribute the event to the next page.\n */\nfunction fire(\n eventName: SnapchatEventName,\n eventId: string,\n pixelParams: Record<string, unknown>,\n customData?: SnapchatCapiEventPayload['customData'],\n): void {\n const payload: SnapchatCapiEventPayload = {\n eventName,\n eventId,\n eventSourceUrl: currentUrl(),\n userData: baseUserData(),\n ...(customData ? { customData } : {}),\n }\n const params = compact({ ...pixelParams, client_dedup_id: eventId })\n void loadSnapchatConfig().then((config) => {\n if (!isEventEnabled(config, eventName)) return\n safeSnaptr('track', eventName, params)\n void sendSnapchatCapiEvent(payload)\n })\n}\n\nexport function trackPageView(): void {\n fire(SNAP_EVENTS.PAGE_VIEW, makeUniqueEventId(SNAP_EVENTS.PAGE_VIEW, 'page'), {})\n}\n\nexport function trackViewContent(\n productId: string,\n value?: number,\n currency?: string,\n): void {\n fire(\n SNAP_EVENTS.VIEW_CONTENT,\n makeUniqueEventId(SNAP_EVENTS.VIEW_CONTENT, productId),\n { price: value, currency, item_ids: [productId] },\n { value, currency, contentIds: [productId] },\n )\n}\n\n/**\n * Fire `ADD_CART` for a single variant's quantity delta. `contentIds` is\n * typically `[productId, variantId]`.\n */\nexport function trackAddCart(\n variantId: string,\n quantity: number,\n value?: number,\n currency?: string,\n contentIds?: string[],\n): void {\n const ids = contentIds?.length ? contentIds : [variantId]\n fire(\n SNAP_EVENTS.ADD_CART,\n makeUniqueEventId(SNAP_EVENTS.ADD_CART, variantId),\n { price: value, currency, item_ids: ids, number_items: quantity },\n { value, currency, contentIds: ids, numItems: quantity },\n )\n}\n\n/** Deterministic per cart: a checkout refresh collapses into one event. */\nexport function trackStartCheckout(\n cartId: string,\n itemCount: number,\n value?: number,\n currency?: string,\n contentIds?: string[],\n): void {\n fire(\n SNAP_EVENTS.START_CHECKOUT,\n makeEventId(SNAP_EVENTS.START_CHECKOUT, cartId),\n { price: value, currency, item_ids: contentIds?.length ? contentIds : undefined, number_items: itemCount },\n { value, currency, contentIds, numItems: itemCount },\n )\n}\n\n/**\n * Order-complete tracker. Both channels use the SAME event name\n * (`PURCHASE`) - unlike Pinterest there is no browser/server name split.\n * Dedup is doubly anchored: `client_dedup_id` == `event_id` (48h) and pixel\n * `transaction_id` == CAPI `order_id` (30d).\n */\nexport function trackPurchase(\n orderId: string,\n value: number,\n currency: string,\n itemCount?: number,\n contentIds?: string[],\n): void {\n fire(\n SNAP_EVENTS.PURCHASE,\n makeEventId(SNAP_EVENTS.PURCHASE, orderId),\n {\n price: value,\n currency,\n transaction_id: orderId,\n item_ids: contentIds?.length ? contentIds : undefined,\n number_items: itemCount,\n },\n { value, currency, orderId, contentIds, numItems: itemCount },\n )\n}\n","'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { trackViewContent } from '../hooks/useSnapchatEvents'\nimport { toMajorUnits } from '@amboras-dev/pixel-core'\n\nexport interface SnapViewContentProps {\n productId?: string\n productName?: string\n sku?: string\n /**\n * Product price in Medusa's storage unit (×100 for every currency, see\n * `toMajorUnits` in @amboras-dev/pixel-core). Converted to major units\n * before it reaches Snap.\n */\n price?: number\n currency?: string\n}\n\n/**\n * Null-rendering component for the `pdpAnalytics` plugin slot. Fires Snap\n * `VIEW_CONTENT` (pixel + CAPI relay) on PDP mount and once per productId\n * change during SPA navigation.\n */\nexport function SnapViewContent({ productId, price, currency }: SnapViewContentProps): null {\n const lastProductId = useRef<string | null>(null)\n\n useEffect(() => {\n if (!productId || lastProductId.current === productId) return\n lastProductId.current = productId\n trackViewContent(productId, toMajorUnits(price, currency), currency)\n }, [productId, price, currency])\n\n return null\n}\n","'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { createCartAddTracker } from '@amboras-dev/pixel-core'\nimport { trackAddCart } from '../hooks/useSnapchatEvents'\nimport { STORAGE_KEYS } from '../constants'\n\nexport interface SnapAddCartProps {\n cartId?: string\n itemCount?: number\n total?: number\n currency?: string\n}\n\n/**\n * Null-rendering component for the `cartUpdate` plugin slot. Fires Snap\n * `ADD_CART` once per newly added variant, with the positive quantity delta\n * and its value in major units.\n *\n * The diff (first-hydration seed, per-cart sessionStorage snapshot, removals\n * ignored, per-variant dedup window, failed-read handling) lives in\n * `@amboras-dev/pixel-core`'s cart-add tracker; this component only maps an\n * add to the Snap event. The cart is self-fetched there, so the event never\n * depends on merchant-passed `total` / `currency`.\n */\nexport function SnapAddCart({ cartId, itemCount }: SnapAddCartProps): null {\n const tracker = useRef(createCartAddTracker({ namespace: STORAGE_KEYS.CART_STATE }))\n\n useEffect(() => {\n if (!cartId) return\n let cancelled = false\n\n void (async () => {\n const adds = await tracker.current.check(cartId, () => cancelled)\n if (!adds) return\n for (const add of adds) {\n const contentIds = add.productId ? [add.productId, add.variantId] : [add.variantId]\n trackAddCart(add.variantId, add.quantity, add.value, add.currency, contentIds)\n }\n })()\n\n return () => {\n cancelled = true\n }\n // `total` and `currency` are intentionally NOT deps - the tracker\n // self-fetches the cart, so a recompute-only signal (promo apply, tax\n // change) would cost a cart read that never produces an event.\n }, [cartId, itemCount])\n\n return null\n}\n","'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { trackStartCheckout } from '../hooks/useSnapchatEvents'\nimport { fetchCart, toMajorUnits, type LineItem } from '@amboras-dev/pixel-core'\nimport { STORAGE_KEYS } from '../constants'\n\nexport interface SnapStartCheckoutProps {\n cartId?: string\n itemCount?: number\n total?: number\n currency?: string\n}\n\nconst STORAGE_KEY = STORAGE_KEYS.CHECKOUT_SEEN\n\nfunction readSeen(): Set<string> {\n if (typeof window === 'undefined') return new Set()\n try {\n const raw = window.sessionStorage.getItem(STORAGE_KEY)\n if (!raw) return new Set()\n const parsed = JSON.parse(raw)\n return Array.isArray(parsed) ? new Set(parsed.filter((v) => typeof v === 'string')) : new Set()\n } catch {\n return new Set()\n }\n}\n\nfunction writeSeen(set: Set<string>): void {\n if (typeof window === 'undefined') return\n try {\n window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(set).slice(-50)))\n } catch {\n // best-effort\n }\n}\n\n/**\n * Null-rendering component for the `checkoutStart` plugin slot. Fires\n * Snap `START_CHECKOUT` exactly once per cartId. Two-layer dedup:\n * - `useRef` guards StrictMode double-mount.\n * - `sessionStorage` guards page-refresh + back-navigation within the\n * same session.\n *\n * Self-fetches the cart from Medusa so `value` and `currency` are correct\n * even when the merchant slot context is thin or wrong. Snap treats\n * `value` as major units, so `toMajorUnits` runs at this boundary (Medusa\n * stores everything in minor units).\n *\n * The dedup ref is set only AFTER a successful dispatch - a StrictMode\n * cleanup between effect-run and fetch-complete does NOT poison the ref,\n * so the event still fires on the second mount.\n */\nexport function SnapStartCheckout({\n cartId,\n itemCount,\n total,\n currency,\n}: SnapStartCheckoutProps): null {\n const fired = useRef<string | null>(null)\n\n useEffect(() => {\n if (!cartId) return\n if (fired.current === cartId) return\n const seen = readSeen()\n if (seen.has(cartId)) {\n fired.current = cartId\n return\n }\n\n let cancelled = false\n\n void (async () => {\n const cart = await fetchCart(cartId)\n if (cancelled) return\n\n let resolvedItems: LineItem[] = []\n let resolvedTotal = total\n let resolvedCurrency = currency\n let resolvedNumItems = itemCount ?? 0\n\n if (cart) {\n if (Array.isArray(cart.items)) resolvedItems = cart.items\n if (typeof cart.total === 'number') resolvedTotal = cart.total\n if (typeof cart.currency_code === 'string') resolvedCurrency = cart.currency_code\n resolvedNumItems = resolvedItems.reduce((sum, it) => sum + (it.quantity ?? 0), 0) || resolvedNumItems\n }\n\n const contentIds = resolvedItems\n .map((it) => it.product_id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n const majorValue = toMajorUnits(resolvedTotal, resolvedCurrency)\n\n // Skip the fire if neither the fetch nor the merchant props gave us\n // enough to describe a meaningful checkout. num_items: 0 with empty\n // content_ids is a garbage event that clutters Snap reporting.\n if (resolvedNumItems <= 0 && contentIds.length === 0) return\n\n trackStartCheckout(cartId, resolvedNumItems, majorValue, resolvedCurrency, contentIds)\n\n // Only mark dedup AFTER successful dispatch - StrictMode cleanup\n // during the fetch shouldn't leave the ref set with no event fired.\n fired.current = cartId\n seen.add(cartId)\n writeSeen(seen)\n })()\n\n return () => {\n cancelled = true\n }\n }, [cartId, itemCount, total, currency])\n\n return null\n}\n","'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { trackPurchase } from '../hooks/useSnapchatEvents'\nimport { fetchOrder, toMajorUnits, type LineItem } from '@amboras-dev/pixel-core'\nimport { STORAGE_KEYS } from '../constants'\n\nexport interface SnapPurchaseProps {\n orderId?: string\n total?: number\n currency?: string\n itemCount?: number\n}\n\n/**\n * Null-rendering component for the `checkoutComplete` plugin slot.\n * Fires Snap `PURCHASE` (with `transaction_id` = order id) exactly once per\n * orderId.\n *\n * Primary path: self-fetch from `/store/orders/:id` and derive value +\n * currency + contentIds from the authoritative Medusa response, then convert\n * to major units via currency-aware `toMajorUnits` (correct for JPY/KWD).\n *\n * Fallback path (fetch failed - guest order with expired session, network\n * error): fire using the incoming `total` + `currency` props with the same\n * conversion. Snap dedups against the CAPI relay by `client_dedup_id` /\n * `event_id` (48h) and `transaction_id` / `order_id` (30d).\n *\n * Two-layer dedup (useRef + sessionStorage) so a StrictMode double-mount,\n * a page refresh on the thank-you page, or a back-nav never double-fires.\n * The ref is set AFTER dispatch so a StrictMode cleanup between mount and\n * effect body doesn't poison it.\n */\nexport function SnapPurchase({\n orderId,\n total,\n currency,\n itemCount,\n}: SnapPurchaseProps): null {\n const fired = useRef<string | null>(null)\n\n useEffect(() => {\n if (!orderId) return\n if (fired.current === orderId) return\n const dedupeKey = `${STORAGE_KEYS.PURCHASE_PREFIX}${orderId}`\n try {\n if (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(dedupeKey)) {\n fired.current = orderId\n return\n }\n } catch {\n // sessionStorage unavailable - proceed\n }\n\n let cancelled = false\n\n void (async () => {\n const order = await fetchOrder(orderId)\n if (cancelled) return\n\n let resolvedItems: LineItem[] = []\n let resolvedTotal = total\n let resolvedCurrency = currency\n let resolvedNumItems = itemCount\n\n if (order) {\n if (Array.isArray(order.items)) resolvedItems = order.items\n if (typeof order.total === 'number') resolvedTotal = order.total\n if (typeof order.currency_code === 'string') resolvedCurrency = order.currency_code\n const summed = resolvedItems.reduce((sum, it) => sum + (it.quantity ?? 0), 0)\n if (summed > 0) resolvedNumItems = summed\n }\n\n // Purchase requires a real value + currency for Snap. Without\n // them the CAPI event would be rejected and the browser tag would\n // be uselessly counted with $0.\n const majorValue = toMajorUnits(resolvedTotal, resolvedCurrency)\n if (majorValue === undefined || !resolvedCurrency) return\n\n const contentIds = resolvedItems\n .map((it) => it.product_id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n\n trackPurchase(orderId, majorValue, resolvedCurrency, resolvedNumItems, contentIds)\n\n fired.current = orderId\n try {\n if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(dedupeKey, '1')\n } catch {\n // best-effort\n }\n })()\n\n return () => {\n cancelled = true\n }\n }, [orderId, total, currency, itemCount])\n\n return null\n}\n"],"mappings":"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","names":["useEffect","useRef","useRef","useEffect","useEffect","useRef","useRef","useEffect","useEffect","useRef","toMajorUnits","useRef","useEffect","toMajorUnits","useEffect","useRef","toMajorUnits","useRef","useEffect","toMajorUnits"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@amboras-dev/snapchat-ads",
3
+ "version": "0.1.0",
4
+ "description": "Snap Pixel + Conversions API hooks for Amboras storefronts",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./slot-components": {
15
+ "types": "./dist/slot-components/index.d.ts",
16
+ "import": "./dist/slot-components/index.mjs",
17
+ "require": "./dist/slot-components/index.js"
18
+ },
19
+ "./get-config": {
20
+ "types": "./dist/get-config.d.ts",
21
+ "import": "./dist/get-config.mjs",
22
+ "require": "./dist/get-config.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "@amboras-dev/pixel-core": "^0.1.0"
31
+ },
32
+ "peerDependencies": {
33
+ "react": ">=19",
34
+ "next": ">=14"
35
+ },
36
+ "devDependencies": {
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.0.0",
39
+ "@types/react": "^19.0.0",
40
+ "@types/node": "^20.0.0",
41
+ "vitest": "^2.1.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "typecheck": "tsc --noEmit",
49
+ "test": "vitest run"
50
+ }
51
+ }