@tagadapay/plugin-sdk 3.1.22 → 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 (89) hide show
  1. package/build-cdn.js +274 -6
  2. package/dist/external-tracker.js +476 -6774
  3. package/dist/external-tracker.min.js +2 -25
  4. package/dist/external-tracker.min.js.map +4 -4
  5. package/dist/react/config/payment.d.ts +14 -4
  6. package/dist/react/config/payment.js +47 -9
  7. package/dist/react/hooks/useCheckout.d.ts +3 -0
  8. package/dist/react/hooks/useCheckout.js +11 -3
  9. package/dist/react/hooks/usePluginConfig.js +9 -10
  10. package/dist/react/providers/TagadaProvider.js +1 -1
  11. package/dist/tagada-react-sdk-minimal.min.js +36 -0
  12. package/dist/tagada-react-sdk-minimal.min.js.map +7 -0
  13. package/dist/tagada-react-sdk.js +37988 -0
  14. package/dist/tagada-react-sdk.min.js +78 -0
  15. package/dist/tagada-react-sdk.min.js.map +7 -0
  16. package/dist/tagada-sdk.js +7847 -6420
  17. package/dist/tagada-sdk.min.js +4 -22
  18. package/dist/tagada-sdk.min.js.map +4 -4
  19. package/dist/v2/cdn-react-minimal.d.ts +23 -0
  20. package/dist/v2/cdn-react-minimal.js +26 -0
  21. package/dist/v2/core/client.js +2 -1
  22. package/dist/v2/core/config/environment.js +2 -1
  23. package/dist/v2/core/funnelClient.d.ts +106 -10
  24. package/dist/v2/core/funnelClient.js +122 -28
  25. package/dist/v2/core/index.d.ts +0 -1
  26. package/dist/v2/core/index.js +0 -2
  27. package/dist/v2/core/isoData.d.ts +4 -4
  28. package/dist/v2/core/isoData.js +7 -7
  29. package/dist/v2/core/pixelMapping.js +64 -26
  30. package/dist/v2/core/resources/apiClient.d.ts +18 -14
  31. package/dist/v2/core/resources/apiClient.js +151 -109
  32. package/dist/v2/core/resources/checkout.d.ts +10 -0
  33. package/dist/v2/core/resources/checkout.js +6 -0
  34. package/dist/v2/core/resources/expressPaymentMethods.d.ts +1 -0
  35. package/dist/v2/core/resources/index.d.ts +1 -1
  36. package/dist/v2/core/resources/index.js +1 -1
  37. package/dist/v2/core/resources/offers.js +4 -4
  38. package/dist/v2/core/resources/payments.d.ts +8 -2
  39. package/dist/v2/core/resources/payments.js +1 -0
  40. package/dist/v2/core/resources/postPurchases.d.ts +17 -0
  41. package/dist/v2/core/resources/postPurchases.js +20 -0
  42. package/dist/v2/core/utils/currency.d.ts +3 -0
  43. package/dist/v2/core/utils/currency.js +40 -2
  44. package/dist/v2/core/utils/deviceInfo.d.ts +1 -10
  45. package/dist/v2/core/utils/deviceInfo.js +153 -76
  46. package/dist/v2/core/utils/order.d.ts +2 -0
  47. package/dist/v2/core/utils/pluginConfig.js +18 -22
  48. package/dist/v2/core/utils/previewMode.js +12 -0
  49. package/dist/v2/index.d.ts +4 -3
  50. package/dist/v2/index.js +4 -2
  51. package/dist/v2/react/components/ApplePayButton.js +39 -16
  52. package/dist/v2/react/components/FunnelScriptInjector.js +145 -77
  53. package/dist/v2/react/components/StripeExpressButton.d.ts +13 -0
  54. package/dist/v2/react/components/StripeExpressButton.js +170 -0
  55. package/dist/v2/react/components/WhopCheckout.js +7 -1
  56. package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
  57. package/dist/v2/react/hooks/payment-actions/useProcessorAuthAction.js +21 -3
  58. package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
  59. package/dist/v2/react/hooks/useApiQuery.js +1 -1
  60. package/dist/v2/react/hooks/useApplePayCheckout.js +8 -8
  61. package/dist/v2/react/hooks/useCheckoutQuery.d.ts +10 -0
  62. package/dist/v2/react/hooks/useCheckoutQuery.js +27 -15
  63. package/dist/v2/react/hooks/useFunnel.d.ts +15 -4
  64. package/dist/v2/react/hooks/useFunnel.js +8 -4
  65. package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +2 -0
  66. package/dist/v2/react/hooks/useGoogleAutocomplete.js +29 -15
  67. package/dist/v2/react/hooks/useISOData.d.ts +2 -5
  68. package/dist/v2/react/hooks/useISOData.js +25 -26
  69. package/dist/v2/react/hooks/usePaymentPolling.d.ts +2 -2
  70. package/dist/v2/react/hooks/usePixelTracking.js +151 -70
  71. package/dist/v2/react/hooks/usePostPurchasesQuery.js +34 -2
  72. package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
  73. package/dist/v2/react/hooks/useRemappableParams.d.ts +2 -6
  74. package/dist/v2/react/hooks/useRemappableParams.js +23 -23
  75. package/dist/v2/react/hooks/useSetPaymentMethod.d.ts +16 -0
  76. package/dist/v2/react/hooks/useSetPaymentMethod.js +33 -0
  77. package/dist/v2/react/hooks/useStepConfig.d.ts +23 -6
  78. package/dist/v2/react/hooks/useStepConfig.js +14 -7
  79. package/dist/v2/react/hooks/useTranslation.js +23 -8
  80. package/dist/v2/react/index.d.ts +8 -1
  81. package/dist/v2/react/index.js +3 -0
  82. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +8 -0
  83. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +106 -10
  84. package/dist/v2/react/providers/TagadaProvider.js +5 -5
  85. package/dist/v2/standalone/index.d.ts +21 -3
  86. package/dist/v2/standalone/index.js +25 -3
  87. package/dist/v2/standalone/payment-service.d.ts +134 -0
  88. package/dist/v2/standalone/payment-service.js +929 -0
  89. package/package.json +4 -2
@@ -72,9 +72,43 @@ function convertValueToMajor(params) {
72
72
  }
73
73
  return params;
74
74
  }
75
+ /** Convert top-level monetary fields (shipping, tax, subtotal, total_discount) from minor → major units. */
76
+ function convertMonetaryFieldsToMajor(params) {
77
+ const currency = params.currency ? String(params.currency) : null;
78
+ if (!currency)
79
+ return params;
80
+ const result = { ...params };
81
+ for (const field of ['shipping', 'tax', 'subtotal', 'total_discount']) {
82
+ if (result[field] != null) {
83
+ result[field] = minorToMajor(Number(result[field]), currency);
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ /** Convert price fields inside contents[] items from minor → major units. */
89
+ function convertContentsPricesToMajor(params) {
90
+ if (!params.contents || !Array.isArray(params.contents))
91
+ return params;
92
+ const currency = params.currency ? String(params.currency) : null;
93
+ const result = { ...params };
94
+ result.contents = params.contents.map((item) => {
95
+ const itemCurrency = item.currency ? String(item.currency) : currency;
96
+ if (!itemCurrency)
97
+ return { ...item };
98
+ const converted = { ...item };
99
+ if (converted.price != null)
100
+ converted.price = minorToMajor(Number(converted.price), itemCurrency);
101
+ if (converted.original_price != null)
102
+ converted.original_price = minorToMajor(Number(converted.original_price), itemCurrency);
103
+ return converted;
104
+ });
105
+ return result;
106
+ }
75
107
  function baseTransform(params) {
76
108
  let p = ensureCurrencyUppercase(params);
77
109
  p = convertValueToMajor(p);
110
+ p = convertMonetaryFieldsToMajor(p);
111
+ p = convertContentsPricesToMajor(p);
78
112
  return p;
79
113
  }
80
114
  // ---------------------------------------------------------------------------
@@ -98,6 +132,14 @@ export function mapMetaEvent(eventName, parameters) {
98
132
  if (params.contents && Array.isArray(params.contents)) {
99
133
  params.content_ids = params.contents.map((i) => i.content_id).filter(Boolean);
100
134
  params.content_type = params.content_type ?? 'product';
135
+ // Reshape contents to Meta's expected format (prices already converted by baseTransform)
136
+ params.contents = params.contents.map((item) => ({
137
+ id: item.content_id,
138
+ quantity: item.quantity != null ? Number(item.quantity) : undefined,
139
+ item_price: item.price != null ? Number(item.price) : undefined,
140
+ content_name: item.content_name,
141
+ content_category: item.content_category,
142
+ }));
101
143
  }
102
144
  return { name, params };
103
145
  }
@@ -124,35 +166,29 @@ const TIKTOK_EVENT_MAP = {
124
166
  export function mapTikTokEvent(eventName, parameters) {
125
167
  const name = TIKTOK_EVENT_MAP[eventName] ?? eventName;
126
168
  const params = baseTransform(parameters);
127
- // Convert additional monetary fields from minor to major units
128
- const currency = params.currency ? String(params.currency) : null;
129
- if (currency) {
130
- for (const field of ['shipping', 'tax', 'subtotal', 'total_discount']) {
131
- if (params[field] != null) {
132
- params[field] = minorToMajor(Number(params[field]), currency);
133
- }
134
- }
135
- }
136
169
  if (params.contents && Array.isArray(params.contents)) {
137
- // Extract content_ids at top level for TikTok product matching
170
+ const ids = params.contents
171
+ .map((i) => i.content_id)
172
+ .filter(Boolean);
173
+ // TikTok requires content_id (singular) at the top level for VSA
174
+ if (!params.content_id && ids.length > 0) {
175
+ params.content_id = ids.length === 1 ? ids[0] : JSON.stringify(ids);
176
+ }
177
+ // Also keep content_ids for backwards compatibility
138
178
  if (!params.content_ids) {
139
- params.content_ids = params.contents
140
- .map((i) => i.content_id)
141
- .filter(Boolean);
179
+ params.content_ids = ids;
142
180
  }
143
- params.contents = params.contents.map((item) => {
144
- const itemCurrency = item.currency ? String(item.currency) : currency;
145
- return {
146
- content_id: item.content_id,
147
- content_name: item.content_name,
148
- content_type: item.content_type ?? 'product',
149
- content_category: item.content_category,
150
- price: item.price != null && itemCurrency ? minorToMajor(Number(item.price), itemCurrency) : item.price != null ? Number(item.price) : undefined,
151
- original_price: item.original_price != null && itemCurrency ? minorToMajor(Number(item.original_price), itemCurrency) : item.original_price != null ? Number(item.original_price) : undefined,
152
- quantity: item.quantity != null ? Number(item.quantity) : undefined,
153
- description: item.description,
154
- };
155
- });
181
+ // Reshape contents to TikTok's expected format (prices already converted by baseTransform)
182
+ params.contents = params.contents.map((item) => ({
183
+ content_id: item.content_id,
184
+ content_name: item.content_name,
185
+ content_type: item.content_type ?? 'product',
186
+ content_category: item.content_category,
187
+ price: item.price != null ? Number(item.price) : undefined,
188
+ original_price: item.original_price != null ? Number(item.original_price) : undefined,
189
+ quantity: item.quantity != null ? Number(item.quantity) : undefined,
190
+ description: item.description,
191
+ }));
156
192
  }
157
193
  // Set required fields for InitiateCheckout
158
194
  if (eventName === 'InitiateCheckout') {
@@ -224,6 +260,7 @@ export function mapPinterestEvent(eventName, parameters) {
224
260
  params.order_quantity = Number(params.order_quantity);
225
261
  }
226
262
  if (params.contents && Array.isArray(params.contents)) {
263
+ // Reshape to Pinterest line_items format (prices already converted by baseTransform)
227
264
  params.line_items = params.contents.map((item) => ({
228
265
  product_id: item.content_id,
229
266
  product_name: item.content_name,
@@ -259,6 +296,7 @@ export function mapGTMEvent(eventName, parameters) {
259
296
  params.num_items = Number(params.num_items);
260
297
  }
261
298
  if (params.contents && Array.isArray(params.contents)) {
299
+ // Reshape to GA4 items format (prices already converted by baseTransform)
262
300
  params.items = params.contents.map((item) => ({
263
301
  item_id: item.content_id,
264
302
  item_name: item.content_name,
@@ -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,6 +4,7 @@
4
4
  */
5
5
  import type { OrderAddress } from '../types';
6
6
  import { ApiClient } from './apiClient';
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 & {});
7
8
  export interface CheckoutLineItem {
8
9
  externalProductId?: string | null;
9
10
  externalVariantId?: string | null;
@@ -26,6 +27,7 @@ export interface CheckoutInitParams {
26
27
  currency?: string;
27
28
  locale?: string;
28
29
  };
30
+ enabledOrderBumpOfferIds?: string[];
29
31
  }
30
32
  export interface UpsellTrigger {
31
33
  id: string;
@@ -185,6 +187,7 @@ export interface CheckoutSummaryItem {
185
187
  intervalCount?: number;
186
188
  totalBillingCycles?: number;
187
189
  unitAmountAfterFirstCycle?: number;
190
+ properties?: Record<string, unknown>;
188
191
  orderLineItemProduct: {
189
192
  name: string;
190
193
  };
@@ -257,6 +260,7 @@ export interface CheckoutSessionPreviewItem {
257
260
  totalBillingCycles?: number;
258
261
  unitAmountAfterFirstCycle?: number;
259
262
  subscriptionSettings?: Record<string, unknown>;
263
+ properties?: Record<string, unknown>;
260
264
  orderLineItemProduct: {
261
265
  name: string | null;
262
266
  } | null;
@@ -519,4 +523,10 @@ export declare class CheckoutResource {
519
523
  success: boolean;
520
524
  preview: CheckoutSessionPreview;
521
525
  }>;
526
+ /**
527
+ * Set selected payment method for a checkout session
528
+ */
529
+ setPaymentMethod(checkoutSessionId: string, paymentMethodName: PaymentMethodName): Promise<{
530
+ success: boolean;
531
+ }>;
522
532
  }
@@ -213,4 +213,10 @@ export class CheckoutResource {
213
213
  async previewCheckoutSession(params) {
214
214
  return this.apiClient.post('/api/v1/checkout-sessions/preview', params);
215
215
  }
216
+ /**
217
+ * Set selected payment method for a checkout session
218
+ */
219
+ async setPaymentMethod(checkoutSessionId, paymentMethodName) {
220
+ return this.apiClient.post(`/api/v1/checkout-sessions/${checkoutSessionId}/payment-method`, { paymentMethodName });
221
+ }
216
222
  }
@@ -9,6 +9,7 @@ export interface PaymentMethod {
9
9
  title: string;
10
10
  iconUrl: string;
11
11
  default: boolean;
12
+ settings?: Record<string, unknown>;
12
13
  metadata?: Record<string, unknown>;
13
14
  }
14
15
  export interface Address {
@@ -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,