@tagadapay/plugin-sdk 3.1.24 → 4.0.0
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/README.md +1129 -1129
- package/build-cdn.js +499 -499
- package/dist/external-tracker.js +247 -2875
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/config/payment.d.ts +2 -2
- package/dist/react/config/payment.js +5 -5
- package/dist/react/hooks/useCheckout.js +7 -2
- package/dist/react/hooks/usePayment.d.ts +7 -0
- package/dist/react/hooks/usePayment.js +1 -0
- package/dist/react/providers/TagadaProvider.js +5 -5
- package/dist/tagada-react-sdk-minimal.min.js +2 -2
- package/dist/tagada-react-sdk-minimal.min.js.map +4 -4
- package/dist/tagada-react-sdk.js +1680 -1172
- package/dist/tagada-react-sdk.min.js +2 -2
- package/dist/tagada-react-sdk.min.js.map +4 -4
- package/dist/tagada-sdk.js +1701 -3410
- 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/config/environment.d.ts +3 -3
- package/dist/v2/core/config/environment.js +7 -7
- package/dist/v2/core/funnelClient.d.ts +10 -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/funnel.d.ts +1 -1
- package/dist/v2/core/resources/geo.d.ts +50 -0
- package/dist/v2/core/resources/geo.js +35 -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 +20 -1
- package/dist/v2/core/resources/payments.js +8 -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/core/utils/previewModeIndicator.js +101 -101
- package/dist/v2/react/components/ApplePayButton.js +39 -16
- package/dist/v2/react/components/FunnelScriptInjector.js +167 -19
- package/dist/v2/react/components/StripeExpressButton.d.ts +8 -0
- package/dist/v2/react/components/StripeExpressButton.js +23 -3
- package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
- package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.d.ts +15 -0
- package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.js +166 -0
- package/dist/v2/react/hooks/payment-actions/usePaymentActionHandler.js +12 -0
- package/dist/v2/react/hooks/payment-processing/usePaymentProcessors.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/useISOData.js +25 -7
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +1 -1
- 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 +105 -9
- package/dist/v2/react/providers/TagadaProvider.js +6 -6
- package/dist/v2/standalone/apple-pay-service.d.ts +12 -0
- package/dist/v2/standalone/apple-pay-service.js +12 -0
- package/dist/v2/standalone/external-tracker.d.ts +1 -1
- package/dist/v2/standalone/google-pay-service.d.ts +9 -0
- package/dist/v2/standalone/google-pay-service.js +9 -0
- package/dist/v2/standalone/index.d.ts +8 -1
- package/dist/v2/standalone/index.js +7 -0
- package/dist/v2/standalone/payment-service.d.ts +18 -5
- package/dist/v2/standalone/payment-service.js +63 -9
- package/package.json +115 -114
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,
|
|
@@ -8,11 +8,11 @@ import { ApiConfig, Environment, EnvironmentConfig } from '../types';
|
|
|
8
8
|
* Environment detection priority (highest to lowest):
|
|
9
9
|
* 1. **tagadaClientEnv** - Explicit override via URL param, localStorage, or cookie
|
|
10
10
|
* Example: ?tagadaClientEnv=production
|
|
11
|
-
* 2. **Production domains** → production API (
|
|
12
|
-
* 3. **Dev/staging domains** → development API (
|
|
11
|
+
* 2. **Production domains** → production API (api.tagada.io)
|
|
12
|
+
* 3. **Dev/staging domains** → development API (api.tagada.dev, vercel.app, etc.)
|
|
13
13
|
* 4. **Localhost/local IPs** → local API (localhost, 127.0.0.1, etc.)
|
|
14
14
|
* - Can be overridden via window.__TAGADA_ENV__.TAGADA_ENVIRONMENT
|
|
15
|
-
* 5. **Default fallback** → production API (safest
|
|
15
|
+
* 5. **Default fallback** → production API (safest)
|
|
16
16
|
*
|
|
17
17
|
* Build-time .env variables (VITE_*, REACT_APP_*, NEXT_PUBLIC_*) are IGNORED
|
|
18
18
|
* to prevent incorrect API connections when plugins are deployed to different environments.
|
|
@@ -19,11 +19,11 @@ function getCookie(name) {
|
|
|
19
19
|
* Environment detection priority (highest to lowest):
|
|
20
20
|
* 1. **tagadaClientEnv** - Explicit override via URL param, localStorage, or cookie
|
|
21
21
|
* Example: ?tagadaClientEnv=production
|
|
22
|
-
* 2. **Production domains** → production API (
|
|
23
|
-
* 3. **Dev/staging domains** → development API (
|
|
22
|
+
* 2. **Production domains** → production API (api.tagada.io)
|
|
23
|
+
* 3. **Dev/staging domains** → development API (api.tagada.dev, vercel.app, etc.)
|
|
24
24
|
* 4. **Localhost/local IPs** → local API (localhost, 127.0.0.1, etc.)
|
|
25
25
|
* - Can be overridden via window.__TAGADA_ENV__.TAGADA_ENVIRONMENT
|
|
26
|
-
* 5. **Default fallback** → production API (safest
|
|
26
|
+
* 5. **Default fallback** → production API (safest)
|
|
27
27
|
*
|
|
28
28
|
* Build-time .env variables (VITE_*, REACT_APP_*, NEXT_PUBLIC_*) are IGNORED
|
|
29
29
|
* to prevent incorrect API connections when plugins are deployed to different environments.
|
|
@@ -33,7 +33,7 @@ function getCookie(name) {
|
|
|
33
33
|
*/
|
|
34
34
|
export const ENVIRONMENT_CONFIGS = {
|
|
35
35
|
production: {
|
|
36
|
-
baseUrl: 'https://
|
|
36
|
+
baseUrl: 'https://api.tagada.io',
|
|
37
37
|
endpoints: {
|
|
38
38
|
checkout: {
|
|
39
39
|
sessionInit: '/api/v1/checkout/session/init',
|
|
@@ -51,7 +51,7 @@ export const ENVIRONMENT_CONFIGS = {
|
|
|
51
51
|
},
|
|
52
52
|
},
|
|
53
53
|
development: {
|
|
54
|
-
baseUrl: 'https://
|
|
54
|
+
baseUrl: 'https://api.tagada.dev',
|
|
55
55
|
endpoints: {
|
|
56
56
|
checkout: {
|
|
57
57
|
sessionInit: '/api/v1/checkout/session/init',
|
|
@@ -213,13 +213,13 @@ export function detectEnvironment() {
|
|
|
213
213
|
return 'local';
|
|
214
214
|
}
|
|
215
215
|
// 2. Production: deployed to production domains
|
|
216
|
-
if (hostname
|
|
216
|
+
if (hostname.includes('tagada.io') ||
|
|
217
217
|
hostname.includes('tagadapay.com') ||
|
|
218
218
|
hostname.includes('yourproductiondomain.com')) {
|
|
219
219
|
return 'production';
|
|
220
220
|
}
|
|
221
221
|
// 3. Development: deployed to staging/dev domains or has dev indicators
|
|
222
|
-
if (hostname
|
|
222
|
+
if (hostname.includes('tagada.dev') ||
|
|
223
223
|
hostname.includes('tagadapay.dev') ||
|
|
224
224
|
hostname.includes('vercel.app') ||
|
|
225
225
|
hostname.includes('netlify.app') ||
|
|
@@ -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) */
|
|
@@ -129,6 +137,8 @@ export interface PaymentMethodConfig {
|
|
|
129
137
|
note?: string;
|
|
130
138
|
}>;
|
|
131
139
|
metadata?: Record<string, any>;
|
|
140
|
+
/** Integration settings (e.g. custom_payment checkout instructions, thank-you text) */
|
|
141
|
+
settings?: Record<string, any>;
|
|
132
142
|
}
|
|
133
143
|
export type PaymentSetupConfig = Record<string, PaymentMethodConfig>;
|
|
134
144
|
/** Get all enabled entries from a paymentSetupConfig */
|
|
@@ -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;
|
|
@@ -405,7 +405,7 @@ export interface SimpleFunnelContext<TCustom = {}> {
|
|
|
405
405
|
* ✅ Environment context (staging or production)
|
|
406
406
|
* - Determined at session initialization based on entry URL
|
|
407
407
|
* - Ensures all navigation stays in the same environment
|
|
408
|
-
* - 'staging': Uses funnel.config (alias domains like funnel--store.cdn.
|
|
408
|
+
* - 'staging': Uses funnel.config (alias domains like funnel--store.cdn.tagada.io)
|
|
409
409
|
* - 'production': Uses funnel.productionConfig (custom domains)
|
|
410
410
|
*/
|
|
411
411
|
environment?: 'staging' | 'production';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geo Resource Client
|
|
3
|
+
* API client for IP geolocation endpoint
|
|
4
|
+
*/
|
|
5
|
+
import { ApiClient } from './apiClient';
|
|
6
|
+
export interface GeoLocationData {
|
|
7
|
+
ip_address?: string | null;
|
|
8
|
+
city?: string | null;
|
|
9
|
+
region?: string | null;
|
|
10
|
+
region_iso_code?: string | null;
|
|
11
|
+
postal_code?: string | null;
|
|
12
|
+
country?: string | null;
|
|
13
|
+
country_code?: string | null;
|
|
14
|
+
country_is_eu?: boolean | null;
|
|
15
|
+
continent?: string | null;
|
|
16
|
+
continent_code?: string | null;
|
|
17
|
+
latitude?: number | null;
|
|
18
|
+
longitude?: number | null;
|
|
19
|
+
security?: {
|
|
20
|
+
is_vpn?: boolean | null;
|
|
21
|
+
} | null;
|
|
22
|
+
timezone?: {
|
|
23
|
+
name?: string | null;
|
|
24
|
+
abbreviation?: string | null;
|
|
25
|
+
gmt_offset?: number | null;
|
|
26
|
+
current_time?: string | null;
|
|
27
|
+
is_dst?: boolean | null;
|
|
28
|
+
} | null;
|
|
29
|
+
currency?: {
|
|
30
|
+
currency_name?: string | null;
|
|
31
|
+
currency_code?: string | null;
|
|
32
|
+
} | null;
|
|
33
|
+
}
|
|
34
|
+
export declare class GeoResource {
|
|
35
|
+
private apiClient;
|
|
36
|
+
constructor(apiClient: ApiClient);
|
|
37
|
+
/**
|
|
38
|
+
* Get geolocation data from the visitor's IP address
|
|
39
|
+
*/
|
|
40
|
+
getGeoLocation(): Promise<GeoLocationData>;
|
|
41
|
+
/**
|
|
42
|
+
* Get geolocation data for a specific IP address
|
|
43
|
+
*/
|
|
44
|
+
getGeoLocationByIp(ip: string): Promise<GeoLocationData>;
|
|
45
|
+
/**
|
|
46
|
+
* Convenience: get just the country code from IP geolocation
|
|
47
|
+
* Returns ISO 3166-1 alpha-2 code (e.g. 'US', 'FR') or null
|
|
48
|
+
*/
|
|
49
|
+
detectCountry(): Promise<string | null>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geo Resource Client
|
|
3
|
+
* API client for IP geolocation endpoint
|
|
4
|
+
*/
|
|
5
|
+
export class GeoResource {
|
|
6
|
+
constructor(apiClient) {
|
|
7
|
+
this.apiClient = apiClient;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get geolocation data from the visitor's IP address
|
|
11
|
+
*/
|
|
12
|
+
async getGeoLocation() {
|
|
13
|
+
return this.apiClient.get('/api/v1/geo');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get geolocation data for a specific IP address
|
|
17
|
+
*/
|
|
18
|
+
async getGeoLocationByIp(ip) {
|
|
19
|
+
return this.apiClient.get(`/api/v1/geo?ip=${encodeURIComponent(ip)}`);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Convenience: get just the country code from IP geolocation
|
|
23
|
+
* Returns ISO 3166-1 alpha-2 code (e.g. 'US', 'FR') or null
|
|
24
|
+
*/
|
|
25
|
+
async detectCountry() {
|
|
26
|
+
try {
|
|
27
|
+
const data = await this.getGeoLocation();
|
|
28
|
+
const code = data?.country_code;
|
|
29
|
+
return code && typeof code === 'string' && code.length === 2 ? code : null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -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,
|