@tagadapay/plugin-sdk 3.1.24 → 3.1.25

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 (39) hide show
  1. package/dist/external-tracker.js +243 -2871
  2. package/dist/external-tracker.min.js +2 -2
  3. package/dist/external-tracker.min.js.map +4 -4
  4. package/dist/react/hooks/useCheckout.js +7 -2
  5. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  6. package/dist/tagada-react-sdk-minimal.min.js.map +3 -3
  7. package/dist/tagada-react-sdk.js +340 -173
  8. package/dist/tagada-react-sdk.min.js +2 -2
  9. package/dist/tagada-react-sdk.min.js.map +3 -3
  10. package/dist/tagada-sdk.js +776 -3327
  11. package/dist/tagada-sdk.min.js +2 -2
  12. package/dist/tagada-sdk.min.js.map +4 -4
  13. package/dist/v2/core/client.js +1 -0
  14. package/dist/v2/core/funnelClient.d.ts +8 -0
  15. package/dist/v2/core/funnelClient.js +1 -1
  16. package/dist/v2/core/resources/apiClient.d.ts +18 -14
  17. package/dist/v2/core/resources/apiClient.js +151 -109
  18. package/dist/v2/core/resources/checkout.d.ts +1 -1
  19. package/dist/v2/core/resources/index.d.ts +1 -1
  20. package/dist/v2/core/resources/index.js +1 -1
  21. package/dist/v2/core/resources/offers.js +4 -4
  22. package/dist/v2/core/resources/payments.d.ts +1 -0
  23. package/dist/v2/core/utils/currency.d.ts +3 -0
  24. package/dist/v2/core/utils/currency.js +40 -2
  25. package/dist/v2/core/utils/deviceInfo.d.ts +1 -0
  26. package/dist/v2/core/utils/deviceInfo.js +1 -0
  27. package/dist/v2/core/utils/previewMode.js +12 -0
  28. package/dist/v2/react/components/ApplePayButton.js +39 -16
  29. package/dist/v2/react/components/StripeExpressButton.js +1 -2
  30. package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
  31. package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
  32. package/dist/v2/react/hooks/useApiQuery.js +1 -1
  33. package/dist/v2/react/hooks/useCheckoutQuery.js +6 -2
  34. package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
  35. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +7 -0
  36. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +97 -9
  37. package/dist/v2/react/providers/TagadaProvider.js +1 -1
  38. package/dist/v2/standalone/payment-service.js +1 -0
  39. package/package.json +1 -1
@@ -527,6 +527,7 @@ export class TagadaClient {
527
527
  utmSource: urlParams.utmSource,
528
528
  utmMedium: urlParams.utmMedium,
529
529
  utmCampaign: urlParams.utmCampaign,
530
+ gclid: urlParams.gclid,
530
531
  browser: deviceInfo.userAgent.browser.name,
531
532
  browserVersion: deviceInfo.userAgent.browser.version,
532
533
  os: deviceInfo.userAgent.os.name,
@@ -106,6 +106,14 @@ export interface PaymentMethodConfig {
106
106
  method: string;
107
107
  /** Provider / processor family */
108
108
  provider: string;
109
+ /** e.g. 'apm' for alternative payment methods, 'card', etc. */
110
+ type?: string;
111
+ /** Display label for the payment method */
112
+ label?: string;
113
+ /** URL to the payment method's logo */
114
+ logoUrl?: string;
115
+ /** Human-readable description shown to the customer */
116
+ description?: string;
109
117
  /** Render as express checkout button above the form */
110
118
  express?: boolean;
111
119
  /** Card routing via payment flow (cascade, fraud, processor selection) */
@@ -714,7 +714,7 @@ export class FunnelClient {
714
714
  if (this.config.debugMode) {
715
715
  console.log('🚀 [FunnelClient] Auto-redirecting to:', result.url, '(skipped session refresh - next page will initialize)');
716
716
  }
717
- window.location.href = result.url;
717
+ window.location.replace(result.url);
718
718
  }
719
719
  return result;
720
720
  }
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Base API Client using Axios
2
+ * Base API Client using native fetch
3
3
  * Shared between all resource clients
4
4
  */
5
- import { AxiosInstance, AxiosRequestConfig } from 'axios';
6
- declare module 'axios' {
7
- interface AxiosRequestConfig {
8
- skipAuth?: boolean;
9
- }
5
+ export interface RequestConfig {
6
+ headers?: Record<string, string>;
7
+ skipAuth?: boolean;
8
+ params?: Record<string, string>;
9
+ signal?: AbortSignal;
10
10
  }
11
11
  export interface ApiClientConfig {
12
12
  baseURL: string;
@@ -15,7 +15,9 @@ export interface ApiClientConfig {
15
15
  }
16
16
  export type TokenProvider = () => Promise<string | null>;
17
17
  export declare class ApiClient {
18
- axios: AxiosInstance;
18
+ private baseURL;
19
+ private timeout;
20
+ private defaultHeaders;
19
21
  private currentToken;
20
22
  private tokenProvider;
21
23
  private requestHistory;
@@ -23,18 +25,20 @@ export declare class ApiClient {
23
25
  private readonly MAX_REQUESTS;
24
26
  constructor(config: ApiClientConfig);
25
27
  setTokenProvider(provider: TokenProvider): void;
26
- get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>;
27
- post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>;
28
- put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>;
29
- patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>;
30
- delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>;
28
+ get<T = unknown>(url: string, config?: RequestConfig): Promise<T>;
29
+ post<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
30
+ put<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
31
+ patch<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
32
+ delete<T = unknown>(url: string, config?: RequestConfig): Promise<T>;
31
33
  setHeader(key: string, value: string): void;
32
34
  removeHeader(key: string): void;
33
35
  updateToken(token: string | null): void;
34
36
  getCurrentToken(): string | null;
35
37
  updateConfig(config: Partial<ApiClientConfig>): void;
38
+ private request;
39
+ /** Convert a non-ok fetch response into the appropriate TagadaError subclass. */
40
+ private toTagadaError;
41
+ private safeParseJson;
36
42
  private checkRequestLimit;
37
43
  private cleanupHistory;
38
- /** Convert an AxiosError into the appropriate TagadaError subclass. */
39
- private toTagadaError;
40
44
  }
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Base API Client using Axios
2
+ * Base API Client using native fetch
3
3
  * Shared between all resource clients
4
4
  */
5
- import axios from 'axios';
6
5
  import { TagadaApiError, TagadaAuthError, TagadaNetworkError, TagadaCircuitBreakerError, TagadaError, TagadaErrorCode, } from '../errors';
7
6
  export class ApiClient {
8
7
  constructor(config) {
@@ -12,67 +11,16 @@ export class ApiClient {
12
11
  this.requestHistory = new Map();
13
12
  this.WINDOW_MS = 5000; // 5 seconds window
14
13
  this.MAX_REQUESTS = 30; // Max 30 requests per endpoint in window
15
- this.axios = axios.create({
16
- baseURL: config.baseURL,
17
- timeout: config.timeout || 60000, // 60 seconds for payment operations
18
- headers: {
19
- 'Content-Type': 'application/json',
20
- ...config.headers,
21
- },
22
- });
14
+ this.baseURL = config.baseURL;
15
+ this.timeout = config.timeout || 60000; // 60 seconds for payment operations
16
+ this.defaultHeaders = {
17
+ 'Content-Type': 'application/json',
18
+ ...config.headers,
19
+ };
23
20
  // Cleanup interval for circuit breaker history
24
21
  if (typeof setInterval !== 'undefined') {
25
22
  setInterval(() => this.cleanupHistory(), 10000);
26
23
  }
27
- // Request interceptor for logging and auth
28
- this.axios.interceptors.request.use(async (config) => {
29
- // Circuit Breaker Check
30
- if (config.url) {
31
- try {
32
- this.checkRequestLimit(`${config.method?.toUpperCase()}:${config.url}`);
33
- }
34
- catch (error) {
35
- console.error('[SDK] 🛑 Request blocked by Circuit Breaker:', error);
36
- return Promise.reject(error);
37
- }
38
- }
39
- // Check if we need to wait for token
40
- if (!config.skipAuth && !this.currentToken && this.tokenProvider) {
41
- try {
42
- console.log('[SDK] Waiting for token...');
43
- const token = await this.tokenProvider();
44
- if (token) {
45
- this.updateToken(token);
46
- // Ensure header is set on this specific request config
47
- config.headers['x-cms-token'] = token;
48
- }
49
- }
50
- catch (error) {
51
- console.error('[SDK] Failed to get token from provider:', error);
52
- }
53
- }
54
- // Ensure token is in headers if we have it (and not skipped)
55
- if (!config.skipAuth && this.currentToken) {
56
- config.headers['x-cms-token'] = this.currentToken;
57
- }
58
- console.log(`[SDK] Making ${config.method?.toUpperCase()} request to: ${config.baseURL || ''}${config.url}`);
59
- // console.log('[SDK] Request headers:', config.headers);
60
- return config;
61
- }, (error) => {
62
- console.error('[SDK] Request error:', error);
63
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
64
- });
65
- // Response interceptor — maps Axios errors to structured TagadaError subtypes
66
- this.axios.interceptors.response.use((response) => response, (error) => {
67
- console.error('[SDK] Response error:', error.message);
68
- if (error instanceof TagadaError) {
69
- return Promise.reject(error);
70
- }
71
- if (axios.isAxiosError(error)) {
72
- return Promise.reject(this.toTagadaError(error));
73
- }
74
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
75
- });
76
24
  }
77
25
  // Set a provider that returns a promise resolving to the token
78
26
  // This allows requests to wait until the token is ready
@@ -81,31 +29,26 @@ export class ApiClient {
81
29
  }
82
30
  // Convenience methods
83
31
  async get(url, config) {
84
- const response = await this.axios.get(url, config);
85
- return response.data;
32
+ return this.request('GET', url, undefined, config);
86
33
  }
87
34
  async post(url, data, config) {
88
- const response = await this.axios.post(url, data, config);
89
- return response.data;
35
+ return this.request('POST', url, data, config);
90
36
  }
91
37
  async put(url, data, config) {
92
- const response = await this.axios.put(url, data, config);
93
- return response.data;
38
+ return this.request('PUT', url, data, config);
94
39
  }
95
40
  async patch(url, data, config) {
96
- const response = await this.axios.patch(url, data, config);
97
- return response.data;
41
+ return this.request('PATCH', url, data, config);
98
42
  }
99
43
  async delete(url, config) {
100
- const response = await this.axios.delete(url, config);
101
- return response.data;
44
+ return this.request('DELETE', url, undefined, config);
102
45
  }
103
46
  // Update headers (useful for auth tokens)
104
47
  setHeader(key, value) {
105
- this.axios.defaults.headers.common[key] = value;
48
+ this.defaultHeaders[key] = value;
106
49
  }
107
50
  removeHeader(key) {
108
- delete this.axios.defaults.headers.common[key];
51
+ delete this.defaultHeaders[key];
109
52
  }
110
53
  // Token management methods (matching old ApiService pattern)
111
54
  updateToken(token) {
@@ -125,59 +68,106 @@ export class ApiClient {
125
68
  // Update configuration (useful for environment changes)
126
69
  updateConfig(config) {
127
70
  if (config.baseURL) {
128
- this.axios.defaults.baseURL = config.baseURL;
71
+ this.baseURL = config.baseURL;
129
72
  }
130
73
  if (config.timeout) {
131
- this.axios.defaults.timeout = config.timeout;
74
+ this.timeout = config.timeout;
132
75
  }
133
76
  if (config.headers) {
134
- Object.assign(this.axios.defaults.headers.common, config.headers);
77
+ Object.assign(this.defaultHeaders, config.headers);
135
78
  }
136
79
  console.log('[SDK] ApiClient configuration updated');
137
80
  }
138
- // Circuit Breaker Implementation
139
- checkRequestLimit(key) {
140
- const now = Date.now();
141
- const history = this.requestHistory.get(key);
142
- if (!history) {
143
- this.requestHistory.set(key, { count: 1, firstRequestTime: now });
144
- return;
81
+ // ---- Core request method ----
82
+ async request(method, url, data, config) {
83
+ const requestKey = `${method}:${url}`;
84
+ // Circuit Breaker Check
85
+ try {
86
+ this.checkRequestLimit(requestKey);
145
87
  }
146
- if (now - history.firstRequestTime > this.WINDOW_MS) {
147
- // Window expired, reset
148
- this.requestHistory.set(key, { count: 1, firstRequestTime: now });
149
- return;
88
+ catch (error) {
89
+ console.error('[SDK] 🛑 Request blocked by Circuit Breaker:', error);
90
+ throw error;
150
91
  }
151
- history.count++;
152
- if (history.count > this.MAX_REQUESTS) {
153
- throw new TagadaCircuitBreakerError(`Circuit Breaker: Too many requests to ${key} (${history.count} in ${this.WINDOW_MS}ms)`);
92
+ // Token injection
93
+ if (!config?.skipAuth && !this.currentToken && this.tokenProvider) {
94
+ try {
95
+ console.log('[SDK] Waiting for token...');
96
+ const token = await this.tokenProvider();
97
+ if (token) {
98
+ this.updateToken(token);
99
+ }
100
+ }
101
+ catch (error) {
102
+ console.error('[SDK] Failed to get token from provider:', error);
103
+ }
154
104
  }
155
- }
156
- cleanupHistory() {
157
- const now = Date.now();
158
- for (const [key, history] of this.requestHistory.entries()) {
159
- if (now - history.firstRequestTime > this.WINDOW_MS) {
160
- this.requestHistory.delete(key);
105
+ // Build headers
106
+ const headers = { ...this.defaultHeaders };
107
+ if (config?.headers) {
108
+ Object.assign(headers, config.headers);
109
+ }
110
+ if (!config?.skipAuth && this.currentToken) {
111
+ headers['x-cms-token'] = this.currentToken;
112
+ }
113
+ // Build URL
114
+ let fullUrl = `${this.baseURL}${url}`;
115
+ if (config?.params) {
116
+ const searchParams = new URLSearchParams(config.params);
117
+ fullUrl += `?${searchParams.toString()}`;
118
+ }
119
+ console.log(`[SDK] Making ${method} request to: ${fullUrl}`);
120
+ // Timeout via AbortController
121
+ const controller = new AbortController();
122
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
123
+ // Merge signals if caller provided one
124
+ const signal = config?.signal
125
+ ? anySignal([config.signal, controller.signal])
126
+ : controller.signal;
127
+ try {
128
+ const fetchOptions = {
129
+ method,
130
+ headers,
131
+ signal,
132
+ };
133
+ if (data !== undefined) {
134
+ fetchOptions.body = JSON.stringify(data);
135
+ }
136
+ const response = await fetch(fullUrl, fetchOptions);
137
+ if (!response.ok) {
138
+ const errorData = await this.safeParseJson(response);
139
+ throw this.toTagadaError(response.status, errorData, response.statusText);
161
140
  }
141
+ const responseData = await response.json();
142
+ return responseData;
162
143
  }
163
- }
164
- /** Convert an AxiosError into the appropriate TagadaError subclass. */
165
- toTagadaError(error) {
166
- const status = error.response?.status;
167
- const data = error.response?.data;
168
- const serverMessage = data?.message ??
169
- data?.error ??
170
- error.message;
171
- // No response at all → network-level failure
172
- if (!error.response) {
173
- if (error.code === 'ECONNABORTED') {
174
- return new TagadaApiError('Request timed out', 0, {
144
+ catch (error) {
145
+ if (error instanceof TagadaError) {
146
+ throw error;
147
+ }
148
+ // AbortError timeout (from our controller) or caller abort
149
+ if (error instanceof DOMException && error.name === 'AbortError') {
150
+ throw new TagadaApiError('Request timed out', 0, {
175
151
  code: TagadaErrorCode.TIMEOUT,
176
152
  retryable: true,
177
153
  });
178
154
  }
179
- return new TagadaNetworkError(serverMessage);
155
+ // TypeError → network failure (DNS, CORS, offline, etc.)
156
+ if (error instanceof TypeError) {
157
+ throw new TagadaNetworkError(error.message);
158
+ }
159
+ throw error instanceof Error ? error : new Error(String(error));
180
160
  }
161
+ finally {
162
+ clearTimeout(timeoutId);
163
+ }
164
+ }
165
+ // ---- Error mapping ----
166
+ /** Convert a non-ok fetch response into the appropriate TagadaError subclass. */
167
+ toTagadaError(status, data, statusText) {
168
+ const serverMessage = data?.message ??
169
+ data?.error ??
170
+ statusText;
181
171
  // Auth failures
182
172
  if (status === 401 || status === 403) {
183
173
  return new TagadaAuthError(serverMessage, status);
@@ -197,10 +187,62 @@ export class ApiClient {
197
187
  });
198
188
  }
199
189
  // Generic API error
200
- return new TagadaApiError(serverMessage, status ?? 0, {
190
+ return new TagadaApiError(serverMessage, status, {
201
191
  code: data?.code ?? TagadaErrorCode.API_ERROR,
202
192
  details: data,
203
- retryable: (status ?? 0) >= 500,
193
+ retryable: status >= 500,
194
+ });
195
+ }
196
+ // ---- Helpers ----
197
+ async safeParseJson(response) {
198
+ try {
199
+ return await response.json();
200
+ }
201
+ catch {
202
+ return undefined;
203
+ }
204
+ }
205
+ // Circuit Breaker Implementation
206
+ checkRequestLimit(key) {
207
+ const now = Date.now();
208
+ const history = this.requestHistory.get(key);
209
+ if (!history) {
210
+ this.requestHistory.set(key, { count: 1, firstRequestTime: now });
211
+ return;
212
+ }
213
+ if (now - history.firstRequestTime > this.WINDOW_MS) {
214
+ // Window expired, reset
215
+ this.requestHistory.set(key, { count: 1, firstRequestTime: now });
216
+ return;
217
+ }
218
+ history.count++;
219
+ if (history.count > this.MAX_REQUESTS) {
220
+ throw new TagadaCircuitBreakerError(`Circuit Breaker: Too many requests to ${key} (${history.count} in ${this.WINDOW_MS}ms)`);
221
+ }
222
+ }
223
+ cleanupHistory() {
224
+ const now = Date.now();
225
+ for (const [key, history] of this.requestHistory.entries()) {
226
+ if (now - history.firstRequestTime > this.WINDOW_MS) {
227
+ this.requestHistory.delete(key);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ /**
233
+ * Combine multiple AbortSignals into one that aborts when any input signal aborts.
234
+ */
235
+ function anySignal(signals) {
236
+ const controller = new AbortController();
237
+ for (const signal of signals) {
238
+ if (signal.aborted) {
239
+ controller.abort(signal.reason);
240
+ return controller.signal;
241
+ }
242
+ signal.addEventListener('abort', () => controller.abort(signal.reason), {
243
+ once: true,
244
+ signal: controller.signal,
204
245
  });
205
246
  }
247
+ return controller.signal;
206
248
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { OrderAddress } from '../types';
6
6
  import { ApiClient } from './apiClient';
7
- export type PaymentMethodName = 'paypal' | 'card' | 'klarna' | 'zelle';
7
+ export type PaymentMethodName = 'card' | 'paypal' | 'klarna' | 'apple_pay' | 'google_pay' | 'link' | 'bridge' | 'whop' | 'zelle' | 'hipay' | 'crypto' | 'convesiopay' | 'oceanpayment' | 'afterpay' | 'ideal' | 'bancontact' | 'giropay' | 'sepa_debit' | (string & {});
8
8
  export interface CheckoutLineItem {
9
9
  externalProductId?: string | null;
10
10
  externalVariantId?: string | null;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Resource Clients
3
- * Axios-based API clients for all endpoints
3
+ * Fetch-based API clients for all endpoints
4
4
  */
5
5
  export * from './apiClient';
6
6
  export * from './checkout';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Resource Clients
3
- * Axios-based API clients for all endpoints
3
+ * Fetch-based API clients for all endpoints
4
4
  */
5
5
  export * from './apiClient';
6
6
  export * from './checkout';
@@ -44,7 +44,7 @@ export class OffersResource {
44
44
  * - Use lineItemId for precise updates (same product, different variants)
45
45
  * - Use productId for simple updates (all items with this product)
46
46
  */
47
- async previewOffer(offerId, currency = 'USD', lineItems) {
47
+ async previewOffer(offerId, currency = '', lineItems) {
48
48
  console.log('📡 [OffersResource] Calling preview API:', {
49
49
  offerId,
50
50
  currency,
@@ -68,7 +68,7 @@ export class OffersResource {
68
68
  * @param returnUrl - Optional return URL for checkout
69
69
  * @param mainOrderId - Optional main order ID (for upsells)
70
70
  */
71
- async payPreviewedOffer(offerId, currency = 'USD', lineItems, returnUrl, mainOrderId) {
71
+ async payPreviewedOffer(offerId, currency = '', lineItems, returnUrl, mainOrderId) {
72
72
  console.log('💳 [OffersResource] Calling pay-preview API:', {
73
73
  offerId,
74
74
  currency,
@@ -96,7 +96,7 @@ export class OffersResource {
96
96
  * @param returnUrl - Optional return URL for checkout
97
97
  * @param mainOrderId - Optional main order ID
98
98
  */
99
- async toCheckout(offerId, currency = 'USD', lineItems, returnUrl, mainOrderId) {
99
+ async toCheckout(offerId, currency = '', lineItems, returnUrl, mainOrderId) {
100
100
  console.log('🛒 [OffersResource] Calling to-checkout API:', {
101
101
  offerId,
102
102
  currency,
@@ -218,7 +218,7 @@ export class OffersResource {
218
218
  *
219
219
  * // By the time page loads, background processing is usually complete
220
220
  */
221
- async toCheckoutAsync(offerId, currency = 'USD', lineItems, returnUrl, mainOrderId) {
221
+ async toCheckoutAsync(offerId, currency = '', lineItems, returnUrl, mainOrderId) {
222
222
  console.log('🛒 [OffersResource] Calling to-checkout-async API:', {
223
223
  offerId,
224
224
  currency,
@@ -279,6 +279,7 @@ export declare class PaymentsResource {
279
279
  error?: string;
280
280
  }>;
281
281
  saveRadarSession(data: {
282
+ paymentId?: string;
282
283
  orderId?: string;
283
284
  checkoutSessionId?: string;
284
285
  finixRadarSessionId?: string;
@@ -7,6 +7,8 @@ export interface Currency {
7
7
  symbol: string;
8
8
  name: string;
9
9
  decimalPlaces: number;
10
+ /** True when the currency was explicitly set via URL param or persisted storage (not a SDK/store default) */
11
+ isExplicit: boolean;
10
12
  }
11
13
  /**
12
14
  * Format money amount from minor units (cents) to a formatted string
@@ -27,6 +29,7 @@ export declare class CurrencyUtils {
27
29
  * Get currency from context or fallback to default
28
30
  */
29
31
  static getCurrency(context: any, defaultCurrency?: string): Currency;
32
+ private static getCookieValue;
30
33
  /**
31
34
  * Get currency symbol
32
35
  */
@@ -47,9 +47,25 @@ export class CurrencyUtils {
47
47
  * Get currency from context or fallback to default
48
48
  */
49
49
  static getCurrency(context, defaultCurrency = 'USD') {
50
- // Handle case where context.currency might be a Currency object or string
51
50
  let currencyCode;
52
- if (typeof context?.currency === 'string') {
51
+ let isExplicit = false;
52
+ // 1. URL ?currency= param takes highest priority (set by CRM preview, storefront links, or currency selector)
53
+ const urlCurrency = typeof window !== 'undefined'
54
+ ? new URLSearchParams(window.location.search).get('currency')
55
+ : null;
56
+ // 2. Persisted tgd_currency from storage/cookie (survives navigation between funnel steps)
57
+ const storedCurrency = typeof window !== 'undefined'
58
+ ? (localStorage.getItem('tgd_currency') || CurrencyUtils.getCookieValue('tgd_currency'))
59
+ : null;
60
+ if (urlCurrency) {
61
+ currencyCode = urlCurrency.toUpperCase();
62
+ isExplicit = true;
63
+ }
64
+ else if (storedCurrency) {
65
+ currencyCode = storedCurrency.toUpperCase();
66
+ isExplicit = true;
67
+ }
68
+ else if (typeof context?.currency === 'string') {
53
69
  currencyCode = context.currency;
54
70
  }
55
71
  else if (context?.currency?.code) {
@@ -61,13 +77,35 @@ export class CurrencyUtils {
61
77
  else {
62
78
  currencyCode = defaultCurrency;
63
79
  }
80
+ // Validate against store's presentment currencies when available.
81
+ // This catches both explicit overrides (URL/storage) and fallback values that
82
+ // reference a currency the store doesn't support.
83
+ const presentment = context?.store?.presentmentCurrencies;
84
+ if (presentment?.length && !presentment.includes(currencyCode)) {
85
+ console.warn(`[CurrencyUtils] Currency "${currencyCode}" is not in store presentmentCurrencies [${presentment.join(', ')}]. Falling back to ${presentment[0]}.`);
86
+ currencyCode = presentment[0];
87
+ // Update persisted storage so subsequent renders don't keep requesting the unsupported currency
88
+ if (isExplicit && typeof window !== 'undefined') {
89
+ try {
90
+ localStorage.setItem('tgd_currency', currencyCode);
91
+ }
92
+ catch { }
93
+ }
94
+ }
64
95
  return {
65
96
  code: currencyCode,
66
97
  symbol: this.getCurrencySymbol(currencyCode),
67
98
  name: this.getCurrencyName(currencyCode),
68
99
  decimalPlaces: this.getDecimalPlaces(currencyCode),
100
+ isExplicit,
69
101
  };
70
102
  }
103
+ static getCookieValue(name) {
104
+ if (typeof document === 'undefined')
105
+ return null;
106
+ const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
107
+ return match ? decodeURIComponent(match[1]) : null;
108
+ }
71
109
  /**
72
110
  * Get currency symbol
73
111
  */
@@ -43,4 +43,5 @@ export declare function getUrlParams(): {
43
43
  utmSource?: string;
44
44
  utmMedium?: string;
45
45
  utmCampaign?: string;
46
+ gclid?: string;
46
47
  };
@@ -202,5 +202,6 @@ export function getUrlParams() {
202
202
  utmSource: params.get('utm_source') || undefined,
203
203
  utmMedium: params.get('utm_medium') || undefined,
204
204
  utmCampaign: params.get('utm_campaign') || undefined,
205
+ gclid: params.get('gclid') || undefined,
205
206
  };
206
207
  }
@@ -360,6 +360,18 @@ function persistSDKParamsFromURL() {
360
360
  if (urlBaseUrl) {
361
361
  setClientBaseUrl(urlBaseUrl);
362
362
  }
363
+ // Persist currency if in URL (survives navigation between funnel steps)
364
+ const urlCurrency = urlParams.get('currency');
365
+ if (urlCurrency) {
366
+ setInStorage(STORAGE_KEYS.CURRENCY, urlCurrency.toUpperCase());
367
+ setInCookie(STORAGE_KEYS.CURRENCY, urlCurrency.toUpperCase(), 86400);
368
+ }
369
+ // Persist locale if in URL
370
+ const urlLocale = urlParams.get('locale');
371
+ if (urlLocale) {
372
+ setInStorage(STORAGE_KEYS.LOCALE, urlLocale);
373
+ setInCookie(STORAGE_KEYS.LOCALE, urlLocale, 86400);
374
+ }
363
375
  }
364
376
  /**
365
377
  * Set funnel tracking mode in storage for persistence