@tagadapay/plugin-sdk 4.0.6 → 4.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.
Files changed (43) hide show
  1. package/README.md +25 -46
  2. package/dist/external-tracker.js +103 -36
  3. package/dist/external-tracker.min.js +2 -2
  4. package/dist/external-tracker.min.js.map +3 -3
  5. package/dist/react/types.d.ts +2 -2
  6. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  7. package/dist/tagada-react-sdk-minimal.min.js.map +3 -3
  8. package/dist/tagada-react-sdk.js +148 -28
  9. package/dist/tagada-react-sdk.min.js +2 -2
  10. package/dist/tagada-react-sdk.min.js.map +4 -4
  11. package/dist/tagada-sdk.js +125 -45
  12. package/dist/tagada-sdk.min.js +2 -2
  13. package/dist/tagada-sdk.min.js.map +4 -4
  14. package/dist/v2/core/funnelClient.js +14 -9
  15. package/dist/v2/core/pixelMapping.d.ts +84 -0
  16. package/dist/v2/core/pixelMapping.js +102 -0
  17. package/dist/v2/core/pixelTracker.d.ts +1 -6
  18. package/dist/v2/core/pixelTracker.js +36 -2
  19. package/dist/v2/core/resources/credits.d.ts +13 -0
  20. package/dist/v2/core/resources/credits.js +7 -0
  21. package/dist/v2/core/resources/offers.d.ts +5 -1
  22. package/dist/v2/core/resources/offers.js +3 -2
  23. package/dist/v2/core/resources/payments.d.ts +1 -0
  24. package/dist/v2/core/resources/payments.js +1 -0
  25. package/dist/v2/core/types.d.ts +17 -2
  26. package/dist/v2/core/utils/authHandoff.d.ts +2 -1
  27. package/dist/v2/index.d.ts +3 -1
  28. package/dist/v2/index.js +4 -1
  29. package/dist/v2/react/components/FunnelScriptInjector.js +42 -7
  30. package/dist/v2/react/hooks/useAuth.d.ts +1 -0
  31. package/dist/v2/react/hooks/useAuth.js +1 -0
  32. package/dist/v2/react/hooks/useClubOffers.d.ts +16 -0
  33. package/dist/v2/react/hooks/useClubOffers.js +29 -3
  34. package/dist/v2/react/hooks/useCustomer.d.ts +1 -0
  35. package/dist/v2/react/hooks/useCustomer.js +1 -0
  36. package/dist/v2/react/hooks/useStore.d.ts +5 -0
  37. package/dist/v2/react/hooks/useStore.js +16 -0
  38. package/dist/v2/react/index.d.ts +1 -0
  39. package/dist/v2/react/index.js +1 -0
  40. package/dist/v2/standalone/index.js +134 -46
  41. package/dist/v2/standalone/payment-service.d.ts +2 -1
  42. package/dist/v2/standalone/payment-service.js +6 -4
  43. package/package.json +113 -115
@@ -352,16 +352,21 @@ export function getAssignedScripts(position) {
352
352
  * Split a pixel config with semicolon/comma-separated IDs into individual configs.
353
353
  * Handles cases where the CRM stores "ID1; ID2; ID3" as a single pixelId or containerId.
354
354
  */
355
+ // Split "ID1; ID2, ID3" into trimmed, non-empty IDs. Always trims, so even a
356
+ // single ID with leading/trailing whitespace is cleaned.
357
+ function splitIds(rawId) {
358
+ if (!rawId)
359
+ return [];
360
+ return rawId.split(/[;,]/).map((id) => id.trim()).filter((id) => id.length > 0);
361
+ }
355
362
  function splitPixelConfig(px) {
356
- // GTM configs use containerId
357
- const idField = 'containerId' in px ? 'containerId' : 'pixelId';
358
- const rawId = px[idField];
359
- if (!rawId || !rawId.includes(';') && !rawId.includes(','))
360
- return [px];
361
- const ids = rawId.split(/[;,]/).map((id) => id.trim()).filter((id) => id.length > 0);
362
- if (ids.length <= 1)
363
- return [px];
364
- return ids.map((id) => ({ ...px, [idField]: id }));
363
+ // GTM configs key off containerId; every other provider uses pixelId.
364
+ if ('containerId' in px) {
365
+ const ids = splitIds(px.containerId);
366
+ return ids.length === 0 ? [px] : ids.map((id) => ({ ...px, containerId: id }));
367
+ }
368
+ const ids = splitIds(px.pixelId);
369
+ return ids.length === 0 ? [px] : ids.map((id) => ({ ...px, pixelId: id }));
365
370
  }
366
371
  export function getAssignedPixels() {
367
372
  const stepConfig = getAssignedStepConfig();
@@ -18,6 +18,89 @@ export interface MappedEvent {
18
18
  name: string;
19
19
  params: Record<string, unknown>;
20
20
  }
21
+ export declare const GA4_PARAM_KEY: "_ga4";
22
+ export interface Ga4UserData {
23
+ email_address?: string;
24
+ phone_number?: string;
25
+ address?: {
26
+ first_name?: string;
27
+ last_name?: string;
28
+ street?: string;
29
+ city?: string;
30
+ region?: string;
31
+ postal_code?: string;
32
+ country?: string;
33
+ };
34
+ }
35
+ export interface Ga4Item {
36
+ item_id?: string;
37
+ item_name?: string;
38
+ item_variant?: string;
39
+ /** Minor units (cents). Converted to major by mapGTMEvent. */
40
+ price?: number;
41
+ quantity?: number;
42
+ }
43
+ export interface Ga4PurchasePayload {
44
+ user_id?: string;
45
+ user_data?: Ga4UserData;
46
+ /** Minor units (cents). */
47
+ value?: number;
48
+ /** Minor units (cents). */
49
+ tax?: number;
50
+ /** Minor units (cents). */
51
+ shipping?: number;
52
+ items: Ga4Item[];
53
+ }
54
+ /** Loose order shape accepted by buildGa4PurchasePayload (subset of SDK Order). */
55
+ export interface Ga4OrderInput {
56
+ id?: string;
57
+ currency?: string;
58
+ paidAmount?: number;
59
+ items?: Array<{
60
+ productId?: string;
61
+ variantId?: string | null;
62
+ quantity?: number;
63
+ adjustedAmount?: number;
64
+ orderLineItemProduct?: {
65
+ name?: string;
66
+ };
67
+ orderLineItemVariant?: {
68
+ name?: string;
69
+ };
70
+ }>;
71
+ summaries?: Array<{
72
+ currency?: string;
73
+ totalTaxAmount?: number;
74
+ shippingCost?: number;
75
+ totalAdjustedAmount?: number;
76
+ }>;
77
+ customer?: {
78
+ id?: string;
79
+ email?: string;
80
+ phone?: string;
81
+ firstName?: string;
82
+ lastName?: string;
83
+ };
84
+ billingAddress?: Ga4Address;
85
+ shippingAddress?: Ga4Address;
86
+ }
87
+ interface Ga4Address {
88
+ firstName?: string;
89
+ lastName?: string;
90
+ address1?: string;
91
+ city?: string;
92
+ state?: string;
93
+ postal?: string;
94
+ country?: string;
95
+ phone?: string;
96
+ }
97
+ /**
98
+ * Build the GA4-only enrichment payload (`_ga4`) from an order. Pure, defensive,
99
+ * framework-agnostic — shared by the native ThankYouPage and the Studio
100
+ * OrderConfirmation island so both emit an identical canonical GA4 purchase.
101
+ * Returns minor-unit amounts; mapGTMEvent converts them to major units.
102
+ */
103
+ export declare function buildGa4PurchasePayload(order: Ga4OrderInput): Ga4PurchasePayload;
21
104
  /**
22
105
  * Returns `true` if the given event is enabled on the pixel config.
23
106
  * If the pixel has no `events` map (e.g. MetaConversionTrackingConfig) we allow all.
@@ -47,3 +130,4 @@ export interface ProviderEvent {
47
130
  * for every eligible (enabled + event-toggled-on) pixel.
48
131
  */
49
132
  export declare function resolvePixelEvents(pixels: PixelsConfig, eventName: StandardPixelEvent, parameters: Record<string, unknown>): ProviderEvent[];
133
+ export {};
@@ -12,6 +12,71 @@
12
12
  * GTM/GA4: gtag('event', 'purchase', { value, currency, items })
13
13
  */
14
14
  // ---------------------------------------------------------------------------
15
+ // GA4-only enrichment
16
+ //
17
+ // Canonical GA4 ecommerce `purchase` data carries user identity + a nested
18
+ // `ecommerce` object that the flat Meta/TikTok payload does not. We attach it
19
+ // to the shared track() params under the reserved `_ga4` key. ONLY mapGTMEvent
20
+ // reads it; `baseTransform` strips it so it can never leak into the
21
+ // Meta/TikTok/Snapchat/Pinterest payloads (those would otherwise spread it,
22
+ // shipping unhashed PII to those pixels). All monetary fields are in MINOR
23
+ // units here — mapGTMEvent converts to major via the same currency helpers.
24
+ // ---------------------------------------------------------------------------
25
+ export const GA4_PARAM_KEY = '_ga4';
26
+ /**
27
+ * Build the GA4-only enrichment payload (`_ga4`) from an order. Pure, defensive,
28
+ * framework-agnostic — shared by the native ThankYouPage and the Studio
29
+ * OrderConfirmation island so both emit an identical canonical GA4 purchase.
30
+ * Returns minor-unit amounts; mapGTMEvent converts them to major units.
31
+ */
32
+ export function buildGa4PurchasePayload(order) {
33
+ const items = (order.items ?? []).map((item) => ({
34
+ item_id: item.productId || undefined,
35
+ // GA4 convention: item_name = product, item_variant = variant.
36
+ item_name: item.orderLineItemProduct?.name || item.orderLineItemVariant?.name || undefined,
37
+ item_variant: item.orderLineItemVariant?.name || undefined,
38
+ price: item.adjustedAmount,
39
+ quantity: item.quantity,
40
+ }));
41
+ const summary = order.summaries?.find((s) => s.currency === order.currency) ?? order.summaries?.[0];
42
+ // Prefer the captured paid amount; fall back to the summary total, then to
43
+ // the sum of line items. Guards against the value:0 seen in the field.
44
+ const itemsSum = (order.items ?? []).reduce((sum, i) => sum + (i.adjustedAmount ?? 0) * (i.quantity ?? 0), 0);
45
+ const value = order.paidAmount && order.paidAmount > 0
46
+ ? order.paidAmount
47
+ : (summary?.totalAdjustedAmount ?? itemsSum);
48
+ const payload = { items, value };
49
+ if (summary?.totalTaxAmount != null)
50
+ payload.tax = summary.totalTaxAmount;
51
+ if (summary?.shippingCost != null)
52
+ payload.shipping = summary.shippingCost;
53
+ if (order.customer?.id)
54
+ payload.user_id = order.customer.id;
55
+ const addr = order.billingAddress ?? order.shippingAddress;
56
+ const userData = {};
57
+ if (order.customer?.email)
58
+ userData.email_address = order.customer.email;
59
+ const phone = order.customer?.phone || addr?.phone;
60
+ if (phone)
61
+ userData.phone_number = phone;
62
+ if (addr) {
63
+ const address = {
64
+ first_name: addr.firstName || order.customer?.firstName || undefined,
65
+ last_name: addr.lastName || order.customer?.lastName || undefined,
66
+ street: addr.address1 || undefined,
67
+ city: addr.city || undefined,
68
+ region: addr.state || undefined,
69
+ postal_code: addr.postal || undefined,
70
+ country: addr.country || undefined,
71
+ };
72
+ if (Object.values(address).some((v) => v != null))
73
+ userData.address = address;
74
+ }
75
+ if (Object.keys(userData).length > 0)
76
+ payload.user_data = userData;
77
+ return payload;
78
+ }
79
+ // ---------------------------------------------------------------------------
15
80
  // Currency conversion helper (inline, zero-dep)
16
81
  // ---------------------------------------------------------------------------
17
82
  /**
@@ -109,6 +174,12 @@ function baseTransform(params) {
109
174
  p = convertValueToMajor(p);
110
175
  p = convertMonetaryFieldsToMajor(p);
111
176
  p = convertContentsPricesToMajor(p);
177
+ // Strip GA4-only enrichment so it never reaches Meta/TikTok/Snapchat/Pinterest
178
+ // (those mappers spread all params). mapGTMEvent reads it from the raw input.
179
+ if (GA4_PARAM_KEY in p) {
180
+ p = { ...p };
181
+ delete p[GA4_PARAM_KEY];
182
+ }
112
183
  return p;
113
184
  }
114
185
  // ---------------------------------------------------------------------------
@@ -291,6 +362,37 @@ const GTM_EVENT_MAP = {
291
362
  };
292
363
  export function mapGTMEvent(eventName, parameters) {
293
364
  const name = GTM_EVENT_MAP[eventName] ?? eventName.toLowerCase();
365
+ // Canonical GA4 ecommerce shape: when the caller attached `_ga4` enrichment
366
+ // (purchase from the thank-you page) emit a nested `ecommerce` object plus
367
+ // user identity instead of the legacy flat payload. baseTransform strips
368
+ // `_ga4`, so read it from the raw input first.
369
+ const ga4 = parameters[GA4_PARAM_KEY];
370
+ if (ga4) {
371
+ const currency = typeof parameters.currency === 'string' ? parameters.currency.toUpperCase() : undefined;
372
+ const toMajor = (v) => v == null || !currency ? v : minorToMajor(Number(v), currency);
373
+ const ecommerce = {
374
+ transaction_id: parameters.transaction_id,
375
+ currency,
376
+ value: toMajor(ga4.value),
377
+ };
378
+ if (ga4.tax != null)
379
+ ecommerce.tax = toMajor(ga4.tax);
380
+ if (ga4.shipping != null)
381
+ ecommerce.shipping = toMajor(ga4.shipping);
382
+ ecommerce.items = (ga4.items ?? []).map((item) => ({
383
+ item_id: item.item_id,
384
+ item_name: item.item_name,
385
+ item_variant: item.item_variant,
386
+ price: toMajor(item.price),
387
+ quantity: item.quantity != null ? Number(item.quantity) : undefined,
388
+ }));
389
+ const out = { ecommerce };
390
+ if (ga4.user_id)
391
+ out.user_id = ga4.user_id;
392
+ if (ga4.user_data)
393
+ out.user_data = ga4.user_data;
394
+ return { name, params: out };
395
+ }
294
396
  const params = baseTransform(parameters);
295
397
  if (params.num_items !== undefined) {
296
398
  params.num_items = Number(params.num_items);
@@ -28,12 +28,7 @@ export interface PixelTracker {
28
28
  /** Track a standard pixel event. No-ops until init resolves. */
29
29
  track(eventName: StandardPixelEvent, parameters?: Record<string, unknown>, options?: TrackOptions): void;
30
30
  }
31
- /**
32
- * Create a pixel tracker for a given pixels config.
33
- * Kicks off pixel script init immediately, fires PageView once init resolves,
34
- * and returns a `track()` function that no-ops until init completes.
35
- */
36
- export declare function createPixelTracker(pixels: PixelsConfig | undefined): PixelTracker;
31
+ export declare function createPixelTracker(rawPixels: PixelsConfig | undefined): PixelTracker;
37
32
  declare global {
38
33
  interface Window {
39
34
  __TGD_PIXEL_TRACKER__?: PixelTracker;
@@ -86,10 +86,23 @@ function fire(provider, name, params, pixel, options = {}) {
86
86
  function fireGTM(event, params) {
87
87
  try {
88
88
  const w = window;
89
+ const hasEcommerce = params && typeof params === 'object' && 'ecommerce' in params;
89
90
  if (w.gtag) {
90
- w.gtag('event', event, params);
91
+ // gtag.js (GA4 config tag) reads ecommerce fields at the top level of the
92
+ // event, not under a nested `ecommerce` key — flatten it back out.
93
+ if (hasEcommerce) {
94
+ const { ecommerce, ...rest } = params;
95
+ w.gtag('event', event, { ...rest, ...ecommerce });
96
+ }
97
+ else {
98
+ w.gtag('event', event, params);
99
+ }
91
100
  }
92
101
  else if (w.dataLayer) {
102
+ // GA4 best practice: clear the previous ecommerce object before pushing a
103
+ // new one so item arrays from earlier events don't bleed through.
104
+ if (hasEcommerce)
105
+ w.dataLayer.push({ ecommerce: null });
93
106
  w.dataLayer.push({ event, ...params });
94
107
  }
95
108
  }
@@ -323,7 +336,28 @@ function initGTM(containerId) {
323
336
  * Kicks off pixel script init immediately, fires PageView once init resolves,
324
337
  * and returns a `track()` function that no-ops until init completes.
325
338
  */
326
- export function createPixelTracker(pixels) {
339
+ /**
340
+ * Trim leading/trailing whitespace from every pixel/container ID. Mutating a
341
+ * copy here means both init and per-pixel event targeting (TikTok ttq.instance)
342
+ * use the clean ID, even when the CRM stored an ID with stray spaces.
343
+ */
344
+ function trimPixelIds(pixels) {
345
+ const out = {};
346
+ for (const [provider, list] of Object.entries(pixels)) {
347
+ if (!Array.isArray(list))
348
+ continue;
349
+ out[provider] = list.map((px) => {
350
+ // GTM configs key off containerId; every other provider uses pixelId.
351
+ if ('containerId' in px) {
352
+ return typeof px.containerId === 'string' ? { ...px, containerId: px.containerId.trim() } : px;
353
+ }
354
+ return typeof px.pixelId === 'string' ? { ...px, pixelId: px.pixelId.trim() } : px;
355
+ });
356
+ }
357
+ return out;
358
+ }
359
+ export function createPixelTracker(rawPixels) {
360
+ const pixels = rawPixels ? trimPixelIds(rawPixels) : rawPixels;
327
361
  const shouldTrackEvent = createDuplicateGuard(5000);
328
362
  let pixelsInitialized = false;
329
363
  const initPromise = (async () => {
@@ -24,6 +24,12 @@ export interface RedeemProductResponse {
24
24
  creditsSpent: number;
25
25
  remainingCredits: number;
26
26
  }
27
+ export interface RedeemOfferResponse {
28
+ success: boolean;
29
+ orderId: string;
30
+ creditsSpent: number;
31
+ remainingCredits: number;
32
+ }
27
33
  export declare class CreditsResource {
28
34
  private apiClient;
29
35
  constructor(apiClient: ApiClient);
@@ -39,4 +45,11 @@ export declare class CreditsResource {
39
45
  variantId: string;
40
46
  quantity?: number;
41
47
  }): Promise<RedeemProductResponse>;
48
+ /**
49
+ * Redeem an offer (its full bundle of line items) using credits.
50
+ * customerId/storeId/accountId are derived server-side from the verified session.
51
+ */
52
+ redeemOffer(data: {
53
+ offerId: string;
54
+ }): Promise<RedeemOfferResponse>;
42
55
  }
@@ -18,4 +18,11 @@ export class CreditsResource {
18
18
  async redeemProduct(data) {
19
19
  return this.apiClient.post('/api/v1/credits/redeem', data);
20
20
  }
21
+ /**
22
+ * Redeem an offer (its full bundle of line items) using credits.
23
+ * customerId/storeId/accountId are derived server-side from the verified session.
24
+ */
25
+ async redeemOffer(data) {
26
+ return this.apiClient.post('/api/v1/credits/redeem-offer', data);
27
+ }
21
28
  }
@@ -225,6 +225,7 @@ export declare class OffersResource {
225
225
  productId?: string;
226
226
  variantId: string;
227
227
  quantity: number;
228
+ priceId?: string;
228
229
  }>): Promise<any>;
229
230
  /**
230
231
  * Preview and pay for an offer with variant selections
@@ -240,6 +241,7 @@ export declare class OffersResource {
240
241
  productId?: string;
241
242
  variantId: string;
242
243
  quantity: number;
244
+ priceId?: string;
243
245
  }>, returnUrl?: string, mainOrderId?: string, initiatedBy?: 'merchant' | 'customer'): Promise<{
244
246
  preview: any;
245
247
  checkout: {
@@ -262,6 +264,7 @@ export declare class OffersResource {
262
264
  productId?: string;
263
265
  variantId: string;
264
266
  quantity: number;
267
+ priceId?: string;
265
268
  }>, returnUrl?: string, mainOrderId?: string): Promise<{
266
269
  checkoutSessionId?: string;
267
270
  checkoutToken?: string;
@@ -290,7 +293,7 @@ export declare class OffersResource {
290
293
  /**
291
294
  * Pay for an offer directly
292
295
  */
293
- payOffer(offerId: string, orderId?: string, initiatedBy?: 'merchant' | 'customer'): Promise<any>;
296
+ payOffer(offerId: string, orderId?: string, initiatedBy?: 'merchant' | 'customer', customerId?: string): Promise<any>;
294
297
  /**
295
298
  * Transform offer to checkout session with dynamic variant selection
296
299
  * Uses lineItems from the offer to create a new checkout session
@@ -324,6 +327,7 @@ export declare class OffersResource {
324
327
  productId?: string;
325
328
  variantId: string;
326
329
  quantity: number;
330
+ priceId?: string;
327
331
  }>, returnUrl?: string, mainOrderId?: string): Promise<{
328
332
  checkoutToken: string;
329
333
  customerId: string;
@@ -173,14 +173,15 @@ export class OffersResource {
173
173
  /**
174
174
  * Pay for an offer directly
175
175
  */
176
- async payOffer(offerId, orderId, initiatedBy) {
176
+ async payOffer(offerId, orderId, initiatedBy, customerId) {
177
177
  // 🎯 Check draft mode from URL, localStorage, or cookie
178
178
  const draft = isDraftMode();
179
179
  const effectiveInitiatedBy = initiatedBy ?? getAssignedPaymentInitiator();
180
180
  return this.apiClient.post(`/api/v1/offers/${offerId}/pay`, {
181
181
  offerId,
182
- draft, // 🎯 Use dynamic draft mode instead of hardcoded false
182
+ draft,
183
183
  returnUrl: typeof window !== 'undefined' ? window.location.href : '',
184
+ ...(customerId ? { customerId } : {}),
184
185
  ...(effectiveInitiatedBy ? { initiatedBy: effectiveInitiatedBy } : {}),
185
186
  metadata: orderId ? {
186
187
  comingFromPostPurchase: true,
@@ -254,6 +254,7 @@ export declare class PaymentsResource {
254
254
  paymentFlowId?: string;
255
255
  processorId?: string;
256
256
  paymentMethod?: string;
257
+ blikCode?: string;
257
258
  isExpress?: boolean;
258
259
  shippingRateId?: string;
259
260
  }): Promise<PaymentResponse>;
@@ -147,6 +147,7 @@ export class PaymentsResource {
147
147
  ...(options.paymentFlowId && { paymentFlowId: options.paymentFlowId }),
148
148
  ...(options.processorId && { processorId: options.processorId }),
149
149
  ...(options.paymentMethod && { paymentMethod: options.paymentMethod }),
150
+ ...(options.blikCode && { blikCode: options.blikCode }),
150
151
  ...(options.isExpress && { isExpress: options.isExpress }),
151
152
  ...(options.shippingRateId && { shippingRateId: options.shippingRateId }),
152
153
  };
@@ -34,9 +34,9 @@ export interface Customer {
34
34
  lastName?: string;
35
35
  phone?: string;
36
36
  isAuthenticated: boolean;
37
- role: 'authenticated' | 'anonymous';
37
+ role: SessionRole;
38
38
  }
39
- export type SessionRole = 'authenticated' | 'anonymous';
39
+ export type SessionRole = 'authenticated' | 'anonymous' | 'verified';
40
40
  export interface Session {
41
41
  sessionId: string;
42
42
  storeId: string;
@@ -63,6 +63,19 @@ export interface Currency {
63
63
  symbol: string;
64
64
  name: string;
65
65
  }
66
+ export interface StoreCreditSettings {
67
+ enabled: boolean;
68
+ defaultCreditsPerRebill?: number;
69
+ creditRatio?: {
70
+ credits: number;
71
+ baseAmount: number;
72
+ };
73
+ creditRewardRatio?: {
74
+ enabled: boolean;
75
+ credits: number;
76
+ baseAmount: number;
77
+ };
78
+ }
66
79
  export interface Store {
67
80
  id: string;
68
81
  accountId: string;
@@ -72,6 +85,8 @@ export interface Store {
72
85
  locale: string;
73
86
  presentmentCurrencies: string[];
74
87
  chargeCurrencies: string[];
88
+ /** Store-level credit system configuration (from session init). */
89
+ creditSettings?: StoreCreditSettings | null;
75
90
  }
76
91
  export interface PickupPoint {
77
92
  id: string;
@@ -11,6 +11,7 @@
11
11
  * 4. Clean the URL (remove authCode)
12
12
  * 5. Return resolved customer and context
13
13
  */
14
+ import { SessionRole } from '../types';
14
15
  export interface AuthHandoffResolveResponse {
15
16
  sessionId: string;
16
17
  token: string;
@@ -19,7 +20,7 @@ export interface AuthHandoffResolveResponse {
19
20
  email?: string;
20
21
  firstName?: string;
21
22
  lastName?: string;
22
- role: 'authenticated' | 'anonymous';
23
+ role: SessionRole;
23
24
  };
24
25
  context: Record<string, unknown>;
25
26
  }
@@ -20,6 +20,8 @@ export * from './core/pathRemapping';
20
20
  export { bootstrapPixelTrackerFromWindow, createPixelTracker, } from './core/pixelTracker';
21
21
  export type { PixelTracker, TrackOptions } from './core/pixelTracker';
22
22
  export type { PixelProviderKey } from './core/pixelMapping';
23
+ export { buildGa4PurchasePayload, GA4_PARAM_KEY } from './core/pixelMapping';
24
+ export type { Ga4PurchasePayload, Ga4Item, Ga4UserData, Ga4OrderInput } from './core/pixelMapping';
23
25
  export { makeMetaEventId } from './core/utils/metaEventId';
24
26
  export type { CheckoutData, CheckoutInitParams, CheckoutLineItem, CheckoutSession, CheckoutSessionPreview, Promotion } from './core/resources/checkout';
25
27
  export type { Order, OrderLineItem } from './core/utils/order';
@@ -33,7 +35,7 @@ export type { ToggleOrderBumpResponse, VipOffer, VipPreviewResponse } from './co
33
35
  export type { StoreConfig } from './core/resources/storeConfig';
34
36
  export { FunnelActionType } from './core/resources/funnel';
35
37
  export type { BackNavigationActionData, CartUpdatedActionData, DirectNavigationActionData, FormSubmitActionData, FunnelContextUpdateRequest, FunnelContextUpdateResponse, FunnelAction as FunnelEvent, FunnelInitializeRequest, FunnelInitializeResponse, FunnelNavigateRequest, FunnelNavigateResponse, FunnelNavigationAction, FunnelNavigationResult, NextAction, OfferAcceptedActionData, OfferDeclinedActionData, PaymentFailedActionData, PaymentSuccessActionData, SimpleFunnelContext } from './core/resources/funnel';
36
- export { ApplePayButton, StripeExpressButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, PreviewModeIndicator, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useApplePayCheckout, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useGooglePayCheckout, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePaymentRetrieve, usePixelTracking, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStepConfig, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers, useSetPaymentMethod, WhopCheckout, useWhopPaymentPolling } from './react';
38
+ export { ApplePayButton, StripeExpressButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, PreviewModeIndicator, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useApplePayCheckout, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useGooglePayCheckout, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePaymentRetrieve, usePixelTracking, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStepConfig, useStore, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers, useSetPaymentMethod, WhopCheckout, useWhopPaymentPolling } from './react';
37
39
  export type { DebugScript } from './react';
38
40
  export type { PaymentMethodName } from './react';
39
41
  export { resolveClickId, publishTrackingGlobal, CLICK_ID_URL_PARAMS, CLICK_ID_COOKIES, } from './core/utils/clickIdResolver';
package/dist/v2/index.js CHANGED
@@ -25,13 +25,16 @@ loadLocalFunnelConfig } from './core/funnelClient';
25
25
  export * from './core/pathRemapping';
26
26
  // Framework-agnostic pixel tracker (Studio entries bootstrap from this)
27
27
  export { bootstrapPixelTrackerFromWindow, createPixelTracker, } from './core/pixelTracker';
28
+ // GA4 canonical purchase enrichment (used by thank-you pages to attach
29
+ // nested-ecommerce + user data the GTM mapper turns into a GA4 `purchase`).
30
+ export { buildGa4PurchasePayload, GA4_PARAM_KEY } from './core/pixelMapping';
28
31
  // Stable event_id helper for Meta CAPI / browser pixel deduplication.
29
32
  // Re-exported here (already exposed via /v2/react) so framework-agnostic
30
33
  // Studio islands can import it from the same entry as bootstrapPixelTracker.
31
34
  export { makeMetaEventId } from './core/utils/metaEventId';
32
35
  export { FunnelActionType } from './core/resources/funnel';
33
36
  // React exports (hooks and components only, types are exported above)
34
- export { ApplePayButton, StripeExpressButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, PreviewModeIndicator, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useApplePayCheckout, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useGooglePayCheckout, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePaymentRetrieve, usePixelTracking, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStepConfig, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers, useSetPaymentMethod, WhopCheckout, useWhopPaymentPolling } from './react';
37
+ export { ApplePayButton, StripeExpressButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, PreviewModeIndicator, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useApplePayCheckout, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useGooglePayCheckout, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePaymentRetrieve, usePixelTracking, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStepConfig, useStore, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers, useSetPaymentMethod, WhopCheckout, useWhopPaymentPolling } from './react';
35
38
  // Click-id resolver (ad-tracker integrations: ClickFlare, Voluum, Binom, …)
36
39
  // Re-exported from core so it's reachable from any entry point.
37
40
  export { resolveClickId, publishTrackingGlobal, CLICK_ID_URL_PARAMS, CLICK_ID_COOKIES, } from './core/utils/clickIdResolver';
@@ -3,10 +3,27 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
3
3
  import { getAssignedStepConfig } from '../../core';
4
4
  import { useCheckoutQuery } from '../hooks/useCheckoutQuery';
5
5
  import { useOrderQuery } from '../hooks/useOrderQuery';
6
+ /**
7
+ * Classify a chunk of text found OUTSIDE <script>/<noscript> tags.
8
+ *
9
+ * Such chunks are HTML markup (e.g. <link rel="preconnect">, <meta>, <img>,
10
+ * <iframe> pixels) far more often than bare JavaScript. Injecting markup as
11
+ * inline JS produces an uncatchable parse-time `SyntaxError: Unexpected token '<'`
12
+ * that aborts the whole <script> — which silently kills any sibling tracking
13
+ * script bundled in the same snippet (e.g. Hyros + TripleWhale). So we only
14
+ * treat a chunk as inline JS when it does NOT look like an HTML tag.
15
+ */
16
+ function classifyLooseChunk(chunk) {
17
+ if (!chunk || /^<!--[\s\S]*?-->$/.test(chunk))
18
+ return null;
19
+ // Opening tag at the start, or any real element/closing tag anywhere.
20
+ const looksLikeMarkup = /^</.test(chunk) || /<\/?[a-zA-Z][\w-]*[\s/>]/.test(chunk);
21
+ return looksLikeMarkup ? { type: 'html', html: chunk } : { type: 'inline', code: chunk };
22
+ }
6
23
  /**
7
24
  * Parse script content that may contain multiple <script> and <noscript> tags.
8
25
  * Handles: external scripts (<script src="...">), inline scripts, noscript blocks,
9
- * and bare JS without any HTML tags.
26
+ * non-script HTML markup (links/meta/img/iframe), and bare JS without any HTML tags.
10
27
  */
11
28
  function parseScriptContent(content) {
12
29
  const trimmed = content.trim();
@@ -19,11 +36,11 @@ function parseScriptContent(content) {
19
36
  let lastIndex = 0;
20
37
  let match;
21
38
  while ((match = tagRegex.exec(trimmed)) !== null) {
22
- // Capture any bare text between tags (could be JS or HTML comments)
39
+ // Capture any text between tags markup (link/meta/img) or bare JS.
23
40
  const between = trimmed.slice(lastIndex, match.index).trim();
24
- // Skip HTML comments and empty strings
25
- if (between && !/^<!--[\s\S]*?-->$/.test(between)) {
26
- elements.push({ type: 'inline', code: between });
41
+ const betweenElement = classifyLooseChunk(between);
42
+ if (betweenElement) {
43
+ elements.push(betweenElement);
27
44
  }
28
45
  const [, tagName, attrs, body] = match;
29
46
  if (tagName.toLowerCase() === 'noscript') {
@@ -51,8 +68,9 @@ function parseScriptContent(content) {
51
68
  }
52
69
  // Trailing content after last tag
53
70
  const trailing = trimmed.slice(lastIndex).trim();
54
- if (trailing && !/^<!--[\s\S]*?-->$/.test(trailing)) {
55
- elements.push({ type: 'inline', code: trailing });
71
+ const trailingElement = classifyLooseChunk(trailing);
72
+ if (trailingElement) {
73
+ elements.push(trailingElement);
56
74
  }
57
75
  return elements;
58
76
  }
@@ -555,6 +573,23 @@ export function FunnelScriptInjector({ context, isInitialized }) {
555
573
  el.innerHTML = element.html;
556
574
  injectAtPosition(el, position);
557
575
  }
576
+ else if (element.type === 'html') {
577
+ // Non-script markup (link/meta/img/iframe pixels, etc.) — inject as real
578
+ // DOM nodes. Injecting this as JS would throw "Unexpected token '<'" and
579
+ // abort sibling scripts in the same snippet. <script> tags created via
580
+ // innerHTML don't execute, but real <script> blocks are parsed separately
581
+ // above, so none reach here.
582
+ const template = document.createElement('template');
583
+ template.innerHTML = element.html;
584
+ Array.from(template.content.childNodes).forEach(node => {
585
+ if (node.nodeType !== 1 /* ELEMENT_NODE */)
586
+ return;
587
+ const el = node;
588
+ el.setAttribute('data-tagada-stepconfig-script', 'true');
589
+ el.setAttribute('data-script-name', script.name);
590
+ injectAtPosition(el, position);
591
+ });
592
+ }
558
593
  });
559
594
  });
560
595
  // Track injected scripts to prevent re-injection
@@ -5,4 +5,5 @@
5
5
  import { AuthState } from '../../../react/types';
6
6
  export declare function useAuth(): AuthState & {
7
7
  isInitialized: boolean;
8
+ isVerified: boolean;
8
9
  };
@@ -5,5 +5,6 @@ export function useAuth() {
5
5
  return {
6
6
  ...auth,
7
7
  isInitialized,
8
+ isVerified: auth.customer?.role === 'verified',
8
9
  };
9
10
  }