@tagadapay/headless-sdk 1.14.0 → 1.16.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.
Files changed (81) hide show
  1. package/dist/core/client.d.ts +7 -0
  2. package/dist/core/client.d.ts.map +1 -1
  3. package/dist/core/client.js +10 -3
  4. package/dist/core/client.js.map +1 -1
  5. package/dist/core/index.d.ts +3 -1
  6. package/dist/core/index.d.ts.map +1 -1
  7. package/dist/core/index.js +1 -0
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/core/modules/checkout.d.ts +5 -1
  10. package/dist/core/modules/checkout.d.ts.map +1 -1
  11. package/dist/core/modules/checkout.js +13 -11
  12. package/dist/core/modules/checkout.js.map +1 -1
  13. package/dist/core/modules/payment.d.ts +19 -1
  14. package/dist/core/modules/payment.d.ts.map +1 -1
  15. package/dist/core/modules/payment.js +42 -3
  16. package/dist/core/modules/payment.js.map +1 -1
  17. package/dist/core/modules/rx.d.ts +32 -1
  18. package/dist/core/modules/rx.d.ts.map +1 -1
  19. package/dist/core/modules/rx.js +17 -1
  20. package/dist/core/modules/rx.js.map +1 -1
  21. package/dist/core/modules/track.d.ts +127 -0
  22. package/dist/core/modules/track.d.ts.map +1 -0
  23. package/dist/core/modules/track.js +371 -0
  24. package/dist/core/modules/track.js.map +1 -0
  25. package/dist/core/types.d.ts +2 -0
  26. package/dist/core/types.d.ts.map +1 -1
  27. package/dist/core/version.d.ts +4 -0
  28. package/dist/core/version.d.ts.map +1 -0
  29. package/dist/core/version.js +4 -0
  30. package/dist/core/version.js.map +1 -0
  31. package/dist/pixels/engine.d.ts +56 -0
  32. package/dist/pixels/engine.d.ts.map +1 -0
  33. package/dist/pixels/engine.js +263 -0
  34. package/dist/pixels/engine.js.map +1 -0
  35. package/dist/pixels/index.d.ts +10 -0
  36. package/dist/pixels/index.d.ts.map +1 -0
  37. package/dist/pixels/index.js +5 -0
  38. package/dist/pixels/index.js.map +1 -0
  39. package/dist/pixels/metaEventId.d.ts +15 -0
  40. package/dist/pixels/metaEventId.d.ts.map +1 -0
  41. package/dist/pixels/metaEventId.js +17 -0
  42. package/dist/pixels/metaEventId.js.map +1 -0
  43. package/dist/pixels/pixelIdentity.d.ts +34 -0
  44. package/dist/pixels/pixelIdentity.d.ts.map +1 -0
  45. package/dist/pixels/pixelIdentity.js +65 -0
  46. package/dist/pixels/pixelIdentity.js.map +1 -0
  47. package/dist/pixels/pixelMapping.d.ts +172 -0
  48. package/dist/pixels/pixelMapping.d.ts.map +1 -0
  49. package/dist/pixels/pixelMapping.js +574 -0
  50. package/dist/pixels/pixelMapping.js.map +1 -0
  51. package/dist/pixels/pixelTracker.d.ts +81 -0
  52. package/dist/pixels/pixelTracker.d.ts.map +1 -0
  53. package/dist/pixels/pixelTracker.js +795 -0
  54. package/dist/pixels/pixelTracker.js.map +1 -0
  55. package/dist/pixels/types.d.ts +103 -0
  56. package/dist/pixels/types.d.ts.map +1 -0
  57. package/dist/pixels/types.js +14 -0
  58. package/dist/pixels/types.js.map +1 -0
  59. package/dist/react/hooks/useStep.d.ts +7 -0
  60. package/dist/react/hooks/useStep.d.ts.map +1 -0
  61. package/dist/react/hooks/useStep.js +14 -0
  62. package/dist/react/hooks/useStep.js.map +1 -0
  63. package/dist/react/index.d.ts +1 -0
  64. package/dist/react/index.d.ts.map +1 -1
  65. package/dist/react/index.js +1 -0
  66. package/dist/react/index.js.map +1 -1
  67. package/dist/stripe/index.d.ts +3 -1
  68. package/dist/stripe/index.d.ts.map +1 -1
  69. package/dist/stripe/index.js +13 -6
  70. package/dist/stripe/index.js.map +1 -1
  71. package/dist/stripe/useStripeExpress.d.ts.map +1 -1
  72. package/dist/stripe/useStripeExpress.js +15 -1
  73. package/dist/stripe/useStripeExpress.js.map +1 -1
  74. package/dist/tagada-headless-react.min.js +2 -2
  75. package/dist/tagada-headless-react.min.js.map +4 -4
  76. package/dist/tagada-headless.js +442 -26
  77. package/dist/tagada-headless.min.js +2 -2
  78. package/dist/tagada-headless.min.js.map +4 -4
  79. package/dist/tagada-pixels.min.js +7 -0
  80. package/dist/tagada-pixels.min.js.map +7 -0
  81. package/package.json +8 -1
@@ -0,0 +1,795 @@
1
+ /**
2
+ * Framework-agnostic pixel tracker.
3
+ *
4
+ * Init / fire logic for Meta, TikTok, Snapchat, Pinterest, GTM/GA4 and
5
+ * Microsoft Clarity pixels —
6
+ * extracted from `react/hooks/usePixelTracking.tsx` so non-React entry points
7
+ * (Studio islands hydrator, full-SPA runtime) can fire pixel events from
8
+ * `window.__TGD_STEP_CONFIG__.pixels`.
9
+ *
10
+ * The React provider is a thin adapter on top of this module.
11
+ */
12
+ import { resolvePixelEvents, pixelIdKeyOf, GA4_PARAM_KEY, } from './pixelMapping.js';
13
+ import { buildHashedIdentity, canHashIdentity, } from './pixelIdentity.js';
14
+ // ---------------------------------------------------------------------------
15
+ // Duplicate guard (time-window based)
16
+ // ---------------------------------------------------------------------------
17
+ function createDuplicateGuard(windowMs) {
18
+ const lastEvents = new Map();
19
+ return (eventName, parameters, eventId) => {
20
+ try {
21
+ // Include eventId in the dedup key so two distinct logical events with the
22
+ // same name+params (e.g. two Purchases) are not collapsed by the guard.
23
+ const key = JSON.stringify({ eventName, parameters, eventId });
24
+ const now = Date.now();
25
+ const last = lastEvents.get(key);
26
+ if (last && now - last < windowMs)
27
+ return false;
28
+ lastEvents.set(key, now);
29
+ return true;
30
+ }
31
+ catch {
32
+ return true;
33
+ }
34
+ };
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // Browser-specific: fire an event to the correct global pixel function
38
+ // ---------------------------------------------------------------------------
39
+ /* eslint-disable @typescript-eslint/no-explicit-any */
40
+ /**
41
+ * The entry's own identifier, read at whichever key its provider uses —
42
+ * `pixelId`, or `containerId` / `projectId` for GTM and Clarity.
43
+ */
44
+ function pixelIdOf(pixel) {
45
+ if (!pixel)
46
+ return null;
47
+ const value = pixel[pixelIdKeyOf(pixel)];
48
+ return typeof value === 'string' && value ? value : null;
49
+ }
50
+ function fire(provider, name, params, pixel, options = {}) {
51
+ if (typeof window === 'undefined')
52
+ return;
53
+ const w = window;
54
+ const eventId = options.eventId;
55
+ switch (provider) {
56
+ case 'facebook': {
57
+ // Target this pixel by id rather than calling the global `fbq('track')`,
58
+ // which Meta broadcasts to EVERY pixel registered on the page — including
59
+ // ones a merchant initialised from a pasted script we don't manage. That
60
+ // broadcast made our Purchase land on foreign pixels, undeduplicated.
61
+ // Mirrors the per-pixel targeting already used for TikTok below.
62
+ //
63
+ // Meta dedup: pass eventID as the trailing `eventID` option so this
64
+ // browser-pixel event matches a server-side CAPI event with the same id.
65
+ // See https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events
66
+ // No id means no addressable pixel: stay silent rather than fall back to
67
+ // the broadcast this change exists to remove. `sanitizePixels` drops every
68
+ // entry without a usable id, so this is unreachable today.
69
+ const pixelId = pixelIdOf(pixel);
70
+ if (!pixelId)
71
+ break;
72
+ if (eventId)
73
+ w.fbq?.('trackSingle', pixelId, name, params, { eventID: eventId });
74
+ else
75
+ w.fbq?.('trackSingle', pixelId, name, params);
76
+ break;
77
+ }
78
+ case 'tiktok': {
79
+ // Use ttq.instance(pixelId) to target each pixel individually,
80
+ // since the global ttq.track() only fires for the sdkid pixel.
81
+ const pixelId = pixelIdOf(pixel);
82
+ const target = pixelId && w.ttq?.instance ? w.ttq.instance(pixelId) : w.ttq;
83
+ if (name === 'Pageview') {
84
+ target?.page?.();
85
+ }
86
+ else if (eventId) {
87
+ target?.track?.(name, params, { event_id: eventId });
88
+ }
89
+ else {
90
+ target?.track?.(name, params);
91
+ }
92
+ break;
93
+ }
94
+ case 'snapchat':
95
+ w.snaptr?.('track', name, params);
96
+ break;
97
+ case 'pinterest':
98
+ // Pinterest handles page views via pintrk('page'), not pintrk('track', 'pagevisit')
99
+ if (name === 'pagevisit') {
100
+ w.pintrk?.('page');
101
+ }
102
+ else {
103
+ w.pintrk?.('track', name, params);
104
+ }
105
+ break;
106
+ case 'gtm':
107
+ fireGTM(name, params);
108
+ break;
109
+ // Clarity's custom-event API takes a name and nothing else; params stay
110
+ // out rather than being stringified into the event name.
111
+ case 'clarity':
112
+ w.clarity?.('event', name);
113
+ break;
114
+ }
115
+ }
116
+ /**
117
+ * Apply advanced matching to a single Snapchat pixel. Snap reads user info set
118
+ * at init and attaches it to every event fired afterwards; re-calling init with
119
+ * the (now-known) hashed identifiers updates it. We pass pre-hashed values via
120
+ * the `user_hashed_*` keys.
121
+ */
122
+ function applySnapchatIdentity(w, pixelId, h) {
123
+ if (!w?.snaptr)
124
+ return;
125
+ const userInfo = {};
126
+ if (h.email)
127
+ userInfo.user_hashed_email = h.email;
128
+ if (h.phone)
129
+ userInfo.user_hashed_phone_number = h.phone;
130
+ if (Object.keys(userInfo).length === 0)
131
+ return;
132
+ w.snaptr('init', pixelId, userInfo);
133
+ }
134
+ /**
135
+ * Apply advanced matching to a single TikTok pixel via `ttq.identify`. TikTok
136
+ * applies identified data to subsequent events and accepts pre-hashed SHA-256
137
+ * values (it detects 64-char hex and won't double-hash). Target the specific
138
+ * pixel instance, mirroring the per-pixel track() targeting.
139
+ */
140
+ function applyTikTokIdentity(w, pixelId, h) {
141
+ const target = w?.ttq?.instance ? w.ttq.instance(pixelId) : w?.ttq;
142
+ if (!target?.identify)
143
+ return;
144
+ const info = {};
145
+ if (h.email)
146
+ info.email = h.email;
147
+ if (h.phone)
148
+ info.phone_number = h.phone;
149
+ if (h.externalId)
150
+ info.external_id = h.externalId;
151
+ if (Object.keys(info).length === 0)
152
+ return;
153
+ target.identify(info);
154
+ }
155
+ /**
156
+ * Apply advanced matching to a single Meta pixel. Meta reads advanced-matching
157
+ * data passed to `fbq('init', pixelId, userData)` and attaches it to every
158
+ * event fired afterwards; re-calling init with the (now-known) identifiers
159
+ * updates it (init is idempotent and never auto-fires PageView). We pass RAW
160
+ * values — `fbevents.js` normalizes + SHA-256-hashes them in-browser before
161
+ * they leave the page — because Meta's phone normalization (digits-only) differs
162
+ * from Snap/TikTok's E.164, so letting Meta hash avoids a mismatch.
163
+ */
164
+ function applyMetaIdentity(w, pixelId, raw) {
165
+ if (!w?.fbq)
166
+ return;
167
+ const ud = {};
168
+ if (raw.email)
169
+ ud.em = raw.email.trim().toLowerCase();
170
+ if (raw.phone) {
171
+ const d = raw.phone.replace(/[^0-9]/g, '');
172
+ if (d)
173
+ ud.ph = d;
174
+ }
175
+ if (raw.externalId)
176
+ ud.external_id = raw.externalId.trim();
177
+ if (Object.keys(ud).length === 0)
178
+ return;
179
+ w.fbq('init', pixelId, ud);
180
+ }
181
+ /**
182
+ * Google Ads enhanced conversions / GA4 user-provided data.
183
+ *
184
+ * Google's documented mechanism is `gtag('set', 'user_data', {...})` BEFORE the
185
+ * conversion event fires (support.google.com/google-ads/answer/13258081) —
186
+ * `user_data` as an event parameter is not read for enhanced conversions. We
187
+ * pass RAW values; the Google tag normalizes + SHA-256 hashes in-browser.
188
+ * Field names per spec: `email`, `phone_number` (strict E.164 — dropped when
189
+ * not in that shape, email is the preferred key anyway), and
190
+ * `address.{first_name,last_name,street,city,region,postal_code,country}`.
191
+ * Exported for unit tests.
192
+ */
193
+ export function applyGoogleIdentity(w, ud) {
194
+ if (typeof w?.gtag !== 'function')
195
+ return;
196
+ const userData = {};
197
+ if (ud.email_address)
198
+ userData.email = ud.email_address.trim().toLowerCase();
199
+ if (ud.phone_number) {
200
+ const phone = ud.phone_number.replace(/[^0-9+]/g, '');
201
+ if (/^\+[0-9]{10,14}$/.test(phone))
202
+ userData.phone_number = phone;
203
+ }
204
+ if (ud.address) {
205
+ const a = ud.address;
206
+ const address = {};
207
+ if (a.first_name)
208
+ address.first_name = a.first_name;
209
+ if (a.last_name)
210
+ address.last_name = a.last_name;
211
+ if (a.street)
212
+ address.street = a.street;
213
+ if (a.city)
214
+ address.city = a.city;
215
+ if (a.region)
216
+ address.region = a.region;
217
+ if (a.postal_code)
218
+ address.postal_code = a.postal_code;
219
+ if (a.country)
220
+ address.country = a.country;
221
+ if (Object.keys(address).length > 0)
222
+ userData.address = address;
223
+ }
224
+ if (Object.keys(userData).length === 0)
225
+ return;
226
+ w.gtag('set', 'user_data', userData);
227
+ }
228
+ function fireGTM(event, params) {
229
+ try {
230
+ const w = window;
231
+ const hasEcommerce = params && typeof params === 'object' && 'ecommerce' in params;
232
+ if (w.gtag) {
233
+ // gtag.js (GA4 config tag) reads ecommerce fields at the top level of the
234
+ // event, not under a nested `ecommerce` key — flatten it back out.
235
+ if (hasEcommerce) {
236
+ const { ecommerce, ...rest } = params;
237
+ w.gtag('event', event, { ...rest, ...ecommerce });
238
+ }
239
+ else {
240
+ w.gtag('event', event, params);
241
+ }
242
+ }
243
+ else if (w.dataLayer) {
244
+ // GA4 best practice: clear the previous ecommerce object before pushing a
245
+ // new one so item arrays from earlier events don't bleed through.
246
+ if (hasEcommerce)
247
+ w.dataLayer.push({ ecommerce: null });
248
+ w.dataLayer.push({ event, ...params });
249
+ }
250
+ }
251
+ catch (error) {
252
+ console.error('[SDK GTM] Error:', error);
253
+ }
254
+ }
255
+ /* eslint-enable @typescript-eslint/no-explicit-any */
256
+ // ===========================================================================
257
+ // Pixel initialization (browser-only)
258
+ // All pixel globals are accessed via `win` typed as `any` to avoid
259
+ // `declare global` augmentation issues across tsconfig scopes.
260
+ // ===========================================================================
261
+ /* eslint-disable @typescript-eslint/no-explicit-any */
262
+ const win = (typeof window !== 'undefined' ? window : undefined);
263
+ const SCRIPT_LOAD_TIMEOUT_MS = 5000;
264
+ function waitForScriptLoad(script) {
265
+ // If the script has already loaded, resolve immediately
266
+ if (script.dataset.loaded)
267
+ return Promise.resolve();
268
+ return new Promise((resolve) => {
269
+ const done = () => { script.dataset.loaded = '1'; clearTimeout(timer); resolve(); };
270
+ const timer = setTimeout(done, SCRIPT_LOAD_TIMEOUT_MS);
271
+ script.addEventListener('load', done, { once: true });
272
+ script.addEventListener('error', done, { once: true });
273
+ });
274
+ }
275
+ let _metaScriptEl = null;
276
+ function initMetaPixel(pixelId) {
277
+ if (!win)
278
+ return Promise.resolve();
279
+ // Initialize Meta base code once
280
+ if (!win.fbq) {
281
+ const n = function (...args) {
282
+ if (n.callMethod)
283
+ n.callMethod(...args);
284
+ else
285
+ n.queue.push(args);
286
+ };
287
+ n.queue = [];
288
+ n.loaded = true;
289
+ n.version = '2.0';
290
+ win.fbq = n;
291
+ if (!win._fbq)
292
+ win._fbq = n;
293
+ const t = document.createElement('script');
294
+ t.async = true;
295
+ t.src = 'https://connect.facebook.net/en_US/fbevents.js';
296
+ const s = document.getElementsByTagName('script')[0];
297
+ s?.parentNode?.insertBefore(t, s);
298
+ _metaScriptEl = t;
299
+ }
300
+ // Register each pixel ID (fbq supports multiple pixels via multiple init calls)
301
+ win.fbq('init', pixelId);
302
+ return _metaScriptEl ? waitForScriptLoad(_metaScriptEl) : Promise.resolve();
303
+ }
304
+ let _tiktokBaseInitialized = false;
305
+ function initTikTokPixel(pixelId) {
306
+ if (!win)
307
+ return Promise.resolve();
308
+ // Initialize TikTok base code once
309
+ if (!_tiktokBaseInitialized) {
310
+ _tiktokBaseInitialized = true;
311
+ win.TiktokAnalyticsObject = 'ttq';
312
+ const ttq = (win.ttq = win.ttq || []);
313
+ ttq.methods = [
314
+ 'page', 'track', 'identify', 'instances', 'debug', 'on', 'off',
315
+ 'once', 'ready', 'alias', 'group', 'enableCookie', 'disableCookie',
316
+ 'holdConsent', 'revokeConsent', 'grantConsent',
317
+ ];
318
+ ttq.setAndDefer = function (t, e) {
319
+ t[e] = function (...args) { t.push([e, ...args]); };
320
+ };
321
+ for (const method of ttq.methods) {
322
+ ttq.setAndDefer(ttq, method);
323
+ }
324
+ ttq.instance = function (t) {
325
+ const e = ttq._i[t] || [];
326
+ for (const method of ttq.methods) {
327
+ ttq.setAndDefer(e, method);
328
+ }
329
+ return e;
330
+ };
331
+ ttq.load = function (e, n) {
332
+ const r = 'https://analytics.tiktok.com/i18n/pixel/events.js';
333
+ ttq._i = ttq._i || {};
334
+ ttq._i[e] = [];
335
+ ttq._i[e]._u = r;
336
+ ttq._t = ttq._t || {};
337
+ ttq._t[e] = +new Date();
338
+ ttq._o = ttq._o || {};
339
+ ttq._o[e] = n || {};
340
+ const s = document.createElement('script');
341
+ s.type = 'text/javascript';
342
+ s.async = true;
343
+ s.src = r + '?sdkid=' + e + '&lib=ttq';
344
+ const p = document.getElementsByTagName('script')[0];
345
+ p?.parentNode?.insertBefore(s, p);
346
+ };
347
+ }
348
+ // Skip if this specific pixel ID is already registered
349
+ if (win.ttq._i?.[pixelId])
350
+ return Promise.resolve();
351
+ // Register pixel and load its script
352
+ win.ttq.load(pixelId);
353
+ // Find the script we just created and wait for it
354
+ const scripts = document.querySelectorAll('script[src*="analytics.tiktok.com/i18n/pixel/events.js"]');
355
+ const lastScript = scripts[scripts.length - 1];
356
+ return lastScript ? waitForScriptLoad(lastScript) : Promise.resolve();
357
+ }
358
+ let _snapchatScriptEl = null;
359
+ function initSnapchatPixel(pixelId) {
360
+ if (!win)
361
+ return Promise.resolve();
362
+ // Initialize Snapchat base code once
363
+ if (!win.snaptr) {
364
+ const a = function (...args) {
365
+ if (a.handleRequest)
366
+ a.handleRequest(...args);
367
+ else
368
+ a.queue.push(args);
369
+ };
370
+ a.queue = [];
371
+ win.snaptr = a;
372
+ const r = document.createElement('script');
373
+ r.async = true;
374
+ r.src = 'https://sc-static.net/scevent.min.js';
375
+ const u = document.getElementsByTagName('script')[0];
376
+ u?.parentNode?.insertBefore(r, u);
377
+ _snapchatScriptEl = r;
378
+ }
379
+ // Register each pixel ID (snaptr supports multiple pixels via multiple init calls)
380
+ win.snaptr('init', pixelId);
381
+ return _snapchatScriptEl ? waitForScriptLoad(_snapchatScriptEl) : Promise.resolve();
382
+ }
383
+ let _pinterestScriptEl = null;
384
+ function initPinterestPixel(pixelId) {
385
+ if (!win)
386
+ return Promise.resolve();
387
+ // Initialize Pinterest base code once
388
+ if (!win.pintrk) {
389
+ const a = function (...args) { a.queue.push(args); };
390
+ a.queue = [];
391
+ win.pintrk = a;
392
+ const s = document.createElement('script');
393
+ s.async = true;
394
+ s.src = 'https://s.pinimg.com/ct/core.js';
395
+ const u = document.getElementsByTagName('script')[0];
396
+ u?.parentNode?.insertBefore(s, u);
397
+ _pinterestScriptEl = s;
398
+ }
399
+ // Register each pixel ID (pintrk supports multiple pixels via multiple load calls)
400
+ win.pintrk('load', pixelId);
401
+ // Note: pintrk('page') is NOT called here — the auto page-view fires via fire().
402
+ return _pinterestScriptEl ? waitForScriptLoad(_pinterestScriptEl) : Promise.resolve();
403
+ }
404
+ /**
405
+ * Load the Microsoft Clarity tag for one project.
406
+ *
407
+ * Mirrors the vendor snippet: a queueing stub so `clarity(...)` calls made
408
+ * before the script lands are replayed, then the tag itself. Clarity ignores a
409
+ * second project on the same page, so a project already present is a no-op.
410
+ */
411
+ function initClarity(projectId) {
412
+ if (!win || !projectId)
413
+ return Promise.resolve();
414
+ if (document.querySelector(`script[src*="clarity.ms/tag/${projectId}"]`))
415
+ return Promise.resolve();
416
+ if (!win.clarity) {
417
+ const q = function (...args) { q.q.push(args); };
418
+ q.q = [];
419
+ win.clarity = q;
420
+ }
421
+ const s = document.createElement('script');
422
+ s.async = true;
423
+ s.src = `https://www.clarity.ms/tag/${projectId}`;
424
+ const first = document.getElementsByTagName('script')[0];
425
+ if (first?.parentNode)
426
+ first.parentNode.insertBefore(s, first);
427
+ else
428
+ (document.head || document.getElementsByTagName('head')[0])?.appendChild(s);
429
+ return waitForScriptLoad(s);
430
+ }
431
+ function initGTM(containerId) {
432
+ if (!win || !containerId)
433
+ return Promise.resolve();
434
+ const isGtmContainer = containerId.startsWith('GTM-');
435
+ const scriptUrlPart = isGtmContainer
436
+ ? `googletagmanager.com/gtm.js?id=${containerId}`
437
+ : `googletagmanager.com/gtag/js?id=${containerId}`;
438
+ if (document.querySelector(`script[src*="${scriptUrlPart}"]`))
439
+ return Promise.resolve();
440
+ win.dataLayer = win.dataLayer || [];
441
+ // Push referrer domain context into dataLayer before GTM loads, so tags
442
+ // inside the container can use it for cross-domain tracking configuration.
443
+ try {
444
+ const ref = document.referrer && new URL(document.referrer).hostname;
445
+ if (ref && ref !== window.location.hostname) {
446
+ win.dataLayer.push({ tagada_referrer_domain: ref });
447
+ }
448
+ }
449
+ catch { /* ignore invalid referrer */ }
450
+ if (isGtmContainer) {
451
+ win.dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
452
+ const f = document.getElementsByTagName('script')[0];
453
+ const j = document.createElement('script');
454
+ j.async = true;
455
+ j.src = 'https://www.googletagmanager.com/gtm.js?id=' + containerId;
456
+ if (f?.parentNode)
457
+ f.parentNode.insertBefore(j, f);
458
+ else
459
+ (document.head || document.getElementsByTagName('head')[0])?.appendChild(j);
460
+ const noscript = document.createElement('noscript');
461
+ const iframe = document.createElement('iframe');
462
+ iframe.src = `https://www.googletagmanager.com/ns.html?id=${containerId}`;
463
+ iframe.height = '0';
464
+ iframe.width = '0';
465
+ iframe.style.display = 'none';
466
+ iframe.style.visibility = 'hidden';
467
+ noscript.appendChild(iframe);
468
+ (document.body || document.getElementsByTagName('body')[0])?.insertBefore(noscript, document.body?.firstChild ?? null);
469
+ return waitForScriptLoad(j);
470
+ }
471
+ else {
472
+ if (!win.gtag) {
473
+ win.gtag = function (..._args) { win.dataLayer.push(arguments); };
474
+ }
475
+ win.gtag('js', new Date());
476
+ // Enable cross-domain tracking: accept incoming _gl parameter from referring
477
+ // domains (e.g. Shopify store → TagadaPay checkout) so Google preserves the
478
+ // client-id/session across the redirect.
479
+ const linkerConfig = { accept_incoming: true };
480
+ try {
481
+ const ref = document.referrer && new URL(document.referrer).hostname;
482
+ if (ref && ref !== window.location.hostname) {
483
+ linkerConfig.domains = [ref, window.location.hostname];
484
+ }
485
+ }
486
+ catch { /* ignore invalid referrer */ }
487
+ win.gtag('config', containerId, { linker: linkerConfig });
488
+ const script = document.createElement('script');
489
+ script.async = true;
490
+ script.src = 'https://www.googletagmanager.com/gtag/js?id=' + containerId;
491
+ const firstScript = document.getElementsByTagName('script')[0];
492
+ if (firstScript?.parentNode)
493
+ firstScript.parentNode.insertBefore(script, firstScript);
494
+ else
495
+ (document.head || document.getElementsByTagName('head')[0])?.appendChild(script);
496
+ return waitForScriptLoad(script);
497
+ }
498
+ }
499
+ /* eslint-enable @typescript-eslint/no-explicit-any */
500
+ // ---------------------------------------------------------------------------
501
+ // Tracker factory
502
+ // ---------------------------------------------------------------------------
503
+ // ---------------------------------------------------------------------------
504
+ // Config sanitisation (TAG-1871)
505
+ // ---------------------------------------------------------------------------
506
+ /** Longest ID we accept — real IDs top out around 20 chars. */
507
+ const MAX_PIXEL_ID_LENGTH = 64;
508
+ /**
509
+ * The only shape any supported pixel / container / project ID takes: Meta
510
+ * digits, TikTok alphanumerics, Snapchat alphanumerics + dashes, Pinterest
511
+ * digits, Clarity project ids and `GTM-`/`AW-`/`G-` containers are all drawn
512
+ * from `[A-Za-z0-9_-]`.
513
+ */
514
+ const SAFE_PIXEL_ID_PATTERN = new RegExp(`^[A-Za-z0-9_-]{1,${MAX_PIXEL_ID_LENGTH}}$`);
515
+ /** True when `value` is a single, well-formed pixel / container ID. */
516
+ export function isSafePixelId(value) {
517
+ return typeof value === 'string' && SAFE_PIXEL_ID_PATTERN.test(value.trim());
518
+ }
519
+ /**
520
+ * Trim every pixel/container ID and DROP the ones that aren't plausibly IDs.
521
+ *
522
+ * The trim keeps per-pixel event targeting (TikTok `ttq.instance`) working when
523
+ * the CRM stored stray whitespace. The drop is the load-bearing part: a
524
+ * merchant once pasted a whole TikTok `<script>` snippet into the Pixel ID
525
+ * field, the CRM split it on every comma into 56 "pixels", and handing those
526
+ * fragments to `ttq.load()` sent TikTok's SDK into a hot loop that pinned the
527
+ * renderer at 100% CPU *before first paint* — a blank checkout for days.
528
+ *
529
+ * The CRM now rejects such values at input, but this guard is deliberately
530
+ * independent: it neutralises configs that are ALREADY polluted, on pages that
531
+ * were baked before the fix, with no data migration and no re-publish.
532
+ */
533
+ function sanitizePixels(pixels) {
534
+ const out = {};
535
+ const dropped = [];
536
+ for (const [provider, list] of Object.entries(pixels)) {
537
+ if (!Array.isArray(list))
538
+ continue;
539
+ const kept = [];
540
+ for (const px of list) {
541
+ // GTM keys off containerId, Clarity off projectId, everyone else pixelId.
542
+ const key = pixelIdKeyOf(px);
543
+ const raw = px?.[key];
544
+ if (!isSafePixelId(raw)) {
545
+ dropped.push(raw);
546
+ continue;
547
+ }
548
+ kept.push({ ...px, [key]: raw.trim() });
549
+ }
550
+ out[provider] = kept;
551
+ }
552
+ if (dropped.length > 0) {
553
+ console.warn(`[SDK Pixels] Ignored ${dropped.length} malformed pixel ID(s) — tracking for those entries is disabled.`, dropped);
554
+ }
555
+ return out;
556
+ }
557
+ /**
558
+ * Resolve once the browser has had a chance to paint.
559
+ *
560
+ * Third-party pixel SDKs run arbitrary vendor code on the main thread and can
561
+ * stall it (TikTok's did, indefinitely — TAG-1871). Injecting them before first
562
+ * paint turns any such stall into a blank page. Deferring past the first frame
563
+ * means the worst case degrades to "no tracking" instead of "no checkout".
564
+ *
565
+ * Double-rAF is the standard "after paint" signal. The timer is a fallback for
566
+ * backgrounded tabs (where rAF may never fire) and for non-browser environments
567
+ * that expose a `window` without `requestAnimationFrame`.
568
+ */
569
+ const PAINT_FALLBACK_MS = 1500;
570
+ function afterFirstPaint() {
571
+ if (typeof window === 'undefined')
572
+ return Promise.resolve();
573
+ return new Promise((resolve) => {
574
+ let settled = false;
575
+ const finish = () => {
576
+ if (settled)
577
+ return;
578
+ settled = true;
579
+ clearTimeout(fallback);
580
+ resolve();
581
+ };
582
+ const raf = typeof win?.requestAnimationFrame === 'function'
583
+ ? win.requestAnimationFrame.bind(win)
584
+ : undefined;
585
+ const fallback = setTimeout(finish, raf ? PAINT_FALLBACK_MS : 0);
586
+ if (raf)
587
+ raf(() => raf(finish));
588
+ });
589
+ }
590
+ /**
591
+ * Create a pixel tracker for a given pixels config.
592
+ * Kicks off pixel script init once the first frame has painted, fires PageView
593
+ * when init resolves, and returns a `track()` function that no-ops until then.
594
+ */
595
+ export function createPixelTracker(rawPixels, opts) {
596
+ const pixels = rawPixels ? sanitizePixels(rawPixels) : rawPixels;
597
+ const shouldTrackEvent = createDuplicateGuard(5000);
598
+ let pixelsInitialized = false;
599
+ const initPromise = (async () => {
600
+ if (!pixels || typeof window === 'undefined')
601
+ return;
602
+ // Never inject a third-party tag before the page has painted (TAG-1871).
603
+ await afterFirstPaint();
604
+ const loadPromises = [];
605
+ try {
606
+ pixels.facebook?.forEach((px) => { if (px.enabled && px.pixelId)
607
+ loadPromises.push(initMetaPixel(px.pixelId)); });
608
+ pixels.tiktok?.forEach((px) => { if (px.enabled && px.pixelId)
609
+ loadPromises.push(initTikTokPixel(px.pixelId)); });
610
+ pixels.snapchat?.forEach((px) => { if (px.enabled && px.pixelId)
611
+ loadPromises.push(initSnapchatPixel(px.pixelId)); });
612
+ pixels.pinterest?.forEach((px) => { if (px.enabled && px.pixelId)
613
+ loadPromises.push(initPinterestPixel(px.pixelId)); });
614
+ pixels.gtm?.forEach((px) => { if (px.enabled && px.containerId)
615
+ loadPromises.push(initGTM(px.containerId)); });
616
+ pixels.clarity?.forEach((px) => { if (px.enabled && px.projectId)
617
+ loadPromises.push(initClarity(px.projectId)); });
618
+ await Promise.all(loadPromises);
619
+ }
620
+ catch (error) {
621
+ console.error('[SDK Pixels] Initialization error:', error);
622
+ }
623
+ pixelsInitialized = true;
624
+ })();
625
+ // Advanced matching: feed customer identity to Meta (fbq init, raw — Meta
626
+ // hashes), Snapchat (re-init, hashed) + TikTok (identify, hashed). Applied
627
+ // once per identity and cached, so repeated track()/identify() calls don't
628
+ // re-apply. Each provider persists the match keys onto every subsequent
629
+ // event it fires, so calling this as soon as the email is known lifts match
630
+ // quality on InitiateCheckout + Purchase alike. `_identityReady` is awaited
631
+ // before Snap/TikTok events fire so the hashed keys are attached in time.
632
+ let _identityKey = null;
633
+ let _identityReady = null;
634
+ const applyIdentity = (id) => {
635
+ if (!pixels)
636
+ return null;
637
+ if (!id.email && !id.phone && !id.externalId)
638
+ return null;
639
+ const key = `${id.email ?? ''}|${id.phone ?? ''}|${id.externalId ?? ''}`;
640
+ if (_identityKey === key && _identityReady)
641
+ return _identityReady;
642
+ _identityKey = key;
643
+ // Meta takes raw values and hashes in-browser — apply immediately, no need
644
+ // to await our own hashing (and it works even without SubtleCrypto).
645
+ pixels.facebook?.forEach((px) => {
646
+ if (px.enabled && px.pixelId)
647
+ applyMetaIdentity(win, px.pixelId, id);
648
+ });
649
+ // Google Ads enhanced conversions: gtag('set','user_data') persists onto
650
+ // every subsequent gtag event, including the send_to conversion. Raw values,
651
+ // Google hashes in-browser (same rationale as Meta).
652
+ if (pixels.gtm?.some((px) => px.enabled)) {
653
+ applyGoogleIdentity(win, { email_address: id.email, phone_number: id.phone });
654
+ }
655
+ // Snapchat + TikTok want pre-hashed SHA-256 values, which we compute
656
+ // ourselves so raw PII never leaves the page. Skips when no secure context.
657
+ if (!canHashIdentity())
658
+ return (_identityReady = Promise.resolve());
659
+ // Always resolves (errors are swallowed) so deferred event fires never hang.
660
+ _identityReady = (async () => {
661
+ try {
662
+ const hashed = await buildHashedIdentity(id);
663
+ pixels.snapchat?.forEach((px) => {
664
+ if (px.enabled && px.pixelId)
665
+ applySnapchatIdentity(win, px.pixelId, hashed);
666
+ });
667
+ pixels.tiktok?.forEach((px) => {
668
+ if (px.enabled && px.pixelId)
669
+ applyTikTokIdentity(win, px.pixelId, hashed);
670
+ });
671
+ }
672
+ catch (error) {
673
+ console.error('[SDK Pixels] identify error:', error);
674
+ }
675
+ })();
676
+ return _identityReady;
677
+ };
678
+ // Derive identity from the GA4 enrichment the thank-you page already attaches
679
+ // (`_ga4.user_data`), so Purchase events get advanced matching with no
680
+ // call-site changes. Returns the readiness promise, or null when absent.
681
+ const applyIdentityFromParams = (parameters) => {
682
+ const ga4 = parameters[GA4_PARAM_KEY];
683
+ const ud = ga4?.user_data;
684
+ if (!ud)
685
+ return null;
686
+ // Google enhanced conversions get the FULL user_data (incl. address) here,
687
+ // richer than the email/phone identity below — and this runs synchronously
688
+ // before the gtag('event') fires in track(), which is what Google requires.
689
+ // Bypasses the identity dedup on purpose: gtag('set') is idempotent and the
690
+ // address may not have been known at identify() time.
691
+ if (pixels?.gtm?.some((px) => px.enabled))
692
+ applyGoogleIdentity(win, ud);
693
+ return applyIdentity({
694
+ email: ud.email_address,
695
+ phone: ud.phone_number,
696
+ externalId: ga4?.user_id,
697
+ });
698
+ };
699
+ const track = (eventName, parameters = {}, options = {}) => {
700
+ if (!pixels || !pixelsInitialized)
701
+ return;
702
+ if (!shouldTrackEvent(eventName, parameters, options.eventId))
703
+ return;
704
+ try {
705
+ // Apply advanced matching before firing so Snap/TikTok carry the match
706
+ // keys on this event. Other providers fire immediately (unaffected).
707
+ const identityReady = applyIdentityFromParams(parameters);
708
+ const events = resolvePixelEvents(pixels, eventName, parameters);
709
+ // Deduplicate by provider: the Snapchat/Pinterest SDKs broadcast to all
710
+ // registered pixel IDs internally, so we only need to fire once per
711
+ // provider. GTM, TikTok and Meta need per-pixel firing (GTM for Google Ads
712
+ // send_to targeting, TikTok because ttq.instance() targets a specific
713
+ // pixel, Meta because fbq('trackSingle') does — relying on Meta's
714
+ // broadcast instead leaked our events onto pixels a pasted script had
715
+ // initialised).
716
+ const firedProviders = new Set();
717
+ // Per-pixel providers still need per-pixel dedup: a merchant field of
718
+ // "123;123" expands into two entries with the same id (splitPixelConfig),
719
+ // which would otherwise fire the same event twice at the same pixel.
720
+ const firedPixels = new Set();
721
+ for (const { provider, mapped, pixel } of events) {
722
+ // Gate Snap/TikTok on identity application (when present) so the match
723
+ // keys are set first; a few ms of deferral, only for those providers.
724
+ const emit = identityReady && (provider === 'snapchat' || provider === 'tiktok')
725
+ ? (fn) => { identityReady.then(fn); }
726
+ : (fn) => { fn(); };
727
+ if (provider === 'gtm' || provider === 'tiktok' || provider === 'facebook') {
728
+ const pixelKey = `${provider}:${pixelIdOf(pixel) ?? ''}`;
729
+ if (firedPixels.has(pixelKey))
730
+ continue;
731
+ firedPixels.add(pixelKey);
732
+ emit(() => fire(provider, mapped.name, mapped.params, pixel, options));
733
+ }
734
+ else {
735
+ if (!firedProviders.has(provider)) {
736
+ emit(() => fire(provider, mapped.name, mapped.params, undefined, options));
737
+ firedProviders.add(provider);
738
+ }
739
+ }
740
+ }
741
+ }
742
+ catch (error) {
743
+ console.error('[SDK Pixels] Tracking error:', error);
744
+ }
745
+ };
746
+ // Auto page-view: fire once after init resolves.
747
+ if (opts?.autoPageView !== false)
748
+ initPromise.then(() => {
749
+ if (typeof window === 'undefined')
750
+ return;
751
+ track('PageView', { path: window.location.pathname });
752
+ });
753
+ return {
754
+ initPromise,
755
+ get pixelsInitialized() { return pixelsInitialized; },
756
+ track,
757
+ identify: (identity) => { applyIdentity(identity); },
758
+ };
759
+ }
760
+ function readPixelsFromStepConfig() {
761
+ if (typeof window === 'undefined')
762
+ return undefined;
763
+ const raw = window.__TGD_STEP_CONFIG__;
764
+ if (!raw)
765
+ return undefined;
766
+ try {
767
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
768
+ const pixels = parsed?.pixels;
769
+ return pixels && typeof pixels === 'object' ? pixels : undefined;
770
+ }
771
+ catch {
772
+ return undefined;
773
+ }
774
+ }
775
+ /**
776
+ * One-shot bootstrap: reads `window.__TGD_STEP_CONFIG__.pixels`, creates a
777
+ * tracker, kicks off init, and stashes it on `window.__TGD_PIXEL_TRACKER__`.
778
+ *
779
+ * Idempotent — both the islands SDK and the full-SPA runtime may call this,
780
+ * but only the first call creates a tracker. Returns the active tracker
781
+ * (existing or newly created), or null if there's no window or no pixels.
782
+ */
783
+ export function bootstrapPixelTrackerFromWindow() {
784
+ if (typeof window === 'undefined')
785
+ return null;
786
+ if (window.__TGD_PIXEL_TRACKER__)
787
+ return window.__TGD_PIXEL_TRACKER__;
788
+ const pixels = readPixelsFromStepConfig();
789
+ if (!pixels)
790
+ return null;
791
+ const tracker = createPixelTracker(pixels);
792
+ window.__TGD_PIXEL_TRACKER__ = tracker;
793
+ return tracker;
794
+ }
795
+ //# sourceMappingURL=pixelTracker.js.map