@tagadapay/plugin-sdk 3.0.12 → 3.0.14

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.
@@ -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]);
@@ -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.14",
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",