@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.
- package/dist/external-tracker.js +243 -2871
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/hooks/useCheckout.js +7 -2
- package/dist/tagada-react-sdk-minimal.min.js +2 -2
- package/dist/tagada-react-sdk-minimal.min.js.map +3 -3
- package/dist/tagada-react-sdk.js +340 -173
- package/dist/tagada-react-sdk.min.js +2 -2
- package/dist/tagada-react-sdk.min.js.map +3 -3
- package/dist/tagada-sdk.js +776 -3327
- package/dist/tagada-sdk.min.js +2 -2
- package/dist/tagada-sdk.min.js.map +4 -4
- package/dist/v2/core/client.js +1 -0
- package/dist/v2/core/funnelClient.d.ts +8 -0
- package/dist/v2/core/funnelClient.js +1 -1
- 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 +1 -1
- 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 +1 -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 -0
- package/dist/v2/core/utils/deviceInfo.js +1 -0
- package/dist/v2/core/utils/previewMode.js +12 -0
- package/dist/v2/react/components/ApplePayButton.js +39 -16
- package/dist/v2/react/components/StripeExpressButton.js +1 -2
- package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
- package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
- package/dist/v2/react/hooks/useApiQuery.js +1 -1
- package/dist/v2/react/hooks/useCheckoutQuery.js +6 -2
- package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +7 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +97 -9
- package/dist/v2/react/providers/TagadaProvider.js +1 -1
- package/dist/v2/standalone/payment-service.js +1 -0
- package/package.json +1 -1
package/dist/v2/core/client.js
CHANGED
|
@@ -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.
|
|
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
|
|
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,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { OrderAddress } from '../types';
|
|
6
6
|
import { ApiClient } from './apiClient';
|
|
7
|
-
export type PaymentMethodName = '
|
|
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;
|
|
@@ -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,
|
|
@@ -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
|
-
|
|
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
|
*/
|
|
@@ -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
|