@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
@@ -1,91 +1,5 @@
1
- /**
2
- * Get basic browser information from user agent
3
- */
4
- function getBrowserInfo() {
5
- const userAgent = navigator.userAgent;
6
- // Chrome
7
- if (userAgent.includes('Chrome')) {
8
- const match = /Chrome\/(\d+)/.exec(userAgent);
9
- return { name: 'Chrome', version: match ? match[1] : 'unknown' };
10
- }
11
- // Firefox
12
- if (userAgent.includes('Firefox')) {
13
- const match = /Firefox\/(\d+)/.exec(userAgent);
14
- return { name: 'Firefox', version: match ? match[1] : 'unknown' };
15
- }
16
- // Safari
17
- if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) {
18
- const match = /Version\/(\d+)/.exec(userAgent);
19
- return { name: 'Safari', version: match ? match[1] : 'unknown' };
20
- }
21
- // Edge
22
- if (userAgent.includes('Edge')) {
23
- const match = /Edge\/(\d+)/.exec(userAgent);
24
- return { name: 'Edge', version: match ? match[1] : 'unknown' };
25
- }
26
- return { name: 'unknown', version: 'unknown' };
27
- }
28
- /**
29
- * Get basic OS information from user agent
30
- */
31
- function getOSInfo() {
32
- const userAgent = navigator.userAgent;
33
- // Windows
34
- if (userAgent.includes('Windows')) {
35
- if (userAgent.includes('Windows NT 10.0'))
36
- return { name: 'Windows', version: '10' };
37
- if (userAgent.includes('Windows NT 6.3'))
38
- return { name: 'Windows', version: '8.1' };
39
- if (userAgent.includes('Windows NT 6.2'))
40
- return { name: 'Windows', version: '8' };
41
- if (userAgent.includes('Windows NT 6.1'))
42
- return { name: 'Windows', version: '7' };
43
- return { name: 'Windows', version: 'unknown' };
44
- }
45
- // macOS
46
- if (userAgent.includes('Mac OS X')) {
47
- const match = /Mac OS X (\d+[._]\d+)/.exec(userAgent);
48
- return { name: 'macOS', version: match ? match[1].replace('_', '.') : 'unknown' };
49
- }
50
- // iOS
51
- if (userAgent.includes('iPhone') || userAgent.includes('iPad')) {
52
- const match = /OS (\d+[._]\d+)/.exec(userAgent);
53
- return { name: 'iOS', version: match ? match[1].replace('_', '.') : 'unknown' };
54
- }
55
- // Android
56
- if (userAgent.includes('Android')) {
57
- const match = /Android (\d+[.\d]*)/.exec(userAgent);
58
- return { name: 'Android', version: match ? match[1] : 'unknown' };
59
- }
60
- // Linux
61
- if (userAgent.includes('Linux')) {
62
- return { name: 'Linux', version: 'unknown' };
63
- }
64
- return { name: 'unknown', version: 'unknown' };
65
- }
66
- /**
67
- * Get device information
68
- */
69
- function getDeviceInfo() {
70
- const userAgent = navigator.userAgent;
71
- // Mobile devices
72
- if (userAgent.includes('iPhone')) {
73
- return { type: 'mobile', model: 'iPhone' };
74
- }
75
- if (userAgent.includes('iPad')) {
76
- return { type: 'tablet', model: 'iPad' };
77
- }
78
- if (userAgent.includes('Android')) {
79
- if (userAgent.includes('Mobile')) {
80
- return { type: 'mobile', model: 'Android' };
81
- }
82
- else {
83
- return { type: 'tablet', model: 'Android' };
84
- }
85
- }
86
- // Desktop (no specific device info)
87
- return undefined;
88
- }
1
+ import { UAParser } from '@ua-parser-js/pro-enterprise';
2
+ import { isBot, isChromeFamily, isStandalonePWA, isAppleSilicon, } from '@ua-parser-js/pro-enterprise/helpers';
89
3
  /**
90
4
  * Get screen resolution
91
5
  */
@@ -120,28 +34,82 @@ export function getBrowserLocale() {
120
34
  }
121
35
  }
122
36
  /**
123
- * Collect all device information
37
+ * Collect all device information using UAParser
124
38
  */
125
39
  export function collectDeviceInfo() {
126
40
  if (typeof window === 'undefined') {
127
41
  // Server-side fallback
128
42
  return {
129
43
  userAgent: {
130
- browser: { name: 'unknown', version: 'unknown' },
131
- os: { name: 'unknown', version: 'unknown' },
44
+ name: '',
45
+ browser: { major: '', name: '', version: '' },
46
+ os: { name: '', version: '' },
47
+ device: undefined,
48
+ engine: { name: '', version: '' },
49
+ cpu: { architecture: '' },
132
50
  },
133
51
  screenResolution: { width: 0, height: 0 },
134
52
  timeZone: 'UTC',
53
+ flags: {
54
+ isBot: false,
55
+ isChromeFamily: false,
56
+ isStandalonePWA: false,
57
+ isAppleSilicon: false,
58
+ },
59
+ };
60
+ }
61
+ const parser = new UAParser();
62
+ const result = parser.getResult();
63
+ // Enhanced detection using UAParser official helpers
64
+ let flags;
65
+ try {
66
+ flags = {
67
+ isBot: isBot(result),
68
+ isChromeFamily: isChromeFamily(result),
69
+ isStandalonePWA: isStandalonePWA(),
70
+ isAppleSilicon: isAppleSilicon(result),
71
+ };
72
+ }
73
+ catch (error) {
74
+ console.error('Failed to compute device flags:', error);
75
+ flags = {
76
+ isBot: false,
77
+ isChromeFamily: false,
78
+ isStandalonePWA: false,
79
+ isAppleSilicon: false,
135
80
  };
136
81
  }
137
82
  return {
138
83
  userAgent: {
139
- browser: getBrowserInfo(),
140
- os: getOSInfo(),
141
- device: getDeviceInfo(),
84
+ name: result.ua,
85
+ browser: {
86
+ major: result.browser.major || '',
87
+ name: result.browser.name || '',
88
+ version: result.browser.version || '',
89
+ type: result.browser.type,
90
+ },
91
+ os: {
92
+ name: result.os.name || '',
93
+ version: result.os.version || '',
94
+ },
95
+ device: result.device.model || result.device.type || result.device.vendor
96
+ ? {
97
+ model: result.device.model,
98
+ type: result.device.type,
99
+ vendor: result.device.vendor,
100
+ }
101
+ : undefined,
102
+ engine: {
103
+ name: result.engine.name || '',
104
+ version: result.engine.version || '',
105
+ },
106
+ cpu: {
107
+ architecture: result.cpu.architecture || '',
108
+ },
142
109
  },
143
110
  screenResolution: getScreenResolution(),
144
111
  timeZone: getTimeZone(),
112
+ flags,
145
113
  };
146
114
  }
147
115
  /**
@@ -13,6 +13,9 @@
13
13
  * - token: Authentication token (URL > localStorage)
14
14
  * - funnelSessionId: Active funnel session (URL > cookie)
15
15
  *
16
+ * ⚠️ Note: authCode is NOT handled here - it has highest priority and is handled
17
+ * separately in client.ts before all other initialization logic.
18
+ *
16
19
  * Usage examples:
17
20
  * - Force production API: ?tagadaClientEnv=production
18
21
  * - Force development API: ?tagadaClientEnv=development
@@ -20,6 +23,7 @@
20
23
  * - Custom API URL: ?tagadaClientBaseUrl=https://tagada.loclx.io
21
24
  * - Combined: ?tagadaClientEnv=local&tagadaClientBaseUrl=https://tagada.loclx.io
22
25
  * - Hard reset + production: ?forceReset=true&tagadaClientEnv=production
26
+ * - Cross-domain auth: ?authCode=ah_... (automatically handled, highest priority)
23
27
  */
24
28
  /**
25
29
  * SDK Override Parameters - centralized across all SDK functions
@@ -13,6 +13,9 @@
13
13
  * - token: Authentication token (URL > localStorage)
14
14
  * - funnelSessionId: Active funnel session (URL > cookie)
15
15
  *
16
+ * ⚠️ Note: authCode is NOT handled here - it has highest priority and is handled
17
+ * separately in client.ts before all other initialization logic.
18
+ *
16
19
  * Usage examples:
17
20
  * - Force production API: ?tagadaClientEnv=production
18
21
  * - Force development API: ?tagadaClientEnv=development
@@ -20,6 +23,7 @@
20
23
  * - Custom API URL: ?tagadaClientBaseUrl=https://tagada.loclx.io
21
24
  * - Combined: ?tagadaClientEnv=local&tagadaClientBaseUrl=https://tagada.loclx.io
22
25
  * - Hard reset + production: ?forceReset=true&tagadaClientEnv=production
26
+ * - Cross-domain auth: ?authCode=ah_... (automatically handled, highest priority)
23
27
  */
24
28
  import { clearClientToken, setClientToken, getClientToken } from './tokenStorage';
25
29
  import { clearFunnelSessionCookie } from './sessionStorage';
@@ -13,7 +13,6 @@ export interface UseCheckoutQueryResult {
13
13
  error: Error | null;
14
14
  isSuccess: boolean;
15
15
  init: (params: CheckoutInitParams) => Promise<{
16
- checkoutUrl: string;
17
16
  checkoutSession: any;
18
17
  checkoutToken: string;
19
18
  }>;
@@ -87,9 +87,9 @@ export function useCheckoutQuery(options = {}) {
87
87
  await refetch();
88
88
  }
89
89
  }, [refetch, checkoutToken]);
90
- // Initialize checkout mutation
90
+ // Initialize checkout mutation (async mode for fast response)
91
91
  const initMutation = useMutation({
92
- mutationFn: (params) => {
92
+ mutationFn: async (params) => {
93
93
  const requestBody = {
94
94
  ...params,
95
95
  storeId: params.storeId || storeId,
@@ -101,7 +101,12 @@ export function useCheckoutQuery(options = {}) {
101
101
  currency: params.customer?.currency ?? currency.code,
102
102
  },
103
103
  };
104
- return checkoutResource.initCheckout(requestBody);
104
+ // Use async mode for fast response (~50ms vs 2-5s)
105
+ const asyncResponse = await checkoutResource.initCheckoutAsync(requestBody);
106
+ return {
107
+ checkoutToken: asyncResponse.checkoutToken,
108
+ checkoutSession: {}, // Will be populated when getCheckout is called
109
+ };
105
110
  },
106
111
  onSuccess: (response) => {
107
112
  // Update URL with checkout token
@@ -257,9 +262,12 @@ export function useCheckoutQuery(options = {}) {
257
262
  await waitForSession();
258
263
  const result = await initMutation.mutateAsync(params);
259
264
  // Update internal token state so the query can fetch the checkout data
265
+ // The query will automatically refetch when token changes, and getCheckout()
266
+ // will automatically wait for async completion (via SDK skipAsyncWait=false)
260
267
  setInternalToken(result.checkoutToken);
268
+ // Return immediately with token
269
+ // checkoutSession will be populated by the query once background processing completes
261
270
  return {
262
- checkoutUrl: result.checkoutUrl,
263
271
  checkoutSession: checkout?.checkoutSession ?? {},
264
272
  checkoutToken: result.checkoutToken,
265
273
  };
@@ -339,12 +339,31 @@ export function useFunnel(options = {}) {
339
339
  onSuccess: (result) => {
340
340
  if (!context)
341
341
  return;
342
+ // 🔥 Fire-and-forget mode: Just acknowledge, no navigation
343
+ if (result.queued) {
344
+ // Update session ID if changed
345
+ if (result.sessionId && result.sessionId !== context.sessionId) {
346
+ const newContext = {
347
+ ...context,
348
+ sessionId: result.sessionId,
349
+ };
350
+ const enrichedContext = enrichContext(newContext);
351
+ setContext(enrichedContext);
352
+ document.cookie = `funnelSessionId=${result.sessionId}; path=/; max-age=86400; SameSite=Lax`;
353
+ }
354
+ return; // Early return for fire-and-forget
355
+ }
342
356
  // 🔄 Handle session recovery (if backend created a new session)
343
357
  let recoveredSessionId;
344
358
  if (result.sessionId && result.sessionId !== context.sessionId) {
345
359
  console.warn(`🔄 Funnel: Session recovered! Old: ${context.sessionId}, New: ${result.sessionId}`);
346
360
  recoveredSessionId = result.sessionId;
347
361
  }
362
+ // Validate required fields for normal navigation
363
+ if (!result.stepId) {
364
+ console.warn('Funnel: Navigation result missing stepId');
365
+ return;
366
+ }
348
367
  // Update local context
349
368
  const newContext = {
350
369
  ...context,
@@ -366,13 +385,13 @@ export function useFunnel(options = {}) {
366
385
  document.cookie = `funnelSessionId=${recoveredSessionId}; path=/; max-age=86400; SameSite=Lax`;
367
386
  console.log(`🍪 Funnel: Updated cookie with recovered session ID: ${recoveredSessionId}`);
368
387
  }
369
- // Create typed navigation result
388
+ // Create typed navigation result (only if we have URL)
370
389
  const navigationResult = {
371
390
  stepId: result.stepId,
372
- action: {
391
+ action: result.url ? {
373
392
  type: 'redirect', // Default action type
374
393
  url: result.url
375
- },
394
+ } : undefined,
376
395
  context: enrichedContext,
377
396
  tracking: result.tracking
378
397
  };
@@ -384,18 +403,27 @@ export function useFunnel(options = {}) {
384
403
  shouldPerformDefaultNavigation = false;
385
404
  }
386
405
  }
387
- // Perform default navigation if not overridden
388
- if (shouldPerformDefaultNavigation && navigationResult.action.url) {
389
- // Add URL parameters for cross-domain session continuity
390
- const urlWithParams = addSessionParams(navigationResult.action.url, enrichedContext.sessionId, effectiveFunnelId || options.funnelId);
391
- const updatedAction = { ...navigationResult.action, url: urlWithParams };
392
- performNavigation(updatedAction);
406
+ // Perform default navigation if not overridden and we have an action with URL
407
+ if (shouldPerformDefaultNavigation && navigationResult.action?.url) {
408
+ const action = navigationResult.action; // Type narrowing
409
+ const actionUrl = action.url; // Extract URL for type narrowing
410
+ if (actionUrl) {
411
+ // Add URL parameters for cross-domain session continuity
412
+ const urlWithParams = addSessionParams(actionUrl, enrichedContext.sessionId, effectiveFunnelId || options.funnelId);
413
+ const updatedAction = {
414
+ type: action.type || 'redirect', // Ensure type is defined
415
+ url: urlWithParams,
416
+ data: action.data,
417
+ };
418
+ performNavigation(updatedAction);
419
+ }
393
420
  }
394
421
  // Skip background refreshes if we are navigating away (full page reload)
395
422
  // This prevents "lingering" requests from the old page context
423
+ const action = navigationResult.action;
396
424
  const isFullNavigation = shouldPerformDefaultNavigation &&
397
- navigationResult.action.url &&
398
- (navigationResult.action.type === 'redirect' || navigationResult.action.type === 'replace');
425
+ action?.url &&
426
+ (action.type === 'redirect' || action.type === 'replace');
399
427
  if (!isFullNavigation) {
400
428
  // Fetch debug data if in debug mode
401
429
  if (debugMode) {
@@ -76,9 +76,9 @@ export interface UsePreviewOfferResult {
76
76
  checkoutUrl: string;
77
77
  }>;
78
78
  toCheckout: (mainOrderId?: string) => Promise<{
79
- checkoutSessionId?: string;
80
- checkoutToken?: string;
81
- checkoutUrl: string;
79
+ checkoutToken: string;
80
+ customerId: string;
81
+ status: 'processing';
82
82
  }>;
83
83
  }
84
84
  export declare function usePreviewOffer(options: UsePreviewOfferOptions): UsePreviewOfferResult;
@@ -7,12 +7,16 @@
7
7
  * 3. Recalculating totals on-the-fly without backend calls
8
8
  * 4. NO checkout session creation - perfect for browsing/previewing
9
9
  */
10
- import { useCallback, useEffect, useMemo, useState } from 'react';
11
10
  import { useQuery } from '@tanstack/react-query';
11
+ import { useCallback, useEffect, useMemo, useState } from 'react';
12
12
  import { OffersResource } from '../../core/resources/offers';
13
13
  import { getGlobalApiClient } from './useApiQuery';
14
14
  export function usePreviewOffer(options) {
15
15
  const { offerId, currency: requestedCurrency = 'USD', initialSelections = {} } = options;
16
+ const queryParamsCurrency = typeof window !== 'undefined'
17
+ ? new URLSearchParams(window.location.search).get('currency') || undefined
18
+ : undefined;
19
+ const effectiveCurrency = queryParamsCurrency || requestedCurrency;
16
20
  const offersResource = useMemo(() => new OffersResource(getGlobalApiClient()), []);
17
21
  // Track user selections (variant + quantity per product)
18
22
  const [selections, setSelections] = useState(initialSelections);
@@ -35,14 +39,14 @@ export function usePreviewOffer(options) {
35
39
  }, [selections]);
36
40
  // 🎯 ONE API CALL - Fetch offer + preview in one request
37
41
  const { data, isLoading, isFetching, error } = useQuery({
38
- queryKey: ['offer-preview', offerId, requestedCurrency, lineItemsForPreview],
42
+ queryKey: ['offer-preview', offerId, effectiveCurrency, lineItemsForPreview],
39
43
  queryFn: async () => {
40
44
  console.log('🔍 [usePreviewOffer] Fetching preview:', {
41
45
  offerId,
42
- currency: requestedCurrency,
46
+ currency: effectiveCurrency,
43
47
  lineItems: lineItemsForPreview,
44
48
  });
45
- const result = await offersResource.previewOffer(offerId, requestedCurrency, lineItemsForPreview);
49
+ const result = await offersResource.previewOffer(offerId, effectiveCurrency, lineItemsForPreview);
46
50
  console.log('✅ [usePreviewOffer] Preview result:', result);
47
51
  // Extract offer and preview from unified response
48
52
  const offer = result.offer || null;
@@ -66,7 +70,7 @@ export function usePreviewOffer(options) {
66
70
  totalAmount: preview.totalAmount || 0,
67
71
  totalAdjustedAmount: preview.totalAdjustedAmount || 0,
68
72
  totalPromotionAmount: preview.totalPromotionAmount || 0,
69
- currency: preview.currency || requestedCurrency,
73
+ currency: preview.currency || effectiveCurrency,
70
74
  options: preview.options || {},
71
75
  };
72
76
  return { offer, summary };
@@ -200,7 +204,7 @@ export function usePreviewOffer(options) {
200
204
  return [];
201
205
  const currentItem = summary.items.find((i) => i.productId === productId);
202
206
  const activePriceId = currentItem?.priceId;
203
- const currency = summary.currency || requestedCurrency;
207
+ const currency = summary.currency || effectiveCurrency;
204
208
  return summary.options[productId].map((variant) => {
205
209
  // Find matching price or use first
206
210
  let unitAmount = 0;
@@ -222,7 +226,7 @@ export function usePreviewOffer(options) {
222
226
  currency,
223
227
  };
224
228
  });
225
- }, [summary, requestedCurrency]);
229
+ }, [summary, effectiveCurrency]);
226
230
  // Pay for the offer with current selections
227
231
  const pay = useCallback(async (mainOrderId) => {
228
232
  if (isPaying) {
@@ -230,7 +234,7 @@ export function usePreviewOffer(options) {
230
234
  }
231
235
  setIsPaying(true);
232
236
  try {
233
- const result = await offersResource.payPreviewedOffer(offerId, requestedCurrency, lineItemsForPreview, typeof window !== 'undefined' ? window.location.href : undefined, mainOrderId);
237
+ const result = await offersResource.payPreviewedOffer(offerId, effectiveCurrency, lineItemsForPreview, typeof window !== 'undefined' ? window.location.href : undefined, mainOrderId);
234
238
  console.log('[usePreviewOffer] Payment initiated:', result);
235
239
  return {
236
240
  checkoutUrl: result.checkout.checkoutUrl,
@@ -243,20 +247,21 @@ export function usePreviewOffer(options) {
243
247
  finally {
244
248
  setIsPaying(false);
245
249
  }
246
- }, [offerId, requestedCurrency, lineItemsForPreview, offersResource, isPaying]);
247
- // Create checkout session without paying (for landing pages)
250
+ }, [offerId, effectiveCurrency, lineItemsForPreview, offersResource, isPaying]);
251
+ // Create checkout session without paying (for landing pages) - async mode for fast response
248
252
  const toCheckout = useCallback(async (mainOrderId) => {
249
253
  if (isPaying) {
250
254
  throw new Error('Operation already in progress');
251
255
  }
252
256
  setIsPaying(true);
253
257
  try {
254
- const result = await offersResource.toCheckout(offerId, requestedCurrency, lineItemsForPreview, typeof window !== 'undefined' ? window.location.href : undefined, mainOrderId);
255
- console.log('[usePreviewOffer] Checkout session created:', result);
258
+ // Use async mode for fast response (~50ms vs 2-5s)
259
+ const result = await offersResource.toCheckoutAsync(offerId, effectiveCurrency, lineItemsForPreview, typeof window !== 'undefined' ? window.location.href : undefined, mainOrderId);
260
+ console.log('[usePreviewOffer] Checkout session created (async):', result);
256
261
  return {
257
- checkoutSessionId: result.checkoutSessionId,
258
262
  checkoutToken: result.checkoutToken,
259
- checkoutUrl: result.checkoutUrl,
263
+ customerId: result.customerId,
264
+ status: result.status,
260
265
  };
261
266
  }
262
267
  catch (error) {
@@ -266,7 +271,7 @@ export function usePreviewOffer(options) {
266
271
  finally {
267
272
  setIsPaying(false);
268
273
  }
269
- }, [offerId, requestedCurrency, lineItemsForPreview, offersResource, isPaying]);
274
+ }, [offerId, effectiveCurrency, lineItemsForPreview, offersResource, isPaying]);
270
275
  return {
271
276
  offer,
272
277
  isLoading,
@@ -54,19 +54,27 @@ export const useTranslation = (options = {}) => {
54
54
  const { defaultLanguage = 'en', currentLanguage } = options;
55
55
  // Get the current language from query params, browser, or fallback to default
56
56
  const locale = useMemo(() => {
57
+ // Normalizes language codes like 'fr-FR' or 'pt_BR' to just 'fr' / 'pt'
58
+ const normalizeLanguageCode = (code) => {
59
+ if (!code)
60
+ return undefined;
61
+ return code.split(/[-_]/)[0].toLowerCase();
62
+ };
57
63
  if (currentLanguage)
58
64
  return currentLanguage;
59
65
  // Check for language query parameter
60
66
  if (typeof window !== 'undefined') {
61
67
  const urlParams = new URLSearchParams(window.location.search);
62
68
  const langFromQuery = urlParams.get('locale');
63
- if (langFromQuery)
64
- return langFromQuery;
69
+ const normalizedFromQuery = normalizeLanguageCode(langFromQuery);
70
+ if (normalizedFromQuery)
71
+ return normalizedFromQuery;
65
72
  }
66
73
  // Try to get browser language
67
74
  if (typeof navigator !== 'undefined') {
68
- const browserLang = navigator.language.split('-')[0]; // e.g., 'en-US' -> 'en'
69
- return browserLang;
75
+ const browserLang = normalizeLanguageCode(navigator.language); // e.g., 'en-US' -> 'en'
76
+ if (browserLang)
77
+ return browserLang;
70
78
  }
71
79
  return defaultLanguage;
72
80
  }, [defaultLanguage, currentLanguage]);
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  * TagadaProvider - Main provider component for the Tagada Pay React SDK
5
5
  */
6
6
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
7
- import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
7
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
8
8
  import { ApiService } from '../../../react/services/apiService';
9
9
  import { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits, } from '../../../react/utils/money';
10
10
  import { TagadaClient } from '../../core/client';
@@ -211,6 +211,8 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
211
211
  formatSimpleMoney,
212
212
  }), []);
213
213
  const [isDebugDrawerOpen, setIsDebugDrawerOpen] = useState(false);
214
+ // Track last injected script to prevent duplicate execution
215
+ const lastInjectedScriptRef = useRef(null);
214
216
  // Funnel Methods
215
217
  const funnelMethods = useMemo(() => {
216
218
  if (!client.funnel) {
@@ -283,6 +285,63 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
283
285
  },
284
286
  };
285
287
  }, [client, state.auth.session, state.store, funnelId, onNavigate]);
288
+ // Inject funnel script into the page
289
+ useEffect(() => {
290
+ // Only run in browser environment
291
+ if (typeof document === 'undefined') {
292
+ return;
293
+ }
294
+ const scriptContent = funnelState.context?.script;
295
+ const scriptId = 'tagada-funnel-script';
296
+ if (!scriptContent || !scriptContent.trim()) {
297
+ // Clear ref if script is removed
298
+ lastInjectedScriptRef.current = null;
299
+ // Remove existing script if it exists
300
+ const existingScript = document.getElementById(scriptId);
301
+ if (existingScript) {
302
+ existingScript.remove();
303
+ }
304
+ return;
305
+ }
306
+ // Extract script content (remove <script> tags if present)
307
+ let scriptBody = scriptContent.trim();
308
+ // Check if script is wrapped in <script> tags
309
+ const scriptTagMatch = scriptBody.match(/^<script[^>]*>([\s\S]*)<\/script>$/i);
310
+ if (scriptTagMatch) {
311
+ scriptBody = scriptTagMatch[1].trim();
312
+ }
313
+ // Skip if script body is empty after extraction
314
+ if (!scriptBody) {
315
+ return;
316
+ }
317
+ // Prevent duplicate injection of the same script content
318
+ // This handles React StrictMode double-execution in development
319
+ if (lastInjectedScriptRef.current === scriptBody) {
320
+ return;
321
+ }
322
+ // Remove existing script if it exists (for script updates)
323
+ const existingScript = document.getElementById(scriptId);
324
+ if (existingScript) {
325
+ existingScript.remove();
326
+ }
327
+ // Create and inject new script element
328
+ const scriptElement = document.createElement('script');
329
+ scriptElement.id = scriptId;
330
+ scriptElement.textContent = scriptBody;
331
+ document.body.appendChild(scriptElement);
332
+ // Track this script content to prevent re-injection (handles React StrictMode double-execution)
333
+ lastInjectedScriptRef.current = scriptBody;
334
+ // Cleanup: remove script element but keep ref to prevent re-injection on StrictMode second run
335
+ return () => {
336
+ const scriptToRemove = document.getElementById(scriptId);
337
+ if (scriptToRemove) {
338
+ scriptToRemove.remove();
339
+ }
340
+ // Note: We intentionally DON'T clear lastInjectedScriptRef here
341
+ // This prevents React StrictMode from re-injecting the same script on the second run
342
+ // The ref will be cleared when script content actually changes (next effect run)
343
+ };
344
+ }, [funnelState.context?.script]);
286
345
  const contextValue = {
287
346
  client,
288
347
  ...state,
@@ -301,6 +360,7 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
301
360
  refreshCoordinator,
302
361
  money: moneyUtils,
303
362
  };
363
+ console.log('contextValue', contextValue, contextValue.funnel.currentStep);
304
364
  // Query Client
305
365
  const [queryClient] = useState(() => new QueryClient({
306
366
  defaultOptions: {
@@ -8,12 +8,13 @@
8
8
  */
9
9
  import { TagadaClient, TagadaClientConfig, TagadaState } from '../core/client';
10
10
  import { ApiClient } from '../core/resources/apiClient';
11
+ import { CheckoutResource } from '../core/resources/checkout';
11
12
  /**
12
13
  * Factory function to create a Tagada Client instance.
13
14
  * Features (like funnel) can be toggled via the config.
14
15
  */
15
16
  export declare function createTagadaClient(config?: TagadaClientConfig): TagadaClient;
16
- export { TagadaClient, ApiClient };
17
+ export { TagadaClient, ApiClient, CheckoutResource };
17
18
  export type { TagadaClientConfig, TagadaState };
18
19
  export { FunnelActionType } from '../core/resources/funnel';
19
20
  export type { FunnelAction, FunnelNavigationResult, SimpleFunnelContext } from '../core/resources/funnel';
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { TagadaClient } from '../core/client';
10
10
  import { ApiClient } from '../core/resources/apiClient';
11
+ import { CheckoutResource } from '../core/resources/checkout';
11
12
  /**
12
13
  * Factory function to create a Tagada Client instance.
13
14
  * Features (like funnel) can be toggled via the config.
@@ -16,7 +17,7 @@ export function createTagadaClient(config = {}) {
16
17
  return new TagadaClient(config);
17
18
  }
18
19
  // Re-export Core Classes
19
- export { TagadaClient, ApiClient };
20
+ export { TagadaClient, ApiClient, CheckoutResource };
20
21
  export { FunnelActionType } from '../core/resources/funnel';
21
22
  // Re-export Utilities
22
23
  export * from '../core/utils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagadapay/plugin-sdk",
3
- "version": "3.0.12",
3
+ "version": "3.0.15",
4
4
  "description": "Modern React SDK for building Tagada Pay plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -73,7 +73,9 @@
73
73
  "@basis-theory/basis-theory-react": "^1.32.5",
74
74
  "@basis-theory/web-threeds": "^1.0.1",
75
75
  "@google-pay/button-react": "^3.0.10",
76
+ "@tagadapay/plugin-sdk": "link:",
76
77
  "@tanstack/react-query": "^5.90.2",
78
+ "@ua-parser-js/pro-enterprise": "^2.0.6",
77
79
  "axios": "^1.10.0",
78
80
  "iso3166-2-db": "^2.3.11",
79
81
  "path-to-regexp": "^8.2.0",