@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.
Files changed (69) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +499 -499
  3. package/dist/external-tracker.js +247 -2875
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/config/payment.d.ts +2 -2
  7. package/dist/react/config/payment.js +5 -5
  8. package/dist/react/hooks/useCheckout.js +7 -2
  9. package/dist/react/hooks/usePayment.d.ts +7 -0
  10. package/dist/react/hooks/usePayment.js +1 -0
  11. package/dist/react/providers/TagadaProvider.js +5 -5
  12. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  13. package/dist/tagada-react-sdk-minimal.min.js.map +4 -4
  14. package/dist/tagada-react-sdk.js +1680 -1172
  15. package/dist/tagada-react-sdk.min.js +2 -2
  16. package/dist/tagada-react-sdk.min.js.map +4 -4
  17. package/dist/tagada-sdk.js +1701 -3410
  18. package/dist/tagada-sdk.min.js +2 -2
  19. package/dist/tagada-sdk.min.js.map +4 -4
  20. package/dist/v2/core/client.js +1 -0
  21. package/dist/v2/core/config/environment.d.ts +3 -3
  22. package/dist/v2/core/config/environment.js +7 -7
  23. package/dist/v2/core/funnelClient.d.ts +10 -0
  24. package/dist/v2/core/funnelClient.js +1 -1
  25. package/dist/v2/core/resources/apiClient.d.ts +18 -14
  26. package/dist/v2/core/resources/apiClient.js +151 -109
  27. package/dist/v2/core/resources/checkout.d.ts +1 -1
  28. package/dist/v2/core/resources/funnel.d.ts +1 -1
  29. package/dist/v2/core/resources/geo.d.ts +50 -0
  30. package/dist/v2/core/resources/geo.js +35 -0
  31. package/dist/v2/core/resources/index.d.ts +1 -1
  32. package/dist/v2/core/resources/index.js +1 -1
  33. package/dist/v2/core/resources/offers.js +4 -4
  34. package/dist/v2/core/resources/payments.d.ts +20 -1
  35. package/dist/v2/core/resources/payments.js +8 -0
  36. package/dist/v2/core/utils/currency.d.ts +3 -0
  37. package/dist/v2/core/utils/currency.js +40 -2
  38. package/dist/v2/core/utils/deviceInfo.d.ts +1 -0
  39. package/dist/v2/core/utils/deviceInfo.js +1 -0
  40. package/dist/v2/core/utils/previewMode.js +12 -0
  41. package/dist/v2/core/utils/previewModeIndicator.js +101 -101
  42. package/dist/v2/react/components/ApplePayButton.js +39 -16
  43. package/dist/v2/react/components/FunnelScriptInjector.js +167 -19
  44. package/dist/v2/react/components/StripeExpressButton.d.ts +8 -0
  45. package/dist/v2/react/components/StripeExpressButton.js +23 -3
  46. package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
  47. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.d.ts +15 -0
  48. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.js +166 -0
  49. package/dist/v2/react/hooks/payment-actions/usePaymentActionHandler.js +12 -0
  50. package/dist/v2/react/hooks/payment-processing/usePaymentProcessors.js +1 -0
  51. package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
  52. package/dist/v2/react/hooks/useApiQuery.js +1 -1
  53. package/dist/v2/react/hooks/useCheckoutQuery.js +6 -2
  54. package/dist/v2/react/hooks/useISOData.js +25 -7
  55. package/dist/v2/react/hooks/usePaymentPolling.d.ts +1 -1
  56. package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
  57. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +7 -0
  58. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +105 -9
  59. package/dist/v2/react/providers/TagadaProvider.js +6 -6
  60. package/dist/v2/standalone/apple-pay-service.d.ts +12 -0
  61. package/dist/v2/standalone/apple-pay-service.js +12 -0
  62. package/dist/v2/standalone/external-tracker.d.ts +1 -1
  63. package/dist/v2/standalone/google-pay-service.d.ts +9 -0
  64. package/dist/v2/standalone/google-pay-service.js +9 -0
  65. package/dist/v2/standalone/index.d.ts +8 -1
  66. package/dist/v2/standalone/index.js +7 -0
  67. package/dist/v2/standalone/payment-service.d.ts +18 -5
  68. package/dist/v2/standalone/payment-service.js +63 -9
  69. package/package.json +115 -114
@@ -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 (app.tagadapay.com)
12
- * 3. **Dev/staging domains** → development API (app.tagadapay.dev, vercel.app, etc.)
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 for unknown domains)
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 (app.tagadapay.com)
23
- * 3. **Dev/staging domains** → development API (app.tagadapay.dev, vercel.app, etc.)
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 for unknown domains)
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://app.tagadapay.com',
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://app.tagadapay.dev',
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 === 'app.tagadapay.com' ||
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 === 'app.tagadapay.dev' ||
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.href = result.url;
717
+ window.location.replace(result.url);
718
718
  }
719
719
  return result;
720
720
  }
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Base API Client using Axios
2
+ * Base API Client using native fetch
3
3
  * Shared between all resource clients
4
4
  */
5
- import { AxiosInstance, AxiosRequestConfig } from 'axios';
6
- declare module 'axios' {
7
- interface AxiosRequestConfig {
8
- skipAuth?: boolean;
9
- }
5
+ export interface RequestConfig {
6
+ headers?: Record<string, string>;
7
+ skipAuth?: boolean;
8
+ params?: Record<string, string>;
9
+ signal?: AbortSignal;
10
10
  }
11
11
  export interface ApiClientConfig {
12
12
  baseURL: string;
@@ -15,7 +15,9 @@ export interface ApiClientConfig {
15
15
  }
16
16
  export type TokenProvider = () => Promise<string | null>;
17
17
  export declare class ApiClient {
18
- axios: AxiosInstance;
18
+ private baseURL;
19
+ private timeout;
20
+ private defaultHeaders;
19
21
  private currentToken;
20
22
  private tokenProvider;
21
23
  private requestHistory;
@@ -23,18 +25,20 @@ export declare class ApiClient {
23
25
  private readonly MAX_REQUESTS;
24
26
  constructor(config: ApiClientConfig);
25
27
  setTokenProvider(provider: TokenProvider): void;
26
- get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>;
27
- post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>;
28
- put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>;
29
- patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>;
30
- delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>;
28
+ get<T = unknown>(url: string, config?: RequestConfig): Promise<T>;
29
+ post<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
30
+ put<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
31
+ patch<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
32
+ delete<T = unknown>(url: string, config?: RequestConfig): Promise<T>;
31
33
  setHeader(key: string, value: string): void;
32
34
  removeHeader(key: string): void;
33
35
  updateToken(token: string | null): void;
34
36
  getCurrentToken(): string | null;
35
37
  updateConfig(config: Partial<ApiClientConfig>): void;
38
+ private request;
39
+ /** Convert a non-ok fetch response into the appropriate TagadaError subclass. */
40
+ private toTagadaError;
41
+ private safeParseJson;
36
42
  private checkRequestLimit;
37
43
  private cleanupHistory;
38
- /** Convert an AxiosError into the appropriate TagadaError subclass. */
39
- private toTagadaError;
40
44
  }
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Base API Client using Axios
2
+ * Base API Client using native fetch
3
3
  * Shared between all resource clients
4
4
  */
5
- import axios from 'axios';
6
5
  import { TagadaApiError, TagadaAuthError, TagadaNetworkError, TagadaCircuitBreakerError, TagadaError, TagadaErrorCode, } from '../errors';
7
6
  export class ApiClient {
8
7
  constructor(config) {
@@ -12,67 +11,16 @@ export class ApiClient {
12
11
  this.requestHistory = new Map();
13
12
  this.WINDOW_MS = 5000; // 5 seconds window
14
13
  this.MAX_REQUESTS = 30; // Max 30 requests per endpoint in window
15
- this.axios = axios.create({
16
- baseURL: config.baseURL,
17
- timeout: config.timeout || 60000, // 60 seconds for payment operations
18
- headers: {
19
- 'Content-Type': 'application/json',
20
- ...config.headers,
21
- },
22
- });
14
+ this.baseURL = config.baseURL;
15
+ this.timeout = config.timeout || 60000; // 60 seconds for payment operations
16
+ this.defaultHeaders = {
17
+ 'Content-Type': 'application/json',
18
+ ...config.headers,
19
+ };
23
20
  // Cleanup interval for circuit breaker history
24
21
  if (typeof setInterval !== 'undefined') {
25
22
  setInterval(() => this.cleanupHistory(), 10000);
26
23
  }
27
- // Request interceptor for logging and auth
28
- this.axios.interceptors.request.use(async (config) => {
29
- // Circuit Breaker Check
30
- if (config.url) {
31
- try {
32
- this.checkRequestLimit(`${config.method?.toUpperCase()}:${config.url}`);
33
- }
34
- catch (error) {
35
- console.error('[SDK] 🛑 Request blocked by Circuit Breaker:', error);
36
- return Promise.reject(error);
37
- }
38
- }
39
- // Check if we need to wait for token
40
- if (!config.skipAuth && !this.currentToken && this.tokenProvider) {
41
- try {
42
- console.log('[SDK] Waiting for token...');
43
- const token = await this.tokenProvider();
44
- if (token) {
45
- this.updateToken(token);
46
- // Ensure header is set on this specific request config
47
- config.headers['x-cms-token'] = token;
48
- }
49
- }
50
- catch (error) {
51
- console.error('[SDK] Failed to get token from provider:', error);
52
- }
53
- }
54
- // Ensure token is in headers if we have it (and not skipped)
55
- if (!config.skipAuth && this.currentToken) {
56
- config.headers['x-cms-token'] = this.currentToken;
57
- }
58
- console.log(`[SDK] Making ${config.method?.toUpperCase()} request to: ${config.baseURL || ''}${config.url}`);
59
- // console.log('[SDK] Request headers:', config.headers);
60
- return config;
61
- }, (error) => {
62
- console.error('[SDK] Request error:', error);
63
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
64
- });
65
- // Response interceptor — maps Axios errors to structured TagadaError subtypes
66
- this.axios.interceptors.response.use((response) => response, (error) => {
67
- console.error('[SDK] Response error:', error.message);
68
- if (error instanceof TagadaError) {
69
- return Promise.reject(error);
70
- }
71
- if (axios.isAxiosError(error)) {
72
- return Promise.reject(this.toTagadaError(error));
73
- }
74
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
75
- });
76
24
  }
77
25
  // Set a provider that returns a promise resolving to the token
78
26
  // This allows requests to wait until the token is ready
@@ -81,31 +29,26 @@ export class ApiClient {
81
29
  }
82
30
  // Convenience methods
83
31
  async get(url, config) {
84
- const response = await this.axios.get(url, config);
85
- return response.data;
32
+ return this.request('GET', url, undefined, config);
86
33
  }
87
34
  async post(url, data, config) {
88
- const response = await this.axios.post(url, data, config);
89
- return response.data;
35
+ return this.request('POST', url, data, config);
90
36
  }
91
37
  async put(url, data, config) {
92
- const response = await this.axios.put(url, data, config);
93
- return response.data;
38
+ return this.request('PUT', url, data, config);
94
39
  }
95
40
  async patch(url, data, config) {
96
- const response = await this.axios.patch(url, data, config);
97
- return response.data;
41
+ return this.request('PATCH', url, data, config);
98
42
  }
99
43
  async delete(url, config) {
100
- const response = await this.axios.delete(url, config);
101
- return response.data;
44
+ return this.request('DELETE', url, undefined, config);
102
45
  }
103
46
  // Update headers (useful for auth tokens)
104
47
  setHeader(key, value) {
105
- this.axios.defaults.headers.common[key] = value;
48
+ this.defaultHeaders[key] = value;
106
49
  }
107
50
  removeHeader(key) {
108
- delete this.axios.defaults.headers.common[key];
51
+ delete this.defaultHeaders[key];
109
52
  }
110
53
  // Token management methods (matching old ApiService pattern)
111
54
  updateToken(token) {
@@ -125,59 +68,106 @@ export class ApiClient {
125
68
  // Update configuration (useful for environment changes)
126
69
  updateConfig(config) {
127
70
  if (config.baseURL) {
128
- this.axios.defaults.baseURL = config.baseURL;
71
+ this.baseURL = config.baseURL;
129
72
  }
130
73
  if (config.timeout) {
131
- this.axios.defaults.timeout = config.timeout;
74
+ this.timeout = config.timeout;
132
75
  }
133
76
  if (config.headers) {
134
- Object.assign(this.axios.defaults.headers.common, config.headers);
77
+ Object.assign(this.defaultHeaders, config.headers);
135
78
  }
136
79
  console.log('[SDK] ApiClient configuration updated');
137
80
  }
138
- // Circuit Breaker Implementation
139
- checkRequestLimit(key) {
140
- const now = Date.now();
141
- const history = this.requestHistory.get(key);
142
- if (!history) {
143
- this.requestHistory.set(key, { count: 1, firstRequestTime: now });
144
- return;
81
+ // ---- Core request method ----
82
+ async request(method, url, data, config) {
83
+ const requestKey = `${method}:${url}`;
84
+ // Circuit Breaker Check
85
+ try {
86
+ this.checkRequestLimit(requestKey);
145
87
  }
146
- if (now - history.firstRequestTime > this.WINDOW_MS) {
147
- // Window expired, reset
148
- this.requestHistory.set(key, { count: 1, firstRequestTime: now });
149
- return;
88
+ catch (error) {
89
+ console.error('[SDK] 🛑 Request blocked by Circuit Breaker:', error);
90
+ throw error;
150
91
  }
151
- history.count++;
152
- if (history.count > this.MAX_REQUESTS) {
153
- throw new TagadaCircuitBreakerError(`Circuit Breaker: Too many requests to ${key} (${history.count} in ${this.WINDOW_MS}ms)`);
92
+ // Token injection
93
+ if (!config?.skipAuth && !this.currentToken && this.tokenProvider) {
94
+ try {
95
+ console.log('[SDK] Waiting for token...');
96
+ const token = await this.tokenProvider();
97
+ if (token) {
98
+ this.updateToken(token);
99
+ }
100
+ }
101
+ catch (error) {
102
+ console.error('[SDK] Failed to get token from provider:', error);
103
+ }
154
104
  }
155
- }
156
- cleanupHistory() {
157
- const now = Date.now();
158
- for (const [key, history] of this.requestHistory.entries()) {
159
- if (now - history.firstRequestTime > this.WINDOW_MS) {
160
- this.requestHistory.delete(key);
105
+ // Build headers
106
+ const headers = { ...this.defaultHeaders };
107
+ if (config?.headers) {
108
+ Object.assign(headers, config.headers);
109
+ }
110
+ if (!config?.skipAuth && this.currentToken) {
111
+ headers['x-cms-token'] = this.currentToken;
112
+ }
113
+ // Build URL
114
+ let fullUrl = `${this.baseURL}${url}`;
115
+ if (config?.params) {
116
+ const searchParams = new URLSearchParams(config.params);
117
+ fullUrl += `?${searchParams.toString()}`;
118
+ }
119
+ console.log(`[SDK] Making ${method} request to: ${fullUrl}`);
120
+ // Timeout via AbortController
121
+ const controller = new AbortController();
122
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
123
+ // Merge signals if caller provided one
124
+ const signal = config?.signal
125
+ ? anySignal([config.signal, controller.signal])
126
+ : controller.signal;
127
+ try {
128
+ const fetchOptions = {
129
+ method,
130
+ headers,
131
+ signal,
132
+ };
133
+ if (data !== undefined) {
134
+ fetchOptions.body = JSON.stringify(data);
135
+ }
136
+ const response = await fetch(fullUrl, fetchOptions);
137
+ if (!response.ok) {
138
+ const errorData = await this.safeParseJson(response);
139
+ throw this.toTagadaError(response.status, errorData, response.statusText);
161
140
  }
141
+ const responseData = await response.json();
142
+ return responseData;
162
143
  }
163
- }
164
- /** Convert an AxiosError into the appropriate TagadaError subclass. */
165
- toTagadaError(error) {
166
- const status = error.response?.status;
167
- const data = error.response?.data;
168
- const serverMessage = data?.message ??
169
- data?.error ??
170
- error.message;
171
- // No response at all → network-level failure
172
- if (!error.response) {
173
- if (error.code === 'ECONNABORTED') {
174
- return new TagadaApiError('Request timed out', 0, {
144
+ catch (error) {
145
+ if (error instanceof TagadaError) {
146
+ throw error;
147
+ }
148
+ // AbortError timeout (from our controller) or caller abort
149
+ if (error instanceof DOMException && error.name === 'AbortError') {
150
+ throw new TagadaApiError('Request timed out', 0, {
175
151
  code: TagadaErrorCode.TIMEOUT,
176
152
  retryable: true,
177
153
  });
178
154
  }
179
- return new TagadaNetworkError(serverMessage);
155
+ // TypeError → network failure (DNS, CORS, offline, etc.)
156
+ if (error instanceof TypeError) {
157
+ throw new TagadaNetworkError(error.message);
158
+ }
159
+ throw error instanceof Error ? error : new Error(String(error));
180
160
  }
161
+ finally {
162
+ clearTimeout(timeoutId);
163
+ }
164
+ }
165
+ // ---- Error mapping ----
166
+ /** Convert a non-ok fetch response into the appropriate TagadaError subclass. */
167
+ toTagadaError(status, data, statusText) {
168
+ const serverMessage = data?.message ??
169
+ data?.error ??
170
+ statusText;
181
171
  // Auth failures
182
172
  if (status === 401 || status === 403) {
183
173
  return new TagadaAuthError(serverMessage, status);
@@ -197,10 +187,62 @@ export class ApiClient {
197
187
  });
198
188
  }
199
189
  // Generic API error
200
- return new TagadaApiError(serverMessage, status ?? 0, {
190
+ return new TagadaApiError(serverMessage, status, {
201
191
  code: data?.code ?? TagadaErrorCode.API_ERROR,
202
192
  details: data,
203
- retryable: (status ?? 0) >= 500,
193
+ retryable: status >= 500,
194
+ });
195
+ }
196
+ // ---- Helpers ----
197
+ async safeParseJson(response) {
198
+ try {
199
+ return await response.json();
200
+ }
201
+ catch {
202
+ return undefined;
203
+ }
204
+ }
205
+ // Circuit Breaker Implementation
206
+ checkRequestLimit(key) {
207
+ const now = Date.now();
208
+ const history = this.requestHistory.get(key);
209
+ if (!history) {
210
+ this.requestHistory.set(key, { count: 1, firstRequestTime: now });
211
+ return;
212
+ }
213
+ if (now - history.firstRequestTime > this.WINDOW_MS) {
214
+ // Window expired, reset
215
+ this.requestHistory.set(key, { count: 1, firstRequestTime: now });
216
+ return;
217
+ }
218
+ history.count++;
219
+ if (history.count > this.MAX_REQUESTS) {
220
+ throw new TagadaCircuitBreakerError(`Circuit Breaker: Too many requests to ${key} (${history.count} in ${this.WINDOW_MS}ms)`);
221
+ }
222
+ }
223
+ cleanupHistory() {
224
+ const now = Date.now();
225
+ for (const [key, history] of this.requestHistory.entries()) {
226
+ if (now - history.firstRequestTime > this.WINDOW_MS) {
227
+ this.requestHistory.delete(key);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ /**
233
+ * Combine multiple AbortSignals into one that aborts when any input signal aborts.
234
+ */
235
+ function anySignal(signals) {
236
+ const controller = new AbortController();
237
+ for (const signal of signals) {
238
+ if (signal.aborted) {
239
+ controller.abort(signal.reason);
240
+ return controller.signal;
241
+ }
242
+ signal.addEventListener('abort', () => controller.abort(signal.reason), {
243
+ once: true,
244
+ signal: controller.signal,
204
245
  });
205
246
  }
247
+ return controller.signal;
206
248
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { OrderAddress } from '../types';
6
6
  import { ApiClient } from './apiClient';
7
- export type PaymentMethodName = 'paypal' | 'card' | 'klarna' | 'zelle';
7
+ export type PaymentMethodName = 'card' | 'paypal' | 'klarna' | 'apple_pay' | 'google_pay' | 'link' | 'bridge' | 'whop' | 'zelle' | 'hipay' | 'crypto' | 'convesiopay' | 'oceanpayment' | 'afterpay' | 'ideal' | 'bancontact' | 'giropay' | 'sepa_debit' | (string & {});
8
8
  export interface CheckoutLineItem {
9
9
  externalProductId?: string | null;
10
10
  externalVariantId?: string | null;
@@ -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.tagadapay.com)
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
+ }
@@ -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,