@tagadapay/plugin-sdk 3.0.3 → 3.0.9

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 (48) hide show
  1. package/build-cdn.js +113 -0
  2. package/dist/config/basisTheory.d.ts +26 -0
  3. package/dist/config/basisTheory.js +29 -0
  4. package/dist/external-tracker.js +4947 -0
  5. package/dist/external-tracker.min.js +11 -0
  6. package/dist/external-tracker.min.js.map +7 -0
  7. package/dist/react/config/payment.d.ts +8 -8
  8. package/dist/react/config/payment.js +17 -21
  9. package/dist/react/hooks/useApplePay.js +1 -1
  10. package/dist/react/hooks/usePayment.js +1 -3
  11. package/dist/react/hooks/useThreeds.js +2 -2
  12. package/dist/v2/core/client.d.ts +30 -3
  13. package/dist/v2/core/client.js +219 -8
  14. package/dist/v2/core/config/environment.d.ts +16 -3
  15. package/dist/v2/core/config/environment.js +72 -3
  16. package/dist/v2/core/funnelClient.d.ts +4 -0
  17. package/dist/v2/core/funnelClient.js +106 -4
  18. package/dist/v2/core/resources/funnel.d.ts +22 -0
  19. package/dist/v2/core/resources/offers.d.ts +64 -3
  20. package/dist/v2/core/resources/offers.js +112 -10
  21. package/dist/v2/core/resources/postPurchases.js +4 -1
  22. package/dist/v2/core/utils/configHotReload.d.ts +39 -0
  23. package/dist/v2/core/utils/configHotReload.js +75 -0
  24. package/dist/v2/core/utils/eventBus.d.ts +11 -0
  25. package/dist/v2/core/utils/eventBus.js +34 -0
  26. package/dist/v2/core/utils/pluginConfig.d.ts +14 -5
  27. package/dist/v2/core/utils/pluginConfig.js +74 -59
  28. package/dist/v2/core/utils/previewMode.d.ts +114 -0
  29. package/dist/v2/core/utils/previewMode.js +379 -0
  30. package/dist/v2/core/utils/sessionStorage.d.ts +5 -0
  31. package/dist/v2/core/utils/sessionStorage.js +22 -0
  32. package/dist/v2/index.d.ts +4 -1
  33. package/dist/v2/index.js +3 -1
  34. package/dist/v2/react/hooks/useOfferQuery.js +50 -17
  35. package/dist/v2/react/hooks/usePaymentQuery.js +1 -3
  36. package/dist/v2/react/hooks/usePreviewOffer.d.ts +84 -0
  37. package/dist/v2/react/hooks/usePreviewOffer.js +290 -0
  38. package/dist/v2/react/hooks/useThreeds.js +2 -2
  39. package/dist/v2/react/index.d.ts +2 -0
  40. package/dist/v2/react/index.js +1 -0
  41. package/dist/v2/react/providers/TagadaProvider.js +49 -32
  42. package/dist/v2/standalone/external-tracker.d.ts +119 -0
  43. package/dist/v2/standalone/external-tracker.js +260 -0
  44. package/dist/v2/standalone/index.d.ts +2 -0
  45. package/dist/v2/standalone/index.js +6 -0
  46. package/package.json +11 -3
  47. package/dist/v2/react/hooks/useOffersQuery.d.ts +0 -12
  48. package/dist/v2/react/hooks/useOffersQuery.js +0 -404
@@ -0,0 +1,379 @@
1
+ /**
2
+ * SDK Override Parameters
3
+ *
4
+ * Centralized parameter management for SDK behavior overrides.
5
+ * Parameters can come from URL query params, localStorage, or cookies.
6
+ *
7
+ * Key concepts:
8
+ * - forceReset: Clears all stored state (localStorage, cookies) - simulates hard refresh
9
+ * - draft: Marks customers/sessions as draft (for staging/preview/testing)
10
+ * - funnelTracking: Controls whether funnel events are tracked
11
+ * - tagadaClientEnv: Forces specific environment (production/development/local) - overrides auto-detection
12
+ * - tagadaClientBaseUrl: Forces custom API base URL - overrides environment-based URL
13
+ * - token: Authentication token (URL > localStorage)
14
+ * - funnelSessionId: Active funnel session (URL > cookie)
15
+ *
16
+ * Usage examples:
17
+ * - Force production API: ?tagadaClientEnv=production
18
+ * - Force development API: ?tagadaClientEnv=development
19
+ * - Force local API: ?tagadaClientEnv=local
20
+ * - Custom API URL: ?tagadaClientBaseUrl=https://tagada.loclx.io
21
+ * - Combined: ?tagadaClientEnv=local&tagadaClientBaseUrl=https://tagada.loclx.io
22
+ * - Hard reset + production: ?forceReset=true&tagadaClientEnv=production
23
+ */
24
+ import { clearClientToken, setClientToken, getClientToken } from './tokenStorage';
25
+ import { clearFunnelSessionCookie } from './sessionStorage';
26
+ // Storage keys for SDK override params
27
+ const STORAGE_KEYS = {
28
+ DRAFT: 'tgd_draft',
29
+ FUNNEL_TRACKING: 'tgd_funnel_tracking',
30
+ FORCE_RESET: 'tgd_force_reset',
31
+ CLIENT_ENV: 'tgd_client_env',
32
+ CLIENT_BASE_URL: 'tgd_client_base_url',
33
+ };
34
+ /**
35
+ * Check if force reset is active (simulates hard refresh)
36
+ */
37
+ export function isForceReset() {
38
+ if (typeof window === 'undefined')
39
+ return false;
40
+ const params = new URLSearchParams(window.location.search);
41
+ return params.get('forceReset') === 'true';
42
+ }
43
+ /**
44
+ * Get value from localStorage with fallback
45
+ */
46
+ function getFromStorage(key) {
47
+ if (typeof window === 'undefined')
48
+ return null;
49
+ try {
50
+ return localStorage.getItem(key);
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ /**
57
+ * Set value in localStorage
58
+ */
59
+ function setInStorage(key, value) {
60
+ if (typeof window === 'undefined')
61
+ return;
62
+ try {
63
+ localStorage.setItem(key, value);
64
+ }
65
+ catch {
66
+ // Storage not available
67
+ }
68
+ }
69
+ /**
70
+ * Get value from cookie
71
+ */
72
+ function getFromCookie(key) {
73
+ if (typeof document === 'undefined')
74
+ return null;
75
+ const cookies = document.cookie.split(';');
76
+ const cookie = cookies.find(c => c.trim().startsWith(`${key}=`));
77
+ return cookie ? cookie.split('=')[1]?.trim() : null;
78
+ }
79
+ /**
80
+ * Set value in cookie
81
+ */
82
+ function setInCookie(key, value, maxAge = 86400) {
83
+ if (typeof document === 'undefined')
84
+ return;
85
+ document.cookie = `${key}=${value}; path=/; max-age=${maxAge}`;
86
+ }
87
+ /**
88
+ * Clear value from cookie
89
+ */
90
+ function clearFromCookie(key) {
91
+ if (typeof document === 'undefined')
92
+ return;
93
+ document.cookie = `${key}=; path=/; max-age=0`;
94
+ }
95
+ /**
96
+ * Get SDK override parameters from all sources (URL, localStorage, cookies)
97
+ * Priority: URL > localStorage > Cookie
98
+ */
99
+ export function getSDKParams() {
100
+ if (typeof window === 'undefined') {
101
+ return {};
102
+ }
103
+ const urlParams = new URLSearchParams(window.location.search);
104
+ // Get draft mode (URL > localStorage > cookie)
105
+ let draft;
106
+ const urlDraft = urlParams.get('draft');
107
+ if (urlDraft !== null) {
108
+ draft = urlDraft === 'true';
109
+ }
110
+ else {
111
+ const storageDraft = getFromStorage(STORAGE_KEYS.DRAFT) || getFromCookie(STORAGE_KEYS.DRAFT);
112
+ if (storageDraft !== null) {
113
+ draft = storageDraft === 'true';
114
+ }
115
+ }
116
+ // Get funnel tracking (URL > localStorage > cookie, default true)
117
+ let funnelTracking;
118
+ const urlTracking = urlParams.get('funnelTracking');
119
+ if (urlTracking !== null) {
120
+ funnelTracking = urlTracking !== 'false';
121
+ }
122
+ else {
123
+ const storageTracking = getFromStorage(STORAGE_KEYS.FUNNEL_TRACKING) || getFromCookie(STORAGE_KEYS.FUNNEL_TRACKING);
124
+ if (storageTracking !== null) {
125
+ funnelTracking = storageTracking !== 'false';
126
+ }
127
+ }
128
+ // Get token (URL > localStorage)
129
+ const token = urlParams.get('token') || getClientToken() || null;
130
+ // Get funnel session ID (URL only, cookie handled elsewhere)
131
+ const funnelSessionId = urlParams.get('funnelSessionId') || null;
132
+ // Get funnel ID (URL only)
133
+ const funnelId = urlParams.get('funnelId') || null;
134
+ // Force reset is URL-only (not persisted)
135
+ const forceReset = urlParams.get('forceReset') === 'true';
136
+ // Get client environment override (URL > localStorage > cookie)
137
+ let tagadaClientEnv;
138
+ const urlEnv = urlParams.get('tagadaClientEnv');
139
+ if (urlEnv && (urlEnv === 'production' || urlEnv === 'development' || urlEnv === 'local')) {
140
+ tagadaClientEnv = urlEnv;
141
+ }
142
+ else {
143
+ const storageEnv = getFromStorage(STORAGE_KEYS.CLIENT_ENV) || getFromCookie(STORAGE_KEYS.CLIENT_ENV);
144
+ if (storageEnv && (storageEnv === 'production' || storageEnv === 'development' || storageEnv === 'local')) {
145
+ tagadaClientEnv = storageEnv;
146
+ }
147
+ }
148
+ // Get custom API base URL override (URL > localStorage > cookie)
149
+ let tagadaClientBaseUrl;
150
+ const urlBaseUrl = urlParams.get('tagadaClientBaseUrl');
151
+ if (urlBaseUrl) {
152
+ tagadaClientBaseUrl = urlBaseUrl;
153
+ }
154
+ else {
155
+ const storageBaseUrl = getFromStorage(STORAGE_KEYS.CLIENT_BASE_URL) || getFromCookie(STORAGE_KEYS.CLIENT_BASE_URL);
156
+ if (storageBaseUrl) {
157
+ tagadaClientBaseUrl = storageBaseUrl;
158
+ }
159
+ }
160
+ return {
161
+ forceReset,
162
+ token,
163
+ funnelSessionId,
164
+ funnelId,
165
+ draft,
166
+ funnelTracking,
167
+ tagadaClientEnv,
168
+ tagadaClientBaseUrl,
169
+ };
170
+ }
171
+ /**
172
+ * Legacy alias for backward compatibility
173
+ * @deprecated Use getSDKParams() instead
174
+ */
175
+ export function getPreviewParams() {
176
+ return getSDKParams();
177
+ }
178
+ /**
179
+ * Check if draft mode is active
180
+ * Uses centralized getSDKParams()
181
+ */
182
+ export function isDraftMode() {
183
+ const params = getSDKParams();
184
+ return params.draft ?? false;
185
+ }
186
+ /**
187
+ * Set draft mode in storage for persistence
188
+ */
189
+ export function setDraftMode(draft) {
190
+ if (typeof window === 'undefined')
191
+ return;
192
+ if (draft) {
193
+ setInStorage(STORAGE_KEYS.DRAFT, 'true');
194
+ setInCookie(STORAGE_KEYS.DRAFT, 'true', 86400); // 24 hours
195
+ }
196
+ else {
197
+ setInStorage(STORAGE_KEYS.DRAFT, 'false');
198
+ clearFromCookie(STORAGE_KEYS.DRAFT);
199
+ }
200
+ }
201
+ /**
202
+ * Initialize SDK with override parameters
203
+ *
204
+ * This function handles SDK initialization based on override params:
205
+ * 1. If forceReset flag is set: Clear all stored state (simulates hard refresh)
206
+ * 2. If token in URL: Use it (override localStorage)
207
+ * 3. If draft in URL: Persist it to storage
208
+ * 4. If funnelTracking in URL: Persist it to storage
209
+ *
210
+ * @returns True if force reset was activated and state was cleared
211
+ */
212
+ export function handlePreviewMode(debugMode = false) {
213
+ const params = getSDKParams();
214
+ const shouldReset = params.forceReset || false;
215
+ if (!shouldReset && !params.token) {
216
+ // Not forcing reset and no explicit token override
217
+ // But still persist draft/tracking if in URL
218
+ persistSDKParamsFromURL();
219
+ return false;
220
+ }
221
+ if (debugMode) {
222
+ console.log('[SDK] Detected params:', params);
223
+ }
224
+ // CASE 1: Force reset - clear all state (simulates hard refresh)
225
+ if (shouldReset) {
226
+ if (debugMode) {
227
+ console.log('[SDK] Force reset: Clearing all stored state');
228
+ }
229
+ clearClientToken();
230
+ clearFunnelSessionCookie();
231
+ // Clear all tagadapay-related localStorage keys
232
+ if (typeof window !== 'undefined' && window.localStorage) {
233
+ const keys = Object.keys(localStorage);
234
+ keys.forEach(key => {
235
+ if (key.startsWith('tagadapay_') || key.startsWith('tgd_')) {
236
+ if (debugMode) {
237
+ console.log(`[SDK] Clearing localStorage: ${key}`);
238
+ }
239
+ localStorage.removeItem(key);
240
+ }
241
+ });
242
+ }
243
+ }
244
+ // CASE 2: Token in URL - override stored token
245
+ if (params.token !== null && params.token !== undefined) {
246
+ if (debugMode) {
247
+ console.log('[SDK] Using token from URL:', params.token.substring(0, 20) + '...');
248
+ }
249
+ if (params.token === '' || params.token === 'null') {
250
+ // Explicitly cleared token
251
+ clearClientToken();
252
+ }
253
+ else {
254
+ // Set token from URL
255
+ setClientToken(params.token);
256
+ }
257
+ }
258
+ else if (shouldReset) {
259
+ // Force reset but no token = clear stored token
260
+ if (debugMode) {
261
+ console.log('[SDK] Force reset mode (no token)');
262
+ }
263
+ clearClientToken();
264
+ }
265
+ // CASE 3: Persist URL params to storage
266
+ persistSDKParamsFromURL();
267
+ // CASE 4: Funnel session ID in URL - will be used by FunnelClient
268
+ if (params.funnelSessionId && debugMode) {
269
+ console.log('[SDK] Using funnelSessionId from URL:', params.funnelSessionId);
270
+ }
271
+ return shouldReset;
272
+ }
273
+ /**
274
+ * Persist SDK parameters from URL to storage
275
+ * This ensures params survive page navigation
276
+ */
277
+ function persistSDKParamsFromURL() {
278
+ if (typeof window === 'undefined')
279
+ return;
280
+ const urlParams = new URLSearchParams(window.location.search);
281
+ // Persist draft mode if in URL
282
+ const urlDraft = urlParams.get('draft');
283
+ if (urlDraft !== null) {
284
+ setDraftMode(urlDraft === 'true');
285
+ }
286
+ // Persist funnel tracking if in URL
287
+ const urlTracking = urlParams.get('funnelTracking');
288
+ if (urlTracking !== null) {
289
+ setFunnelTracking(urlTracking !== 'false');
290
+ }
291
+ // Persist client environment if in URL
292
+ const urlEnv = urlParams.get('tagadaClientEnv');
293
+ if (urlEnv && (urlEnv === 'production' || urlEnv === 'development' || urlEnv === 'local')) {
294
+ setClientEnvironment(urlEnv);
295
+ }
296
+ // Persist custom base URL if in URL
297
+ const urlBaseUrl = urlParams.get('tagadaClientBaseUrl');
298
+ if (urlBaseUrl) {
299
+ setClientBaseUrl(urlBaseUrl);
300
+ }
301
+ }
302
+ /**
303
+ * Set funnel tracking mode in storage for persistence
304
+ */
305
+ export function setFunnelTracking(enabled) {
306
+ if (typeof window === 'undefined')
307
+ return;
308
+ const value = enabled ? 'true' : 'false';
309
+ setInStorage(STORAGE_KEYS.FUNNEL_TRACKING, value);
310
+ setInCookie(STORAGE_KEYS.FUNNEL_TRACKING, value, 86400); // 24 hours
311
+ }
312
+ /**
313
+ * Set client environment override in storage for persistence
314
+ */
315
+ export function setClientEnvironment(env) {
316
+ if (typeof window === 'undefined')
317
+ return;
318
+ setInStorage(STORAGE_KEYS.CLIENT_ENV, env);
319
+ setInCookie(STORAGE_KEYS.CLIENT_ENV, env, 86400); // 24 hours
320
+ }
321
+ /**
322
+ * Clear client environment override
323
+ */
324
+ export function clearClientEnvironment() {
325
+ if (typeof window === 'undefined')
326
+ return;
327
+ try {
328
+ localStorage.removeItem(STORAGE_KEYS.CLIENT_ENV);
329
+ clearFromCookie(STORAGE_KEYS.CLIENT_ENV);
330
+ }
331
+ catch {
332
+ // Storage not available
333
+ }
334
+ }
335
+ /**
336
+ * Set custom API base URL override in storage for persistence
337
+ */
338
+ export function setClientBaseUrl(baseUrl) {
339
+ if (typeof window === 'undefined')
340
+ return;
341
+ setInStorage(STORAGE_KEYS.CLIENT_BASE_URL, baseUrl);
342
+ setInCookie(STORAGE_KEYS.CLIENT_BASE_URL, baseUrl, 86400); // 24 hours
343
+ }
344
+ /**
345
+ * Clear custom API base URL override
346
+ */
347
+ export function clearClientBaseUrl() {
348
+ if (typeof window === 'undefined')
349
+ return;
350
+ try {
351
+ localStorage.removeItem(STORAGE_KEYS.CLIENT_BASE_URL);
352
+ clearFromCookie(STORAGE_KEYS.CLIENT_BASE_URL);
353
+ }
354
+ catch {
355
+ // Storage not available
356
+ }
357
+ }
358
+ /**
359
+ * Check if we should use URL params over stored values
360
+ *
361
+ * Returns true if:
362
+ * - Force reset is active, OR
363
+ * - Explicit token/sessionId in URL (indicating intentional override)
364
+ */
365
+ export function shouldUseUrlParams() {
366
+ const params = getSDKParams();
367
+ return !!(params.forceReset ||
368
+ params.token !== null ||
369
+ params.funnelSessionId !== null);
370
+ }
371
+ /**
372
+ * Check if funnel tracking is enabled
373
+ * Uses centralized getSDKParams()
374
+ * Default is true for normal operations
375
+ */
376
+ export function isFunnelTrackingEnabled() {
377
+ const params = getSDKParams();
378
+ return params.funnelTracking ?? true; // Default true
379
+ }
@@ -18,3 +18,8 @@ export declare function clearFunnelSessionCookie(): void;
18
18
  * Check if a funnel session cookie exists
19
19
  */
20
20
  export declare function hasFunnelSessionCookie(): boolean;
21
+ /**
22
+ * Retrieve funnel variant ID from sticky session (for A/B tracking)
23
+ * The variant ID is stored in the sticky session metadata by plugin-routing
24
+ */
25
+ export declare function getFunnelVariantId(): string | undefined;
@@ -3,6 +3,7 @@
3
3
  * Handles session persistence using browser cookies
4
4
  */
5
5
  const FUNNEL_SESSION_COOKIE_NAME = 'tgd-funnel-session-id';
6
+ const STICKY_SESSION_COOKIE_NAME = 'tgd-session-id';
6
7
  const SESSION_MAX_AGE = 30 * 24 * 60 * 60; // 30 days
7
8
  /**
8
9
  * Save funnel session ID to browser cookie
@@ -37,3 +38,24 @@ export function clearFunnelSessionCookie() {
37
38
  export function hasFunnelSessionCookie() {
38
39
  return !!getFunnelSessionCookie();
39
40
  }
41
+ /**
42
+ * Retrieve funnel variant ID from sticky session (for A/B tracking)
43
+ * The variant ID is stored in the sticky session metadata by plugin-routing
44
+ */
45
+ export function getFunnelVariantId() {
46
+ if (typeof document === 'undefined')
47
+ return undefined;
48
+ try {
49
+ const cookie = document.cookie
50
+ .split('; ')
51
+ .find((row) => row.startsWith(`${STICKY_SESSION_COOKIE_NAME}=`));
52
+ if (!cookie)
53
+ return undefined;
54
+ const sessionData = JSON.parse(decodeURIComponent(cookie.split('=')[1]));
55
+ return sessionData?.metadata?.funnelVariantId;
56
+ }
57
+ catch (error) {
58
+ console.warn('Failed to parse sticky session for variant ID:', error);
59
+ return undefined;
60
+ }
61
+ }
@@ -10,6 +10,8 @@ export * from './core/isoData';
10
10
  export * from './core/utils/currency';
11
11
  export * from './core/utils/pluginConfig';
12
12
  export * from './core/utils/products';
13
+ export * from './core/utils/previewMode';
14
+ export * from './core/utils/configHotReload';
13
15
  export * from './core/pathRemapping';
14
16
  export type { CheckoutData, CheckoutInitParams, CheckoutLineItem, CheckoutSession, CheckoutSessionPreview, Promotion } from './core/resources/checkout';
15
17
  export type { Order, OrderLineItem } from './core/utils/order';
@@ -23,7 +25,7 @@ export type { ToggleOrderBumpResponse, VipOffer, VipPreviewResponse } from './co
23
25
  export type { StoreConfig } from './core/resources/storeConfig';
24
26
  export { FunnelActionType } from './core/resources/funnel';
25
27
  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';
26
- export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
28
+ export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
27
29
  export type { DebugScript } from './react';
28
30
  export type { TranslateFunction, TranslationText, UseTranslationOptions, UseTranslationResult } from './react/hooks/useTranslation';
29
31
  export type { FunnelContextValue } from './react/hooks/useFunnel';
@@ -36,3 +38,4 @@ export type { UseCustomerInfosOptions, UseCustomerInfosResult } from './react/ho
36
38
  export type { UseCustomerOrdersOptions, UseCustomerOrdersResult } from './react/hooks/useCustomerOrders';
37
39
  export type { UseCustomerSubscriptionsOptions, UseCustomerSubscriptionsResult } from './react/hooks/useCustomerSubscriptions';
38
40
  export type { UseShippingRatesQueryOptions, UseShippingRatesQueryResult } from './react/hooks/useShippingRatesQuery';
41
+ export type { PreviewOfferSummary, UsePreviewOfferOptions, UsePreviewOfferResult } from './react/hooks/usePreviewOffer';
package/dist/v2/index.js CHANGED
@@ -11,8 +11,10 @@ export * from './core/isoData';
11
11
  export * from './core/utils/currency';
12
12
  export * from './core/utils/pluginConfig';
13
13
  export * from './core/utils/products';
14
+ export * from './core/utils/previewMode';
15
+ export * from './core/utils/configHotReload';
14
16
  // Path remapping helpers (framework-agnostic)
15
17
  export * from './core/pathRemapping';
16
18
  export { FunnelActionType } from './core/resources/funnel';
17
19
  // React exports (hooks and components only, types are exported above)
18
- export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
20
+ export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
@@ -15,9 +15,12 @@ import { useTagadaContext } from '../providers/TagadaProvider';
15
15
  import { getGlobalApiClient } from './useApiQuery';
16
16
  import { usePluginConfig } from './usePluginConfig';
17
17
  export function useOfferQuery(options) {
18
- const { offerId, enabled = true, mainOrderId } = options;
18
+ const { offerId, enabled = true, mainOrderId: rawMainOrderId } = options;
19
19
  const { storeId } = usePluginConfig();
20
20
  const { isSessionInitialized, session } = useTagadaContext();
21
+ // 🎯 Normalize mainOrderId: treat undefined and empty string as the same
22
+ // This prevents effect re-runs when value changes from undefined to ''
23
+ const mainOrderId = rawMainOrderId || undefined;
21
24
  // ─────────────────────────────────────────────────────────────────────────
22
25
  // State
23
26
  // ─────────────────────────────────────────────────────────────────────────
@@ -140,17 +143,28 @@ export function useOfferQuery(options) {
140
143
  });
141
144
  }, [initCheckoutSessionAsync, session?.customerId, isSessionInitialized]);
142
145
  // ─────────────────────────────────────────────────────────────────────────
143
- // 2. Auto-initialize checkout session when mainOrderId is provided
146
+ // 2. Auto-initialize checkout session when offer is loaded
147
+ // (mainOrderId is optional - used for post-purchase upsells, not needed for standalone offers)
144
148
  // ─────────────────────────────────────────────────────────────────────────
145
149
  useEffect(() => {
146
- if (!offer || !mainOrderId || !isSessionInitialized)
150
+ // Only require offer and session to be ready (mainOrderId is optional)
151
+ if (!offer || !isSessionInitialized) {
147
152
  return;
148
- if (checkoutSession.checkoutSessionId || isInitializing || hasInitializedRef.current)
153
+ }
154
+ if (checkoutSession.checkoutSessionId || isInitializing) {
155
+ return;
156
+ }
157
+ // 🎯 CRITICAL: Check and set initialization flag atomically
158
+ // This ensures only ONE effect can proceed with initialization
159
+ // Once set to true, it stays true (never reset) to prevent duplicate inits
160
+ if (hasInitializedRef.current) {
149
161
  return;
162
+ }
163
+ hasInitializedRef.current = true;
150
164
  const initSession = async () => {
151
- hasInitializedRef.current = true;
152
165
  setIsInitializing(true);
153
166
  try {
167
+ // mainOrderId is optional - pass undefined if not provided
154
168
  const result = await initCheckoutSession(offerId, mainOrderId);
155
169
  setCheckoutSession(prev => ({
156
170
  ...prev,
@@ -160,7 +174,7 @@ export function useOfferQuery(options) {
160
174
  await fetchOrderSummary(result.checkoutSessionId);
161
175
  }
162
176
  catch (err) {
163
- console.error('Failed to init checkout session:', err);
177
+ console.error('[useOfferQuery] Failed to init checkout session:', err);
164
178
  hasInitializedRef.current = false; // Allow retry on error
165
179
  }
166
180
  finally {
@@ -198,7 +212,8 @@ export function useOfferQuery(options) {
198
212
  const product = variant?.product;
199
213
  if (!variant || !product)
200
214
  continue;
201
- const currency = offer.summaries?.[0]?.currency || 'USD';
215
+ // Currency is determined from the checkout session, not from the offer
216
+ const currency = checkoutSession.orderSummary?.currency || 'USD';
202
217
  const currencyOption = price?.currencyOptions?.[currency];
203
218
  const unitAmount = currencyOption?.amount ?? variant.price ?? 0;
204
219
  items.push({
@@ -282,21 +297,39 @@ export function useOfferQuery(options) {
282
297
  const orderSummary = checkoutSession.orderSummary;
283
298
  if (orderSummary?.options?.[productId]) {
284
299
  const currency = orderSummary.currency || 'USD';
285
- return orderSummary.options[productId].map((variant) => ({
286
- variantId: variant.id,
287
- variantName: variant.name,
288
- sku: variant.sku || null,
289
- imageUrl: variant.imageUrl || null,
290
- unitAmount: variant.prices?.[0]?.currencyOptions?.[currency]?.amount ?? 0,
291
- currency,
292
- isDefault: variant.default ?? false,
293
- }));
300
+ // Find the active line item for this product to get the correct priceId
301
+ const activeLineItem = orderSummary.items?.find((item) => item.productId === productId);
302
+ const activePriceId = activeLineItem?.priceId;
303
+ return orderSummary.options[productId].map((variant) => {
304
+ // Try to find the price that matches the active priceId, otherwise use first price
305
+ let unitAmount = 0;
306
+ if (activePriceId && variant.prices) {
307
+ const matchingPrice = variant.prices.find((p) => p.id === activePriceId);
308
+ if (matchingPrice?.currencyOptions?.[currency]?.amount) {
309
+ unitAmount = matchingPrice.currencyOptions[currency].amount;
310
+ }
311
+ }
312
+ // Fallback to first price if no match found
313
+ if (unitAmount === 0 && variant.prices?.[0]?.currencyOptions?.[currency]?.amount) {
314
+ unitAmount = variant.prices[0].currencyOptions[currency].amount;
315
+ }
316
+ return {
317
+ variantId: variant.id,
318
+ variantName: variant.name,
319
+ sku: variant.sku || null,
320
+ imageUrl: variant.imageUrl || null,
321
+ unitAmount,
322
+ currency,
323
+ isDefault: variant.default ?? false,
324
+ };
325
+ });
294
326
  }
295
327
  // Fallback to static offer data
296
328
  if (!offer?.offerLineItems)
297
329
  return [];
298
330
  const variants = [];
299
- const currency = offer.summaries?.[0]?.currency || 'USD';
331
+ // Currency is determined from the checkout session, not from the offer
332
+ const currency = checkoutSession.orderSummary?.currency || 'USD';
300
333
  for (const item of offer.offerLineItems) {
301
334
  const price = item.price;
302
335
  const variant = price?.variant;
@@ -28,10 +28,8 @@ export function usePaymentQuery() {
28
28
  const { createSession, startChallenge } = useThreeds();
29
29
  // Track challenge in progress to prevent multiple challenges
30
30
  const challengeInProgressRef = useRef(false);
31
- // Stabilize environment value to prevent re-renders
32
- const currentEnvironment = useMemo(() => environment?.environment || 'local', [environment?.environment]);
33
31
  // Get API key from embedded configuration with proper environment detection
34
- const apiKey = useMemo(() => getBasisTheoryApiKey(currentEnvironment), [currentEnvironment]);
32
+ const apiKey = useMemo(() => getBasisTheoryApiKey(), []); // Auto-detects environment
35
33
  // Initialize BasisTheory using React wrapper
36
34
  const { bt: basisTheory, error: btError } = useBasisTheory(apiKey, {
37
35
  elements: false,
@@ -0,0 +1,84 @@
1
+ /**
2
+ * usePreviewOffer - Lightweight offer preview without checkout sessions
3
+ *
4
+ * This hook provides a fast, client-side offer preview by:
5
+ * 1. Fetching offer data with pre-computed summaries
6
+ * 2. Allowing variant/quantity selection
7
+ * 3. Recalculating totals on-the-fly without backend calls
8
+ * 4. NO checkout session creation - perfect for browsing/previewing
9
+ */
10
+ import { Offer } from '../../core/resources/offers';
11
+ export interface PreviewLineItemSelection {
12
+ lineItemId: string;
13
+ variantId: string;
14
+ quantity: number;
15
+ priceId?: string;
16
+ }
17
+ export interface PreviewOfferSummary {
18
+ items: Array<{
19
+ id: string;
20
+ productId: string;
21
+ variantId: string;
22
+ priceId: string;
23
+ productName: string;
24
+ variantName: string;
25
+ imageUrl: string | null;
26
+ quantity: number;
27
+ unitAmount: number;
28
+ amount: number;
29
+ adjustedAmount: number;
30
+ currency: string;
31
+ }>;
32
+ totalAmount: number;
33
+ totalAdjustedAmount: number;
34
+ totalPromotionAmount: number;
35
+ currency: string;
36
+ options: Record<string, Array<{
37
+ variantId: string;
38
+ variantName: string;
39
+ sku: string | null;
40
+ imageUrl: string | null;
41
+ prices: Array<{
42
+ id: string;
43
+ currencyOptions: Record<string, {
44
+ amount: number;
45
+ currency: string;
46
+ }>;
47
+ }>;
48
+ default: boolean;
49
+ }>>;
50
+ }
51
+ export interface UsePreviewOfferOptions {
52
+ offerId: string;
53
+ currency?: string;
54
+ initialSelections?: Record<string, PreviewLineItemSelection>;
55
+ }
56
+ export interface UsePreviewOfferResult {
57
+ offer: Offer | null;
58
+ isLoading: boolean;
59
+ isFetching: boolean;
60
+ isPaying: boolean;
61
+ error: Error | null;
62
+ summary: PreviewOfferSummary | null;
63
+ selections: Record<string, PreviewLineItemSelection>;
64
+ selectVariant: (lineItemId: string, variantId: string) => void;
65
+ updateQuantity: (lineItemId: string, quantity: number) => void;
66
+ selectVariantByProduct: (productId: string, variantId: string) => void;
67
+ updateQuantityByProduct: (productId: string, quantity: number) => void;
68
+ getAvailableVariants: (productId: string) => Array<{
69
+ variantId: string;
70
+ variantName: string;
71
+ imageUrl: string | null;
72
+ unitAmount: number;
73
+ currency: string;
74
+ }>;
75
+ pay: (mainOrderId?: string) => Promise<{
76
+ checkoutUrl: string;
77
+ }>;
78
+ toCheckout: (mainOrderId?: string) => Promise<{
79
+ checkoutSessionId?: string;
80
+ checkoutToken?: string;
81
+ checkoutUrl: string;
82
+ }>;
83
+ }
84
+ export declare function usePreviewOffer(options: UsePreviewOfferOptions): UsePreviewOfferResult;