@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.
@@ -0,0 +1,627 @@
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
+
7
+ // src/lib/consent.ts
8
+ function hasAnalyticsConsent() {
9
+ if (typeof document === "undefined") return false;
10
+ const match = document.cookie.match(/(?:^|;\s*)amboras_consent=([^;]*)/);
11
+ if (!match) return true;
12
+ try {
13
+ return JSON.parse(decodeURIComponent(match[1]))?.analytics === true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ // src/lib/snap-pixel-loader.ts
20
+ var SCRIPT_SRC = "https://sc-static.net/scevent.min.js";
21
+ function installSnapPixelShim() {
22
+ if (typeof window === "undefined" || typeof document === "undefined") return;
23
+ if (window.snaptr) return;
24
+ const queue = [];
25
+ const shim = function(...args) {
26
+ if (typeof shim.handleRequest === "function") {
27
+ shim.handleRequest(...args);
28
+ return;
29
+ }
30
+ queue.push(args);
31
+ };
32
+ shim.push = function(...args) {
33
+ queue.push(args);
34
+ };
35
+ shim.queue = queue;
36
+ shim.version = "1.0";
37
+ shim.loaded = true;
38
+ window.snaptr = shim;
39
+ const script = document.createElement("script");
40
+ script.async = true;
41
+ script.src = SCRIPT_SRC;
42
+ document.head.appendChild(script);
43
+ }
44
+ function safeSnaptr(...args) {
45
+ if (typeof window === "undefined") return false;
46
+ const fn = window.snaptr;
47
+ if (typeof fn !== "function") return false;
48
+ try {
49
+ fn(...args);
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ // src/lib/dedup.ts
57
+ var MS_PER_MINUTE = 60 * 1e3;
58
+ function deriveMinuteBucket(nowMs) {
59
+ if (!Number.isFinite(nowMs)) return 0;
60
+ return Math.floor(nowMs / MS_PER_MINUTE);
61
+ }
62
+ function minuteBucketNow() {
63
+ return deriveMinuteBucket(Date.now());
64
+ }
65
+ function safeKey(primaryKey) {
66
+ if (!primaryKey) return "unknown";
67
+ return primaryKey.replace(/[^a-zA-Z0-9_-]/g, "_");
68
+ }
69
+ function makeEventId(eventName, primaryKey, minuteBucket = minuteBucketNow()) {
70
+ return `${eventName}_${safeKey(primaryKey)}_${minuteBucket}`;
71
+ }
72
+ function deriveOrderIdForPurchase(eventName, orderId) {
73
+ if (eventName !== "PURCHASE") return void 0;
74
+ if (typeof orderId !== "string") return void 0;
75
+ const trimmed = orderId.trim();
76
+ return trimmed ? trimmed : void 0;
77
+ }
78
+
79
+ // src/slot-components/SnapPixelProvider.tsx
80
+ function SnapPixelProvider({ pixelId }) {
81
+ const [consented, setConsented] = useState(false);
82
+ const initedForPixelId = useRef(null);
83
+ const pathname = usePathname();
84
+ const searchParams = useSearchParams();
85
+ useEffect(() => {
86
+ setConsented(hasAnalyticsConsent());
87
+ const handler = () => setConsented(hasAnalyticsConsent());
88
+ window.addEventListener("amboras-consent-changed", handler);
89
+ return () => window.removeEventListener("amboras-consent-changed", handler);
90
+ }, []);
91
+ useEffect(() => {
92
+ if (!pixelId || !consented) return;
93
+ installSnapPixelShim();
94
+ if (initedForPixelId.current !== pixelId) {
95
+ safeSnaptr("init", pixelId);
96
+ initedForPixelId.current = pixelId;
97
+ }
98
+ }, [pixelId, consented]);
99
+ useEffect(() => {
100
+ if (!pixelId || !consented) return;
101
+ if (initedForPixelId.current === null) return;
102
+ const key = `${pathname ?? "/"}?${searchParams?.toString() ?? ""}`;
103
+ const eid = makeEventId("PAGE_VIEW", key, minuteBucketNow());
104
+ safeSnaptr("track", "PAGE_VIEW", { client_dedup_id: eid });
105
+ }, [pathname, searchParams, pixelId, consented]);
106
+ return null;
107
+ }
108
+
109
+ // src/slot-components/SnapViewContent.tsx
110
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
111
+
112
+ // src/hooks/useSnapchatEvents.ts
113
+ var CAPI_PROXY_ENDPOINT = "/api/snapchat-ads/events";
114
+ function currentUrl() {
115
+ return typeof window !== "undefined" ? window.location.href : "";
116
+ }
117
+ async function sendSnapchatCapiEvent(payload) {
118
+ try {
119
+ await fetch(CAPI_PROXY_ENDPOINT, {
120
+ method: "POST",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify(payload)
123
+ });
124
+ } catch {
125
+ }
126
+ }
127
+ function trackViewContent(productId, options = {}) {
128
+ const eid = options.eventId ?? makeEventId("VIEW_CONTENT", productId, minuteBucketNow());
129
+ const contentIds = options.contentIds ?? [productId];
130
+ const customData = {
131
+ ...options.currency ? { currency: options.currency } : {},
132
+ ...options.value !== void 0 ? { value: options.value } : {},
133
+ ...contentIds.length ? { contentIds } : {},
134
+ ...options.contents?.length ? { contents: options.contents } : {},
135
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {}
136
+ };
137
+ safeSnaptr("track", "VIEW_CONTENT", {
138
+ ...contentIds.length ? { item_ids: contentIds } : {},
139
+ ...options.currency ? { currency: options.currency } : {},
140
+ ...options.value !== void 0 ? { value: options.value } : {},
141
+ client_dedup_id: eid
142
+ });
143
+ void sendSnapchatCapiEvent({
144
+ eventName: "VIEW_CONTENT",
145
+ eventId: eid,
146
+ eventSourceUrl: currentUrl(),
147
+ userData: options.userData ?? {},
148
+ customData
149
+ });
150
+ }
151
+ function trackAddCart(cartId, options = {}) {
152
+ const eid = options.eventId ?? makeEventId("ADD_CART", cartId, minuteBucketNow());
153
+ const customData = {
154
+ ...options.currency ? { currency: options.currency } : {},
155
+ ...options.value !== void 0 ? { value: options.value } : {},
156
+ ...options.contentIds?.length ? { contentIds: options.contentIds } : {},
157
+ ...options.contents?.length ? { contents: options.contents } : {},
158
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {}
159
+ };
160
+ safeSnaptr("track", "ADD_CART", {
161
+ ...options.contentIds?.length ? { item_ids: options.contentIds } : {},
162
+ ...options.currency ? { currency: options.currency } : {},
163
+ ...options.value !== void 0 ? { value: options.value } : {},
164
+ ...typeof options.numItems === "number" ? { number_items: options.numItems } : {},
165
+ client_dedup_id: eid
166
+ });
167
+ void sendSnapchatCapiEvent({
168
+ eventName: "ADD_CART",
169
+ eventId: eid,
170
+ eventSourceUrl: currentUrl(),
171
+ userData: options.userData ?? {},
172
+ customData
173
+ });
174
+ }
175
+ function trackStartCheckout(cartId, options = {}) {
176
+ const eid = options.eventId ?? makeEventId("START_CHECKOUT", cartId, minuteBucketNow());
177
+ const customData = {
178
+ ...options.currency ? { currency: options.currency } : {},
179
+ ...options.value !== void 0 ? { value: options.value } : {},
180
+ ...options.contentIds?.length ? { contentIds: options.contentIds } : {},
181
+ ...options.contents?.length ? { contents: options.contents } : {},
182
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {}
183
+ };
184
+ safeSnaptr("track", "START_CHECKOUT", {
185
+ ...options.contentIds?.length ? { item_ids: options.contentIds } : {},
186
+ ...options.currency ? { currency: options.currency } : {},
187
+ ...options.value !== void 0 ? { value: options.value } : {},
188
+ ...typeof options.numItems === "number" ? { number_items: options.numItems } : {},
189
+ client_dedup_id: eid
190
+ });
191
+ void sendSnapchatCapiEvent({
192
+ eventName: "START_CHECKOUT",
193
+ eventId: eid,
194
+ eventSourceUrl: currentUrl(),
195
+ userData: options.userData ?? {},
196
+ customData
197
+ });
198
+ }
199
+ function trackPurchase(orderId, options = {}) {
200
+ const eid = options.eventId ?? makeEventId("PURCHASE", orderId, 0);
201
+ const derivedOrderId = deriveOrderIdForPurchase("PURCHASE", orderId);
202
+ const customData = {
203
+ ...options.currency ? { currency: options.currency } : {},
204
+ ...options.value !== void 0 ? { value: options.value } : {},
205
+ ...options.contentIds?.length ? { contentIds: options.contentIds } : {},
206
+ ...options.contents?.length ? { contents: options.contents } : {},
207
+ ...typeof options.numItems === "number" ? { numItems: options.numItems } : {},
208
+ ...derivedOrderId ? { orderId: derivedOrderId } : {}
209
+ };
210
+ safeSnaptr("track", "PURCHASE", {
211
+ ...options.contentIds?.length ? { item_ids: options.contentIds } : {},
212
+ ...options.currency ? { currency: options.currency } : {},
213
+ ...options.value !== void 0 ? { value: options.value } : {},
214
+ ...typeof options.numItems === "number" ? { number_items: options.numItems } : {},
215
+ ...derivedOrderId ? { transaction_id: derivedOrderId } : {},
216
+ client_dedup_id: eid
217
+ });
218
+ void sendSnapchatCapiEvent({
219
+ eventName: "PURCHASE",
220
+ eventId: eid,
221
+ eventSourceUrl: currentUrl(),
222
+ userData: options.userData ?? {},
223
+ customData
224
+ });
225
+ }
226
+
227
+ // src/lib/currency.ts
228
+ var ZERO_DECIMAL = [
229
+ "BIF",
230
+ "CLP",
231
+ "DJF",
232
+ "GNF",
233
+ "ISK",
234
+ "JPY",
235
+ "KMF",
236
+ "KRW",
237
+ "PYG",
238
+ "RWF",
239
+ "UGX",
240
+ "UYI",
241
+ "VND",
242
+ "VUV",
243
+ "XAF",
244
+ "XOF",
245
+ "XPF"
246
+ ];
247
+ var THREE_DECIMAL = ["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"];
248
+ function decimalsForCurrency(currency) {
249
+ if (!currency) return 2;
250
+ const code = currency.toUpperCase();
251
+ if (ZERO_DECIMAL.includes(code)) return 0;
252
+ if (THREE_DECIMAL.includes(code)) return 3;
253
+ return 2;
254
+ }
255
+ function toMajorUnits(minorAmount, currency) {
256
+ if (typeof minorAmount !== "number" || !Number.isFinite(minorAmount)) return void 0;
257
+ const decimals = decimalsForCurrency(currency);
258
+ if (decimals === 0) return minorAmount;
259
+ const factor = 10 ** decimals;
260
+ return minorAmount / factor;
261
+ }
262
+ function toSnapValue(majorAmount, currency) {
263
+ if (typeof majorAmount !== "number" || !Number.isFinite(majorAmount)) return void 0;
264
+ const decimals = decimalsForCurrency(currency);
265
+ return majorAmount.toFixed(decimals);
266
+ }
267
+ function normalizeCurrency(currency) {
268
+ if (!currency) return void 0;
269
+ const trimmed = currency.trim();
270
+ if (!trimmed) return void 0;
271
+ return trimmed.toUpperCase();
272
+ }
273
+ function toSnapQuantity(quantity) {
274
+ if (typeof quantity !== "number" || !Number.isFinite(quantity)) return void 0;
275
+ return String(Math.trunc(quantity));
276
+ }
277
+
278
+ // src/slot-components/SnapViewContent.tsx
279
+ function SnapViewContent({ productId, price, currency }) {
280
+ const lastProductId = useRef2(null);
281
+ useEffect2(() => {
282
+ if (!productId || lastProductId.current === productId) return;
283
+ lastProductId.current = productId;
284
+ const normalizedCurrency = normalizeCurrency(currency);
285
+ const value = toSnapValue(price, normalizedCurrency);
286
+ trackViewContent(productId, {
287
+ currency: normalizedCurrency,
288
+ value,
289
+ contentIds: [productId],
290
+ numItems: 1
291
+ });
292
+ }, [productId, price, currency]);
293
+ return null;
294
+ }
295
+
296
+ // src/slot-components/SnapAddCart.tsx
297
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
298
+
299
+ // src/lib/backend.ts
300
+ function resolveBackendUrl() {
301
+ if (typeof process === "undefined" || !process.env) return "";
302
+ return process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL ?? process.env.NEXT_PUBLIC_API_URL ?? process.env.VITE_MEDUSA_BACKEND_URL ?? "";
303
+ }
304
+ function resolvePublishableKey() {
305
+ if (typeof process === "undefined" || !process.env) return "";
306
+ return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ?? process.env.NEXT_PUBLIC_PUBLISHABLE_KEY ?? process.env.VITE_MEDUSA_PUBLISHABLE_KEY ?? "";
307
+ }
308
+ function resolveStoreEnvironmentId() {
309
+ if (typeof process === "undefined" || !process.env) return "";
310
+ return process.env.NEXT_PUBLIC_STORE_ID ?? "";
311
+ }
312
+ function storeHeaders() {
313
+ const key = resolvePublishableKey();
314
+ const storeId = resolveStoreEnvironmentId();
315
+ const headers = {};
316
+ if (key) headers["x-publishable-api-key"] = key;
317
+ if (storeId) headers["X-Store-Environment-ID"] = storeId;
318
+ return headers;
319
+ }
320
+
321
+ // src/slot-components/SnapAddCart.tsx
322
+ var STORAGE_KEY = "amboras_snap_cart_state_v1";
323
+ var DEDUPE_WINDOW_MS = 1500;
324
+ var RECENT_FIRES = (() => {
325
+ const KEY = "__amboras_snap_ads_recent_fires__";
326
+ const g = globalThis;
327
+ const existing = g[KEY];
328
+ if (existing instanceof Map) return existing;
329
+ const fresh = /* @__PURE__ */ new Map();
330
+ g[KEY] = fresh;
331
+ return fresh;
332
+ })();
333
+ function readStored() {
334
+ if (typeof window === "undefined") return null;
335
+ try {
336
+ const raw = window.sessionStorage.getItem(STORAGE_KEY);
337
+ return raw ? JSON.parse(raw) : null;
338
+ } catch {
339
+ return null;
340
+ }
341
+ }
342
+ function writeStored(snapshot) {
343
+ if (typeof window === "undefined") return;
344
+ try {
345
+ window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
346
+ } catch {
347
+ }
348
+ }
349
+ async function fetchCart(cartId) {
350
+ const baseUrl = resolveBackendUrl();
351
+ if (!baseUrl) return null;
352
+ try {
353
+ const res = await fetch(
354
+ `${baseUrl}/store/carts/${encodeURIComponent(cartId)}?fields=id,currency_code,*items`,
355
+ { method: "GET", headers: storeHeaders(), credentials: "include" }
356
+ );
357
+ if (!res.ok) return null;
358
+ const json = await res.json();
359
+ return json.cart ?? null;
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+ function withinDedupeWindow(variantId) {
365
+ const now = Date.now();
366
+ const last = RECENT_FIRES.get(variantId);
367
+ if (last && now - last < DEDUPE_WINDOW_MS) return true;
368
+ RECENT_FIRES.set(variantId, now);
369
+ if (RECENT_FIRES.size > 50) {
370
+ const cutoff = now - DEDUPE_WINDOW_MS * 4;
371
+ for (const [k, t] of RECENT_FIRES) {
372
+ if (t < cutoff) RECENT_FIRES.delete(k);
373
+ }
374
+ }
375
+ return false;
376
+ }
377
+ function SnapAddCart({ cartId, itemCount: _itemCount }) {
378
+ const previousCartId = useRef3(null);
379
+ useEffect3(() => {
380
+ if (!cartId) return;
381
+ let cancelled = false;
382
+ void (async () => {
383
+ const cart = await fetchCart(cartId);
384
+ if (cancelled) return;
385
+ if (!cart) {
386
+ previousCartId.current = cartId;
387
+ return;
388
+ }
389
+ const items = Array.isArray(cart.items) ? cart.items : [];
390
+ const currency = normalizeCurrency(cart.currency_code);
391
+ const currentMap = {};
392
+ const unitPriceByVariant = {};
393
+ const productIdByVariant = {};
394
+ for (const it of items) {
395
+ if (!it.variant_id) continue;
396
+ currentMap[it.variant_id] = (currentMap[it.variant_id] ?? 0) + (it.quantity ?? 0);
397
+ unitPriceByVariant[it.variant_id] = it.unit_price ?? void 0;
398
+ productIdByVariant[it.variant_id] = it.product_id ?? void 0;
399
+ }
400
+ const stored = readStored();
401
+ const isFirstHydration = previousCartId.current === null;
402
+ let baseline;
403
+ if (stored && stored.cartId === cartId) {
404
+ baseline = stored.quantitiesByVariant;
405
+ } else if (isFirstHydration) {
406
+ baseline = currentMap;
407
+ } else {
408
+ baseline = {};
409
+ }
410
+ previousCartId.current = cartId;
411
+ for (const [variantId, newQty] of Object.entries(currentMap)) {
412
+ const oldQty = baseline[variantId] ?? 0;
413
+ const delta = newQty - oldQty;
414
+ if (delta <= 0) continue;
415
+ if (withinDedupeWindow(variantId)) continue;
416
+ const productId = productIdByVariant[variantId];
417
+ const unitPriceMinor = unitPriceByVariant[variantId];
418
+ const contentIds = productId ? [productId, variantId] : [variantId];
419
+ const majorValue = typeof unitPriceMinor === "number" && Number.isFinite(unitPriceMinor) ? toMajorUnits(unitPriceMinor * delta, currency) : void 0;
420
+ const valueStr = toSnapValue(majorValue, currency);
421
+ const perItemMajor = typeof unitPriceMinor === "number" && Number.isFinite(unitPriceMinor) ? toMajorUnits(unitPriceMinor, currency) : void 0;
422
+ const contents = productId ? [
423
+ {
424
+ id: productId,
425
+ quantity: toSnapQuantity(delta) ?? String(delta),
426
+ ...perItemMajor !== void 0 ? { item_price: toSnapValue(perItemMajor, currency) } : {}
427
+ }
428
+ ] : [];
429
+ trackAddCart(cartId, {
430
+ currency,
431
+ value: valueStr,
432
+ contentIds,
433
+ contents,
434
+ numItems: delta
435
+ });
436
+ }
437
+ writeStored({ cartId, quantitiesByVariant: currentMap });
438
+ })();
439
+ return () => {
440
+ cancelled = true;
441
+ };
442
+ }, [cartId, _itemCount]);
443
+ return null;
444
+ }
445
+
446
+ // src/slot-components/SnapStartCheckout.tsx
447
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
448
+ async function fetchCart2(cartId) {
449
+ const baseUrl = resolveBackendUrl();
450
+ if (!baseUrl) return null;
451
+ try {
452
+ const res = await fetch(
453
+ `${baseUrl}/store/carts/${encodeURIComponent(cartId)}?fields=id,currency_code,total,*items`,
454
+ { method: "GET", headers: storeHeaders(), credentials: "include" }
455
+ );
456
+ if (!res.ok) return null;
457
+ const json = await res.json();
458
+ return json.cart ?? null;
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+ var STORAGE_KEY2 = "amboras_snap_start_checkout_seen";
464
+ function readSeen() {
465
+ if (typeof window === "undefined") return /* @__PURE__ */ new Set();
466
+ try {
467
+ const raw = window.sessionStorage.getItem(STORAGE_KEY2);
468
+ if (!raw) return /* @__PURE__ */ new Set();
469
+ const parsed = JSON.parse(raw);
470
+ return Array.isArray(parsed) ? new Set(parsed.filter((v) => typeof v === "string")) : /* @__PURE__ */ new Set();
471
+ } catch {
472
+ return /* @__PURE__ */ new Set();
473
+ }
474
+ }
475
+ function writeSeen(set) {
476
+ if (typeof window === "undefined") return;
477
+ try {
478
+ window.sessionStorage.setItem(STORAGE_KEY2, JSON.stringify(Array.from(set).slice(-50)));
479
+ } catch {
480
+ }
481
+ }
482
+ function SnapStartCheckout({ cartId, itemCount, total, currency }) {
483
+ const fired = useRef4(null);
484
+ useEffect4(() => {
485
+ if (!cartId) return;
486
+ if (fired.current === cartId) return;
487
+ const seen = readSeen();
488
+ if (seen.has(cartId)) {
489
+ fired.current = cartId;
490
+ return;
491
+ }
492
+ let cancelled = false;
493
+ void (async () => {
494
+ const cart = await fetchCart2(cartId);
495
+ if (cancelled) return;
496
+ let resolvedItems = [];
497
+ let resolvedTotal = total;
498
+ let resolvedCurrency = currency;
499
+ let resolvedNumItems = itemCount ?? 0;
500
+ if (cart) {
501
+ if (Array.isArray(cart.items)) resolvedItems = cart.items;
502
+ if (typeof cart.total === "number" && resolvedTotal === void 0) resolvedTotal = cart.total;
503
+ if (typeof cart.currency_code === "string" && !resolvedCurrency) resolvedCurrency = cart.currency_code;
504
+ resolvedNumItems = resolvedItems.reduce((sum, it) => sum + (it.quantity ?? 0), 0) || resolvedNumItems;
505
+ }
506
+ const normalizedCurrency = normalizeCurrency(resolvedCurrency);
507
+ const contentIds = resolvedItems.map((it) => it.product_id).filter((id) => typeof id === "string" && id.length > 0);
508
+ const contents = resolvedItems.filter((it) => it.product_id).map((it) => ({
509
+ id: it.product_id,
510
+ quantity: toSnapQuantity(it.quantity ?? 1) ?? "1",
511
+ ...typeof it.unit_price === "number" ? { item_price: toSnapValue(toMajorUnits(it.unit_price, normalizedCurrency), normalizedCurrency) } : {}
512
+ }));
513
+ const majorValue = toMajorUnits(resolvedTotal, normalizedCurrency);
514
+ const valueStr = toSnapValue(majorValue, normalizedCurrency);
515
+ if (resolvedNumItems <= 0 && contentIds.length === 0) return;
516
+ trackStartCheckout(cartId, {
517
+ currency: normalizedCurrency,
518
+ value: valueStr,
519
+ contentIds,
520
+ contents,
521
+ numItems: resolvedNumItems
522
+ });
523
+ fired.current = cartId;
524
+ seen.add(cartId);
525
+ writeSeen(seen);
526
+ })();
527
+ return () => {
528
+ cancelled = true;
529
+ };
530
+ }, [cartId, itemCount, total, currency]);
531
+ return null;
532
+ }
533
+
534
+ // src/slot-components/SnapPurchase.tsx
535
+ import { useEffect as useEffect5, useRef as useRef5 } from "react";
536
+ async function fetchOrder(orderId) {
537
+ const baseUrl = resolveBackendUrl();
538
+ if (!baseUrl) return null;
539
+ try {
540
+ const res = await fetch(
541
+ `${baseUrl}/store/orders/${encodeURIComponent(orderId)}?fields=id,total,currency_code,*items`,
542
+ { method: "GET", headers: storeHeaders(), credentials: "include" }
543
+ );
544
+ if (!res.ok) return null;
545
+ const json = await res.json();
546
+ return json.order ?? null;
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+ function SnapPurchase({ orderId, total, currency, itemCount: _itemCount }) {
552
+ const fired = useRef5(null);
553
+ useEffect5(() => {
554
+ if (!orderId) return;
555
+ if (fired.current === orderId) return;
556
+ const dedupeKey = `amboras_snap_purchase_${orderId}`;
557
+ try {
558
+ if (typeof sessionStorage !== "undefined" && sessionStorage.getItem(dedupeKey)) {
559
+ fired.current = orderId;
560
+ return;
561
+ }
562
+ } catch {
563
+ }
564
+ let cancelled = false;
565
+ void (async () => {
566
+ const order = await fetchOrder(orderId);
567
+ if (cancelled) return;
568
+ const fetchSucceeded = !!order && typeof order.total === "number" && !!order.currency_code;
569
+ let dispatched = false;
570
+ const eid = makeEventId("PURCHASE", orderId, 0);
571
+ if (fetchSucceeded && order) {
572
+ const normalizedCurrency = normalizeCurrency(order.currency_code);
573
+ const resolvedItems = Array.isArray(order.items) ? order.items : [];
574
+ const contentIds = resolvedItems.map((it) => it.product_id).filter((id) => typeof id === "string" && id.length > 0);
575
+ const contents = resolvedItems.filter((it) => it.product_id).map((it) => ({
576
+ id: it.product_id,
577
+ quantity: toSnapQuantity(it.quantity ?? 1) ?? "1",
578
+ ...typeof it.unit_price === "number" ? { item_price: toSnapValue(toMajorUnits(it.unit_price, normalizedCurrency), normalizedCurrency) } : {}
579
+ }));
580
+ const majorValue = toMajorUnits(order.total, normalizedCurrency);
581
+ const valueStr = toSnapValue(majorValue, normalizedCurrency);
582
+ if (valueStr !== void 0) {
583
+ trackPurchase(orderId, {
584
+ currency: normalizedCurrency,
585
+ value: valueStr,
586
+ contentIds,
587
+ contents,
588
+ numItems: resolvedItems.reduce((sum, it) => sum + (it.quantity ?? 0), 0),
589
+ eventId: eid
590
+ });
591
+ dispatched = true;
592
+ }
593
+ } else if (typeof total === "number" && !!currency) {
594
+ const normalizedCurrency = normalizeCurrency(currency);
595
+ if (process.env.NODE_ENV !== "production") {
596
+ console.warn(
597
+ "[snapchat-ads] SnapPurchase: order fetch failed, firing minimal browser PURCHASE from merchant-supplied total/currency. content_ids unavailable in this branch. If your CAPI subscriber is healthy the server event will provide full attribution."
598
+ );
599
+ }
600
+ trackPurchase(orderId, {
601
+ currency: normalizedCurrency,
602
+ value: toSnapValue(total, normalizedCurrency),
603
+ eventId: eid
604
+ });
605
+ dispatched = true;
606
+ }
607
+ if (!dispatched) return;
608
+ fired.current = orderId;
609
+ try {
610
+ if (typeof sessionStorage !== "undefined") sessionStorage.setItem(dedupeKey, "1");
611
+ } catch {
612
+ }
613
+ })();
614
+ return () => {
615
+ cancelled = true;
616
+ };
617
+ }, [orderId, total, currency]);
618
+ return null;
619
+ }
620
+ export {
621
+ SnapAddCart,
622
+ SnapPixelProvider,
623
+ SnapPurchase,
624
+ SnapStartCheckout,
625
+ SnapViewContent
626
+ };
627
+ //# sourceMappingURL=index.mjs.map