@tagadapay/plugin-sdk 3.0.12 → 3.0.15

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 (31) hide show
  1. package/dist/external-tracker.js +3645 -115
  2. package/dist/external-tracker.min.js +25 -2
  3. package/dist/external-tracker.min.js.map +4 -4
  4. package/dist/react/types.d.ts +2 -0
  5. package/dist/v2/core/client.d.ts +5 -0
  6. package/dist/v2/core/client.js +135 -26
  7. package/dist/v2/core/config/environment.js +6 -0
  8. package/dist/v2/core/funnelClient.d.ts +27 -1
  9. package/dist/v2/core/funnelClient.js +124 -23
  10. package/dist/v2/core/resources/checkout.d.ts +76 -1
  11. package/dist/v2/core/resources/checkout.js +86 -1
  12. package/dist/v2/core/resources/funnel.d.ts +45 -4
  13. package/dist/v2/core/resources/offers.d.ts +26 -0
  14. package/dist/v2/core/resources/offers.js +37 -0
  15. package/dist/v2/core/types.d.ts +3 -1
  16. package/dist/v2/core/utils/authHandoff.d.ts +60 -0
  17. package/dist/v2/core/utils/authHandoff.js +154 -0
  18. package/dist/v2/core/utils/deviceInfo.d.ts +20 -3
  19. package/dist/v2/core/utils/deviceInfo.js +62 -94
  20. package/dist/v2/core/utils/previewMode.d.ts +4 -0
  21. package/dist/v2/core/utils/previewMode.js +4 -0
  22. package/dist/v2/react/hooks/useCheckoutQuery.d.ts +0 -1
  23. package/dist/v2/react/hooks/useCheckoutQuery.js +12 -4
  24. package/dist/v2/react/hooks/useFunnelLegacy.js +39 -11
  25. package/dist/v2/react/hooks/usePreviewOffer.d.ts +3 -3
  26. package/dist/v2/react/hooks/usePreviewOffer.js +20 -15
  27. package/dist/v2/react/hooks/useTranslation.js +12 -4
  28. package/dist/v2/react/providers/TagadaProvider.js +61 -1
  29. package/dist/v2/standalone/index.d.ts +2 -1
  30. package/dist/v2/standalone/index.js +2 -1
  31. package/package.json +3 -1
@@ -7,20 +7,105 @@ export class CheckoutResource {
7
7
  this.apiClient = apiClient;
8
8
  }
9
9
  /**
10
- * Initialize a new checkout session
10
+ * Initialize a new checkout session (sync mode)
11
+ * Response time: 2-5 seconds (full processing)
11
12
  */
12
13
  async initCheckout(params) {
13
14
  // Pass all params including customerId to prevent duplicate customer creation
14
15
  return this.apiClient.post('/api/v1/checkout/session/init', params);
15
16
  }
17
+ /**
18
+ * Initialize a new checkout session (async mode) ⚡
19
+ * Response time: ~50ms (20-50x faster!)
20
+ *
21
+ * Returns checkoutToken immediately, background job completes processing.
22
+ * Use getCheckout() to fetch full session data - it auto-waits for completion.
23
+ *
24
+ * @example
25
+ * // Fast init
26
+ * const { checkoutToken } = await checkout.initCheckoutAsync(params);
27
+ *
28
+ * // Redirect user immediately
29
+ * window.location.href = `/checkout/${checkoutToken}/op`;
30
+ *
31
+ * // By the time page loads, background processing is usually complete
32
+ */
33
+ async initCheckoutAsync(params) {
34
+ return this.apiClient.post('/api/v1/checkout/session/init-async', params);
35
+ }
36
+ /**
37
+ * Preload checkout session (ultra-fast background pre-computation) ⚡⚡⚡
38
+ *
39
+ * This is the recommended way to handle cart changes or "Buy Now" intent.
40
+ * It pre-computes everything (checkoutToken, navigation URL, CMS session)
41
+ * before the user even clicks the checkout button.
42
+ *
43
+ * The SDK automatically gets funnelSessionId from FunnelClient if provided.
44
+ * Only FunnelClient knows how to properly extract funnelSessionId (from state, URL, cookies, etc.)
45
+ *
46
+ * @param params - Checkout and funnel parameters
47
+ * @param getFunnelSessionId - Optional function to get funnelSessionId from FunnelClient
48
+ * This maintains separation of concerns - only FunnelClient knows how to get it
49
+ *
50
+ * @returns { checkoutToken, customerId, navigationUrl }
51
+ */
52
+ async preloadCheckout(params, getFunnelSessionId) {
53
+ // ⚡ GET FUNNEL SESSION ID: Only FunnelClient knows how to properly get it
54
+ // Priority: explicit param > FunnelClient > backend fallback (via currentUrl)
55
+ let funnelSessionId = params.funnelSessionId;
56
+ if (!funnelSessionId && getFunnelSessionId) {
57
+ // Let FunnelClient handle extraction (from state, URL, cookies, etc.)
58
+ funnelSessionId = getFunnelSessionId() || undefined;
59
+ }
60
+ // Format navigationEvent if it's a string
61
+ const navigationEvent = typeof params.navigationEvent === 'string'
62
+ ? { type: params.navigationEvent }
63
+ : params.navigationEvent;
64
+ // Build request - backend will also try to extract from currentUrl if not provided
65
+ const requestParams = {
66
+ ...params,
67
+ ...(funnelSessionId && { funnelSessionId }),
68
+ ...(navigationEvent && { navigationEvent }),
69
+ // Ensure currentUrl is always set for backend extraction fallback
70
+ currentUrl: params.currentUrl || (typeof window !== 'undefined' ? window.location.href : undefined),
71
+ };
72
+ return this.apiClient.post('/api/v1/checkout/session/preload', requestParams);
73
+ }
74
+ /**
75
+ * Check async checkout processing status (instant, no waiting)
76
+ * Perfect for polling or checking if background job completed
77
+ */
78
+ async checkAsyncStatus(checkoutToken) {
79
+ return this.apiClient.get(`/api/public/v1/checkout/async-status/${checkoutToken}`);
80
+ }
16
81
  /**
17
82
  * Get checkout session by token
83
+ *
84
+ * SDK automatically waits for async completion (skipAsyncWait=false) for seamless UX.
85
+ * This ensures async checkout sessions are fully processed before returning data.
86
+ *
87
+ * If you need to skip waiting (e.g., for manual polling), use getCheckoutRaw() instead.
18
88
  */
19
89
  async getCheckout(checkoutToken, currency) {
20
90
  const queryParams = new URLSearchParams();
21
91
  if (currency) {
22
92
  queryParams.set('currency', currency);
23
93
  }
94
+ // SDK explicitly waits for async completion (default is skip=true for retro compat)
95
+ queryParams.set('skipAsyncWait', 'false');
96
+ const url = `/api/v1/checkout-sessions/${checkoutToken}/v2${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
97
+ return this.apiClient.get(url);
98
+ }
99
+ /**
100
+ * Get checkout session by token without waiting for async completion
101
+ * Useful for manual polling scenarios
102
+ */
103
+ async getCheckoutRaw(checkoutToken, currency) {
104
+ const queryParams = new URLSearchParams();
105
+ if (currency) {
106
+ queryParams.set('currency', currency);
107
+ }
108
+ queryParams.set('skipAsyncWait', 'true');
24
109
  const url = `/api/v1/checkout-sessions/${checkoutToken}/v2${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
25
110
  return this.apiClient.get(url);
26
111
  }
@@ -343,16 +343,25 @@ export interface FunnelNavigationAction {
343
343
  data?: any;
344
344
  }
345
345
  export interface FunnelNavigationResult {
346
- stepId: string;
346
+ stepId?: string;
347
347
  url?: string;
348
- action: FunnelNavigationAction;
349
- context: SimpleFunnelContext;
348
+ action?: FunnelNavigationAction;
349
+ context?: SimpleFunnelContext;
350
350
  tracking?: {
351
351
  from: string;
352
352
  to: string;
353
353
  event: string;
354
354
  timestamp: string;
355
355
  };
356
+ /**
357
+ * Fire-and-forget response: indicates navigation was queued
358
+ * When true, stepId, url, action, and context will be undefined
359
+ */
360
+ queued?: boolean;
361
+ /**
362
+ * Session ID (may be different if session was recovered)
363
+ */
364
+ sessionId?: string;
356
365
  }
357
366
  /**
358
367
  * Funnel context available to plugins
@@ -421,6 +430,7 @@ export interface SimpleFunnelContext<TCustom = {}> {
421
430
  * For backward compatibility and flexible unstructured data
422
431
  */
423
432
  metadata?: Record<string, any>;
433
+ script?: string;
424
434
  }
425
435
  export interface FunnelInitializeRequest {
426
436
  cmsSession: {
@@ -480,11 +490,37 @@ export interface FunnelNavigateRequest {
480
490
  * If session is not found and funnelId is provided, a new session will be created
481
491
  */
482
492
  funnelId?: string;
493
+ /**
494
+ * Funnel step ID (from SDK injection/URL params)
495
+ * Used to sync session state before navigation
496
+ */
497
+ funnelStepId?: string;
498
+ /**
499
+ * Funnel variant ID (from SDK injection/URL params for A/B testing)
500
+ * Used to sync session state before navigation
501
+ */
502
+ funnelVariantId?: string;
503
+ /**
504
+ * Fire and forget mode - queues navigation to QStash and returns immediately
505
+ * No response data needed, just acknowledgment that request was queued
506
+ * When true, result will only contain queued status and sessionId (no URL or stepId)
507
+ */
508
+ fireAndForget?: boolean;
509
+ /**
510
+ * ✅ Customer tags to set (merged with existing customer tags)
511
+ * @example ['segment:vip', 'cart_value:high']
512
+ */
513
+ customerTags?: string[];
514
+ /**
515
+ * ✅ Device ID for geo/device tag enrichment (optional)
516
+ * @example 'dev_abc123xyz'
517
+ */
518
+ deviceId?: string;
483
519
  }
484
520
  export interface FunnelNavigateResponse {
485
521
  success: boolean;
486
522
  result?: {
487
- stepId: string;
523
+ stepId?: string;
488
524
  url?: string;
489
525
  /**
490
526
  * New session ID if session was recovered (expired/removed)
@@ -497,6 +533,11 @@ export interface FunnelNavigateResponse {
497
533
  event: string;
498
534
  timestamp: string;
499
535
  };
536
+ /**
537
+ * Fire-and-forget response: indicates navigation was queued
538
+ * When present, stepId and url will be undefined
539
+ */
540
+ queued?: boolean;
500
541
  };
501
542
  error?: string;
502
543
  }
@@ -303,6 +303,32 @@ export declare class OffersResource {
303
303
  checkoutSessionId?: string;
304
304
  customerId?: string;
305
305
  }>;
306
+ /**
307
+ * Transform offer to checkout session (async mode) ⚡
308
+ * Response time: ~50ms (20-50x faster!)
309
+ *
310
+ * Returns checkoutToken immediately, background job completes processing.
311
+ * Use getCheckout() to fetch full session data - it auto-waits for completion.
312
+ *
313
+ * @example
314
+ * // Fast transform
315
+ * const { checkoutToken } = await offers.toCheckoutAsync(offerId, currency, lineItems, returnUrl, mainOrderId);
316
+ *
317
+ * // Redirect user immediately
318
+ * window.location.href = `/checkout/${checkoutToken}/op`;
319
+ *
320
+ * // By the time page loads, background processing is usually complete
321
+ */
322
+ toCheckoutAsync(offerId: string, currency?: string, lineItems?: Array<{
323
+ lineItemId?: string;
324
+ productId?: string;
325
+ variantId: string;
326
+ quantity: number;
327
+ }>, returnUrl?: string, mainOrderId?: string): Promise<{
328
+ checkoutToken: string;
329
+ customerId: string;
330
+ status: 'processing';
331
+ }>;
306
332
  /**
307
333
  * @deprecated Use transformToCheckoutSession instead
308
334
  * Transform offer to checkout session with dynamic variant selection
@@ -202,6 +202,43 @@ export class OffersResource {
202
202
  customerId: response.customerId,
203
203
  };
204
204
  }
205
+ /**
206
+ * Transform offer to checkout session (async mode) ⚡
207
+ * Response time: ~50ms (20-50x faster!)
208
+ *
209
+ * Returns checkoutToken immediately, background job completes processing.
210
+ * Use getCheckout() to fetch full session data - it auto-waits for completion.
211
+ *
212
+ * @example
213
+ * // Fast transform
214
+ * const { checkoutToken } = await offers.toCheckoutAsync(offerId, currency, lineItems, returnUrl, mainOrderId);
215
+ *
216
+ * // Redirect user immediately
217
+ * window.location.href = `/checkout/${checkoutToken}/op`;
218
+ *
219
+ * // By the time page loads, background processing is usually complete
220
+ */
221
+ async toCheckoutAsync(offerId, currency = 'USD', lineItems, returnUrl, mainOrderId) {
222
+ console.log('🛒 [OffersResource] Calling to-checkout-async API:', {
223
+ offerId,
224
+ currency,
225
+ lineItems,
226
+ returnUrl,
227
+ mainOrderId,
228
+ endpoint: `/api/v1/offers/${offerId}/to-checkout-async`,
229
+ });
230
+ const response = await this.apiClient.post(`/api/v1/offers/${offerId}/to-checkout-async`, {
231
+ offerId,
232
+ lineItems: lineItems?.map(item => ({
233
+ variantId: item.variantId,
234
+ quantity: item.quantity,
235
+ })) || [],
236
+ returnUrl: returnUrl || (typeof window !== 'undefined' ? window.location.href : ''),
237
+ mainOrderId: mainOrderId || '',
238
+ });
239
+ console.log('📥 [OffersResource] To-checkout-async API response:', response);
240
+ return response;
241
+ }
205
242
  /**
206
243
  * @deprecated Use transformToCheckoutSession instead
207
244
  * Transform offer to checkout session with dynamic variant selection
@@ -7,7 +7,9 @@ export interface ApiConfig {
7
7
  endpoints: {
8
8
  checkout: {
9
9
  sessionInit: string;
10
+ sessionInitAsync: string;
10
11
  sessionStatus: string;
12
+ asyncStatus: string;
11
13
  };
12
14
  customer: {
13
15
  profile: string;
@@ -262,7 +264,7 @@ export interface CustomerInfos {
262
264
  }
263
265
  export interface SessionInitResponse {
264
266
  store: Store;
265
- locale: string;
267
+ locale?: string;
266
268
  messages?: Record<string, string>;
267
269
  customer?: Customer;
268
270
  session?: Session;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Cross-Domain Auth Handoff Utilities
3
+ *
4
+ * Handles automatic resolution of authCode query parameters for seamless
5
+ * cross-domain authentication.
6
+ *
7
+ * Flow:
8
+ * 1. Check URL for authCode parameter
9
+ * 2. If present, resolve it with backend
10
+ * 3. Store the returned token (overrides any existing token)
11
+ * 4. Clean the URL (remove authCode)
12
+ * 5. Return resolved customer and context
13
+ */
14
+ export interface AuthHandoffResolveResponse {
15
+ sessionId: string;
16
+ token: string;
17
+ customer: {
18
+ id: string;
19
+ email?: string;
20
+ firstName?: string;
21
+ lastName?: string;
22
+ role: 'authenticated' | 'anonymous';
23
+ };
24
+ context: Record<string, unknown>;
25
+ }
26
+ /**
27
+ * Check if authCode is present in URL
28
+ */
29
+ export declare function hasAuthCode(): boolean;
30
+ /**
31
+ * Get authCode from URL
32
+ */
33
+ export declare function getAuthCode(): string | null;
34
+ /**
35
+ * Check if a code has already been resolved
36
+ */
37
+ export declare function isCodeAlreadyResolved(code: string): boolean;
38
+ /**
39
+ * Resolve auth handoff and return token + customer info
40
+ *
41
+ * This function:
42
+ * 1. Calls POST /api/v1/cms/auth/resolve-handoff
43
+ * 2. Stores the returned token (overrides existing)
44
+ * 3. Cleans the URL (removes authCode)
45
+ * 4. Returns customer and context data
46
+ *
47
+ * 🔒 Deduplication: Multiple calls with the same code will return the same promise
48
+ * to prevent duplicate API requests (e.g., React StrictMode double-mounting)
49
+ */
50
+ export declare function resolveAuthHandoff(authCode: string, storeId: string, apiBaseUrl: string, debugMode?: boolean): Promise<AuthHandoffResolveResponse>;
51
+ /**
52
+ * Remove authCode from URL without page reload
53
+ * Uses history.replaceState to update URL cleanly
54
+ */
55
+ export declare function cleanAuthCodeFromUrl(debugMode?: boolean): void;
56
+ /**
57
+ * Check if we should resolve authCode
58
+ * Returns true if authCode is present and valid format
59
+ */
60
+ export declare function shouldResolveAuthCode(): boolean;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Cross-Domain Auth Handoff Utilities
3
+ *
4
+ * Handles automatic resolution of authCode query parameters for seamless
5
+ * cross-domain authentication.
6
+ *
7
+ * Flow:
8
+ * 1. Check URL for authCode parameter
9
+ * 2. If present, resolve it with backend
10
+ * 3. Store the returned token (overrides any existing token)
11
+ * 4. Clean the URL (remove authCode)
12
+ * 5. Return resolved customer and context
13
+ */
14
+ import { setClientToken } from './tokenStorage';
15
+ // Track in-flight and completed resolutions to prevent duplicates
16
+ const resolutionCache = new Map();
17
+ const resolvedCodes = new Set();
18
+ /**
19
+ * Check if authCode is present in URL
20
+ */
21
+ export function hasAuthCode() {
22
+ if (typeof window === 'undefined')
23
+ return false;
24
+ const urlParams = new URLSearchParams(window.location.search);
25
+ return urlParams.has('authCode');
26
+ }
27
+ /**
28
+ * Get authCode from URL
29
+ */
30
+ export function getAuthCode() {
31
+ if (typeof window === 'undefined')
32
+ return null;
33
+ const urlParams = new URLSearchParams(window.location.search);
34
+ return urlParams.get('authCode');
35
+ }
36
+ /**
37
+ * Check if a code has already been resolved
38
+ */
39
+ export function isCodeAlreadyResolved(code) {
40
+ return resolvedCodes.has(code);
41
+ }
42
+ /**
43
+ * Resolve auth handoff and return token + customer info
44
+ *
45
+ * This function:
46
+ * 1. Calls POST /api/v1/cms/auth/resolve-handoff
47
+ * 2. Stores the returned token (overrides existing)
48
+ * 3. Cleans the URL (removes authCode)
49
+ * 4. Returns customer and context data
50
+ *
51
+ * 🔒 Deduplication: Multiple calls with the same code will return the same promise
52
+ * to prevent duplicate API requests (e.g., React StrictMode double-mounting)
53
+ */
54
+ export async function resolveAuthHandoff(authCode, storeId, apiBaseUrl, debugMode = false) {
55
+ // Check if already resolved
56
+ if (resolvedCodes.has(authCode)) {
57
+ if (debugMode) {
58
+ console.log('[AuthHandoff] Code already resolved, skipping duplicate request');
59
+ }
60
+ throw new Error('Auth code already resolved');
61
+ }
62
+ // Check if resolution is in-flight
63
+ const inFlightResolution = resolutionCache.get(authCode);
64
+ if (inFlightResolution) {
65
+ if (debugMode) {
66
+ console.log('[AuthHandoff] Resolution already in progress, waiting for existing request');
67
+ }
68
+ return inFlightResolution;
69
+ }
70
+ if (debugMode) {
71
+ console.log('[AuthHandoff] Resolving authCode:', authCode.substring(0, 15) + '...');
72
+ }
73
+ // Create resolution promise
74
+ const resolutionPromise = (async () => {
75
+ try {
76
+ // Call resolve endpoint (no authentication required)
77
+ const response = await fetch(`${apiBaseUrl}/api/v1/cms/auth/resolve-handoff`, {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ },
82
+ body: JSON.stringify({
83
+ code: authCode,
84
+ storeId,
85
+ }),
86
+ });
87
+ if (!response.ok) {
88
+ const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
89
+ throw new Error(errorData.message || `Failed to resolve auth handoff: ${response.status}`);
90
+ }
91
+ const data = await response.json();
92
+ if (debugMode) {
93
+ console.log('[AuthHandoff] ✅ Resolved successfully:', {
94
+ customerId: data.customer.id,
95
+ role: data.customer.role,
96
+ hasContext: Object.keys(data.context).length > 0,
97
+ });
98
+ }
99
+ // Store token (overrides any existing token)
100
+ if (debugMode) {
101
+ console.log('[AuthHandoff] Storing new token (overriding existing)');
102
+ }
103
+ setClientToken(data.token);
104
+ // Clean URL (remove authCode parameter)
105
+ cleanAuthCodeFromUrl(debugMode);
106
+ // Mark as resolved
107
+ resolvedCodes.add(authCode);
108
+ return data;
109
+ }
110
+ catch (error) {
111
+ console.error('[AuthHandoff] ❌ Failed to resolve:', error);
112
+ throw error;
113
+ }
114
+ finally {
115
+ // Remove from in-flight cache after completion (success or failure)
116
+ resolutionCache.delete(authCode);
117
+ }
118
+ })();
119
+ // Cache the in-flight promise
120
+ resolutionCache.set(authCode, resolutionPromise);
121
+ return resolutionPromise;
122
+ }
123
+ /**
124
+ * Remove authCode from URL without page reload
125
+ * Uses history.replaceState to update URL cleanly
126
+ */
127
+ export function cleanAuthCodeFromUrl(debugMode = false) {
128
+ if (typeof window === 'undefined')
129
+ return;
130
+ const url = new URL(window.location.href);
131
+ if (url.searchParams.has('authCode')) {
132
+ url.searchParams.delete('authCode');
133
+ // Use replaceState to update URL without reload
134
+ window.history.replaceState({}, '', url.pathname + url.search + url.hash);
135
+ if (debugMode) {
136
+ console.log('[AuthHandoff] Cleaned authCode from URL');
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * Check if we should resolve authCode
142
+ * Returns true if authCode is present and valid format
143
+ */
144
+ export function shouldResolveAuthCode() {
145
+ const authCode = getAuthCode();
146
+ if (!authCode || !authCode.startsWith('ah_')) {
147
+ return false;
148
+ }
149
+ // Don't resolve if already resolved
150
+ if (resolvedCodes.has(authCode)) {
151
+ return false;
152
+ }
153
+ return true;
154
+ }
@@ -1,16 +1,27 @@
1
1
  export interface DeviceInfo {
2
2
  userAgent: {
3
+ name: string;
3
4
  browser: {
5
+ major: string;
4
6
  name: string;
5
7
  version: string;
8
+ type?: string;
6
9
  };
7
10
  os: {
8
11
  name: string;
9
12
  version: string;
10
13
  };
11
14
  device?: {
12
- type: string;
13
- model: string;
15
+ model?: string;
16
+ type?: string;
17
+ vendor?: string;
18
+ };
19
+ engine: {
20
+ name: string;
21
+ version: string;
22
+ };
23
+ cpu: {
24
+ architecture: string;
14
25
  };
15
26
  };
16
27
  screenResolution: {
@@ -18,13 +29,19 @@ export interface DeviceInfo {
18
29
  height: number;
19
30
  };
20
31
  timeZone: string;
32
+ flags?: {
33
+ isBot: boolean;
34
+ isChromeFamily: boolean;
35
+ isStandalonePWA: boolean;
36
+ isAppleSilicon: boolean;
37
+ };
21
38
  }
22
39
  /**
23
40
  * Get browser locale
24
41
  */
25
42
  export declare function getBrowserLocale(): string;
26
43
  /**
27
- * Collect all device information
44
+ * Collect all device information using UAParser
28
45
  */
29
46
  export declare function collectDeviceInfo(): DeviceInfo;
30
47
  /**