@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.
- package/build-cdn.js +274 -6
- package/dist/external-tracker.js +476 -6774
- package/dist/external-tracker.min.js +2 -25
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/config/payment.d.ts +14 -4
- package/dist/react/config/payment.js +47 -9
- package/dist/react/hooks/useCheckout.d.ts +3 -0
- package/dist/react/hooks/useCheckout.js +11 -3
- package/dist/react/hooks/usePluginConfig.js +9 -10
- package/dist/react/providers/TagadaProvider.js +1 -1
- package/dist/tagada-react-sdk-minimal.min.js +36 -0
- package/dist/tagada-react-sdk-minimal.min.js.map +7 -0
- package/dist/tagada-react-sdk.js +37988 -0
- package/dist/tagada-react-sdk.min.js +78 -0
- package/dist/tagada-react-sdk.min.js.map +7 -0
- package/dist/tagada-sdk.js +7847 -6420
- package/dist/tagada-sdk.min.js +4 -22
- package/dist/tagada-sdk.min.js.map +4 -4
- package/dist/v2/cdn-react-minimal.d.ts +23 -0
- package/dist/v2/cdn-react-minimal.js +26 -0
- package/dist/v2/core/client.js +2 -1
- package/dist/v2/core/config/environment.js +2 -1
- package/dist/v2/core/funnelClient.d.ts +106 -10
- package/dist/v2/core/funnelClient.js +122 -28
- package/dist/v2/core/index.d.ts +0 -1
- package/dist/v2/core/index.js +0 -2
- package/dist/v2/core/isoData.d.ts +4 -4
- package/dist/v2/core/isoData.js +7 -7
- package/dist/v2/core/pixelMapping.js +64 -26
- package/dist/v2/core/resources/apiClient.d.ts +18 -14
- package/dist/v2/core/resources/apiClient.js +151 -109
- package/dist/v2/core/resources/checkout.d.ts +10 -0
- package/dist/v2/core/resources/checkout.js +6 -0
- package/dist/v2/core/resources/expressPaymentMethods.d.ts +1 -0
- package/dist/v2/core/resources/index.d.ts +1 -1
- package/dist/v2/core/resources/index.js +1 -1
- package/dist/v2/core/resources/offers.js +4 -4
- package/dist/v2/core/resources/payments.d.ts +8 -2
- package/dist/v2/core/resources/payments.js +1 -0
- package/dist/v2/core/resources/postPurchases.d.ts +17 -0
- package/dist/v2/core/resources/postPurchases.js +20 -0
- package/dist/v2/core/utils/currency.d.ts +3 -0
- package/dist/v2/core/utils/currency.js +40 -2
- package/dist/v2/core/utils/deviceInfo.d.ts +1 -10
- package/dist/v2/core/utils/deviceInfo.js +153 -76
- package/dist/v2/core/utils/order.d.ts +2 -0
- package/dist/v2/core/utils/pluginConfig.js +18 -22
- package/dist/v2/core/utils/previewMode.js +12 -0
- package/dist/v2/index.d.ts +4 -3
- package/dist/v2/index.js +4 -2
- package/dist/v2/react/components/ApplePayButton.js +39 -16
- package/dist/v2/react/components/FunnelScriptInjector.js +145 -77
- package/dist/v2/react/components/StripeExpressButton.d.ts +13 -0
- package/dist/v2/react/components/StripeExpressButton.js +170 -0
- package/dist/v2/react/components/WhopCheckout.js +7 -1
- package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
- package/dist/v2/react/hooks/payment-actions/useProcessorAuthAction.js +21 -3
- package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
- package/dist/v2/react/hooks/useApiQuery.js +1 -1
- package/dist/v2/react/hooks/useApplePayCheckout.js +8 -8
- package/dist/v2/react/hooks/useCheckoutQuery.d.ts +10 -0
- package/dist/v2/react/hooks/useCheckoutQuery.js +27 -15
- package/dist/v2/react/hooks/useFunnel.d.ts +15 -4
- package/dist/v2/react/hooks/useFunnel.js +8 -4
- package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +2 -0
- package/dist/v2/react/hooks/useGoogleAutocomplete.js +29 -15
- package/dist/v2/react/hooks/useISOData.d.ts +2 -5
- package/dist/v2/react/hooks/useISOData.js +25 -26
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +2 -2
- package/dist/v2/react/hooks/usePixelTracking.js +151 -70
- package/dist/v2/react/hooks/usePostPurchasesQuery.js +34 -2
- package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
- package/dist/v2/react/hooks/useRemappableParams.d.ts +2 -6
- package/dist/v2/react/hooks/useRemappableParams.js +23 -23
- package/dist/v2/react/hooks/useSetPaymentMethod.d.ts +16 -0
- package/dist/v2/react/hooks/useSetPaymentMethod.js +33 -0
- package/dist/v2/react/hooks/useStepConfig.d.ts +23 -6
- package/dist/v2/react/hooks/useStepConfig.js +14 -7
- package/dist/v2/react/hooks/useTranslation.js +23 -8
- package/dist/v2/react/index.d.ts +8 -1
- package/dist/v2/react/index.js +3 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +8 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +106 -10
- package/dist/v2/react/providers/TagadaProvider.js +5 -5
- package/dist/v2/standalone/index.d.ts +21 -3
- package/dist/v2/standalone/index.js +25 -3
- package/dist/v2/standalone/payment-service.d.ts +134 -0
- package/dist/v2/standalone/payment-service.js +929 -0
- 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
|
-
|
|
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 =
|
|
140
|
-
.map((i) => i.content_id)
|
|
141
|
-
.filter(Boolean);
|
|
179
|
+
params.content_ids = ids;
|
|
142
180
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
2
|
+
* Base API Client using native fetch
|
|
3
3
|
* Shared between all resource clients
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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?:
|
|
27
|
-
post<T = unknown>(url: string, data?: unknown, config?:
|
|
28
|
-
put<T = unknown>(url: string, data?: unknown, config?:
|
|
29
|
-
patch<T = unknown>(url: string, data?: unknown, config?:
|
|
30
|
-
delete<T = unknown>(url: string, config?:
|
|
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
|
|
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.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
85
|
-
return response.data;
|
|
32
|
+
return this.request('GET', url, undefined, config);
|
|
86
33
|
}
|
|
87
34
|
async post(url, data, config) {
|
|
88
|
-
|
|
89
|
-
return response.data;
|
|
35
|
+
return this.request('POST', url, data, config);
|
|
90
36
|
}
|
|
91
37
|
async put(url, data, config) {
|
|
92
|
-
|
|
93
|
-
return response.data;
|
|
38
|
+
return this.request('PUT', url, data, config);
|
|
94
39
|
}
|
|
95
40
|
async patch(url, data, config) {
|
|
96
|
-
|
|
97
|
-
return response.data;
|
|
41
|
+
return this.request('PATCH', url, data, config);
|
|
98
42
|
}
|
|
99
43
|
async delete(url, config) {
|
|
100
|
-
|
|
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.
|
|
48
|
+
this.defaultHeaders[key] = value;
|
|
106
49
|
}
|
|
107
50
|
removeHeader(key) {
|
|
108
|
-
delete this.
|
|
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.
|
|
71
|
+
this.baseURL = config.baseURL;
|
|
129
72
|
}
|
|
130
73
|
if (config.timeout) {
|
|
131
|
-
this.
|
|
74
|
+
this.timeout = config.timeout;
|
|
132
75
|
}
|
|
133
76
|
if (config.headers) {
|
|
134
|
-
Object.assign(this.
|
|
77
|
+
Object.assign(this.defaultHeaders, config.headers);
|
|
135
78
|
}
|
|
136
79
|
console.log('[SDK] ApiClient configuration updated');
|
|
137
80
|
}
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return;
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error('[SDK] 🛑 Request blocked by Circuit Breaker:', error);
|
|
90
|
+
throw error;
|
|
150
91
|
}
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
190
|
+
return new TagadaApiError(serverMessage, status, {
|
|
201
191
|
code: data?.code ?? TagadaErrorCode.API_ERROR,
|
|
202
192
|
details: data,
|
|
203
|
-
retryable:
|
|
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
|
}
|
|
@@ -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 = '
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
221
|
+
async toCheckoutAsync(offerId, currency = '', lineItems, returnUrl, mainOrderId) {
|
|
222
222
|
console.log('🛒 [OffersResource] Calling to-checkout-async API:', {
|
|
223
223
|
offerId,
|
|
224
224
|
currency,
|