@tagadapay/plugin-sdk 2.8.8 → 2.8.10

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 (43) hide show
  1. package/dist/react/config/environment.d.ts +1 -22
  2. package/dist/react/config/environment.js +1 -132
  3. package/dist/react/utils/deviceInfo.d.ts +1 -39
  4. package/dist/react/utils/deviceInfo.js +1 -163
  5. package/dist/react/utils/jwtDecoder.d.ts +1 -14
  6. package/dist/react/utils/jwtDecoder.js +1 -86
  7. package/dist/react/utils/tokenStorage.d.ts +1 -16
  8. package/dist/react/utils/tokenStorage.js +1 -53
  9. package/dist/v2/core/client.d.ts +96 -0
  10. package/dist/v2/core/client.js +430 -0
  11. package/dist/v2/core/config/environment.d.ts +36 -0
  12. package/dist/v2/core/config/environment.js +155 -0
  13. package/dist/v2/core/pathRemapping.js +61 -3
  14. package/dist/v2/core/resources/apiClient.d.ts +13 -0
  15. package/dist/v2/core/resources/apiClient.js +77 -9
  16. package/dist/v2/core/resources/funnel.d.ts +21 -0
  17. package/dist/v2/core/resources/payments.d.ts +23 -0
  18. package/dist/v2/core/types.d.ts +271 -0
  19. package/dist/v2/core/types.js +4 -0
  20. package/dist/v2/core/utils/deviceInfo.d.ts +39 -0
  21. package/dist/v2/core/utils/deviceInfo.js +162 -0
  22. package/dist/v2/core/utils/eventDispatcher.d.ts +10 -0
  23. package/dist/v2/core/utils/eventDispatcher.js +24 -0
  24. package/dist/v2/core/utils/jwtDecoder.d.ts +14 -0
  25. package/dist/v2/core/utils/jwtDecoder.js +85 -0
  26. package/dist/v2/core/utils/pluginConfig.d.ts +1 -0
  27. package/dist/v2/core/utils/pluginConfig.js +64 -8
  28. package/dist/v2/core/utils/tokenStorage.d.ts +19 -0
  29. package/dist/v2/core/utils/tokenStorage.js +52 -0
  30. package/dist/v2/react/components/ApplePayButton.js +1 -1
  31. package/dist/v2/react/components/DebugDrawer.js +90 -1
  32. package/dist/v2/react/hooks/__examples__/FunnelContextExample.d.ts +12 -0
  33. package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +54 -0
  34. package/dist/v2/react/hooks/useFunnel.d.ts +2 -1
  35. package/dist/v2/react/hooks/useFunnel.js +245 -69
  36. package/dist/v2/react/hooks/useGoogleAutocomplete.js +26 -18
  37. package/dist/v2/react/hooks/useISOData.js +4 -2
  38. package/dist/v2/react/hooks/useOffersQuery.d.ts +42 -29
  39. package/dist/v2/react/hooks/useOffersQuery.js +266 -204
  40. package/dist/v2/react/hooks/usePaymentQuery.js +99 -6
  41. package/dist/v2/react/providers/TagadaProvider.d.ts +13 -21
  42. package/dist/v2/react/providers/TagadaProvider.js +79 -673
  43. package/package.json +1 -1
@@ -0,0 +1,430 @@
1
+ import { ApiClient } from './resources/apiClient';
2
+ import { detectEnvironment, getEnvironmentConfig } from './config/environment';
3
+ import { loadPluginConfig } from './utils/pluginConfig';
4
+ import { collectDeviceInfo, getBrowserLocale, getUrlParams } from './utils/deviceInfo';
5
+ import { decodeJWTClient, isTokenExpired } from './utils/jwtDecoder';
6
+ import { getClientToken, setClientToken } from './utils/tokenStorage';
7
+ import { EventDispatcher } from './utils/eventDispatcher';
8
+ export class TagadaClient {
9
+ constructor(config = {}) {
10
+ this.eventDispatcher = new EventDispatcher();
11
+ this.tokenPromise = null;
12
+ this.tokenResolver = null;
13
+ // Track initialization state to prevent infinite loops
14
+ this.isInitializingSession = false;
15
+ this.lastSessionInitError = null;
16
+ this.sessionInitRetryCount = 0;
17
+ this.MAX_SESSION_INIT_RETRIES = 3;
18
+ this.config = config;
19
+ this.instanceId = Math.random().toString(36).substr(2, 9);
20
+ this.boundHandleStorageChange = this.handleStorageChange.bind(this);
21
+ if (this.config.debugMode) {
22
+ console.log(`[TagadaClient ${this.instanceId}] Initializing...`);
23
+ }
24
+ // Initialize default state
25
+ const env = this.resolveEnvironment();
26
+ const envConfig = getEnvironmentConfig(env);
27
+ this.state = {
28
+ auth: {
29
+ isAuthenticated: false,
30
+ isLoading: false,
31
+ customer: null,
32
+ session: null,
33
+ },
34
+ session: null,
35
+ customer: null,
36
+ locale: {
37
+ locale: 'en-US',
38
+ language: 'en',
39
+ region: 'US',
40
+ messages: {},
41
+ },
42
+ currency: {
43
+ code: 'USD',
44
+ symbol: '$',
45
+ name: 'US Dollar',
46
+ },
47
+ store: null,
48
+ environment: envConfig,
49
+ isLoading: true,
50
+ isInitialized: false,
51
+ isSessionInitialized: false,
52
+ pluginConfig: { basePath: '/', config: {} },
53
+ pluginConfigLoading: !config.rawPluginConfig,
54
+ debugMode: config.debugMode ?? env !== 'production',
55
+ token: null,
56
+ };
57
+ // Initialize API Client
58
+ this.apiClient = new ApiClient({
59
+ baseURL: envConfig.apiConfig.baseUrl,
60
+ });
61
+ // Setup token waiting mechanism
62
+ this.apiClient.setTokenProvider(this.waitForToken.bind(this));
63
+ // Listen for storage changes (cross-tab sync)
64
+ if (typeof window !== 'undefined') {
65
+ window.addEventListener('storage', this.boundHandleStorageChange);
66
+ }
67
+ // Start initialization
68
+ this.initialize();
69
+ }
70
+ /**
71
+ * Cleanup client resources
72
+ */
73
+ destroy() {
74
+ if (typeof window !== 'undefined') {
75
+ window.removeEventListener('storage', this.boundHandleStorageChange);
76
+ }
77
+ if (this.state.debugMode) {
78
+ console.log(`[TagadaClient ${this.instanceId}] Destroyed`);
79
+ }
80
+ this.eventDispatcher.clear();
81
+ }
82
+ /**
83
+ * Handle storage changes (e.g. token update in another tab)
84
+ */
85
+ handleStorageChange() {
86
+ const storedToken = getClientToken();
87
+ // Avoid unnecessary re-initialization if token hasn't changed
88
+ if (storedToken === this.state.token) {
89
+ if (this.state.debugMode) {
90
+ console.log(`[TagadaClient ${this.instanceId}] Token unchanged (ignoring event)`);
91
+ }
92
+ return;
93
+ }
94
+ // Prevent infinite loop: Don't re-initialize if we're currently initializing
95
+ if (this.isInitializingSession) {
96
+ if (this.state.debugMode) {
97
+ console.log(`[TagadaClient ${this.instanceId}] Session initialization in progress, skipping storage change`);
98
+ }
99
+ return;
100
+ }
101
+ // Prevent infinite loop: Don't retry if we've hit max retries
102
+ if (this.sessionInitRetryCount >= this.MAX_SESSION_INIT_RETRIES && this.lastSessionInitError) {
103
+ if (this.state.debugMode) {
104
+ console.error(`[TagadaClient ${this.instanceId}] Max session init retries reached, giving up`, this.lastSessionInitError);
105
+ }
106
+ return;
107
+ }
108
+ // Re-run initialization when token may have changed
109
+ if (this.state.debugMode) {
110
+ console.log(`[TagadaClient ${this.instanceId}] Storage changed, re-initializing token...`);
111
+ }
112
+ this.initializeToken();
113
+ }
114
+ /**
115
+ * Subscribe to state changes
116
+ */
117
+ subscribe(listener) {
118
+ return this.eventDispatcher.subscribe(listener);
119
+ }
120
+ /**
121
+ * Get current state
122
+ */
123
+ getState() {
124
+ return this.state;
125
+ }
126
+ /**
127
+ * Update state and notify listeners
128
+ */
129
+ updateState(updates) {
130
+ this.state = { ...this.state, ...updates };
131
+ this.eventDispatcher.notify(this.state);
132
+ }
133
+ /**
134
+ * Resolve environment
135
+ */
136
+ resolveEnvironment() {
137
+ if (this.config.environment)
138
+ return this.config.environment;
139
+ return detectEnvironment();
140
+ }
141
+ /**
142
+ * Main initialization flow
143
+ */
144
+ async initialize() {
145
+ try {
146
+ // 1. Load Plugin Config
147
+ await this.initializePluginConfig();
148
+ // 2. Initialize Token (Background or Blocking based on config)
149
+ if (this.state.pluginConfig.storeId) {
150
+ await this.initializeToken();
151
+ }
152
+ else {
153
+ console.warn('[TagadaClient] No store ID found in plugin config. Skipping token initialization.');
154
+ this.updateState({ isLoading: false, isInitialized: true });
155
+ }
156
+ }
157
+ catch (error) {
158
+ console.error('[TagadaClient] Initialization failed:', error);
159
+ this.updateState({ isLoading: false, isInitialized: true });
160
+ }
161
+ }
162
+ /**
163
+ * Load plugin configuration
164
+ */
165
+ async initializePluginConfig() {
166
+ if (!this.state.pluginConfigLoading)
167
+ return;
168
+ try {
169
+ const configVariant = this.config.localConfig || 'default';
170
+ const config = await loadPluginConfig(configVariant, this.config.rawPluginConfig);
171
+ this.updateState({
172
+ pluginConfig: config,
173
+ pluginConfigLoading: false,
174
+ });
175
+ if (this.state.debugMode) {
176
+ console.log('[TagadaClient] Plugin config loaded:', config);
177
+ }
178
+ }
179
+ catch (error) {
180
+ console.error('[TagadaClient] Failed to load plugin config:', error);
181
+ this.updateState({
182
+ pluginConfig: { basePath: '/', config: {} },
183
+ pluginConfigLoading: false,
184
+ });
185
+ }
186
+ }
187
+ /**
188
+ * Initialize token and session
189
+ */
190
+ async initializeToken() {
191
+ // Check for existing token in URL or storage
192
+ const existingToken = getClientToken();
193
+ const urlParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
194
+ const queryToken = urlParams.get('token');
195
+ if (this.state.debugMode) {
196
+ console.log(`[TagadaClient ${this.instanceId}] Initializing token...`, {
197
+ hasExistingToken: !!existingToken,
198
+ hasQueryToken: !!queryToken
199
+ });
200
+ }
201
+ let tokenToUse = null;
202
+ let shouldPersist = false;
203
+ if (queryToken) {
204
+ tokenToUse = queryToken;
205
+ shouldPersist = true;
206
+ }
207
+ else if (existingToken && !isTokenExpired(existingToken)) {
208
+ tokenToUse = existingToken;
209
+ }
210
+ if (tokenToUse) {
211
+ this.setToken(tokenToUse);
212
+ // Persist token if it came from query (updates localStorage and fires event)
213
+ // We do this AFTER setToken so state is updated and handleStorageChange sees match
214
+ if (shouldPersist) {
215
+ if (this.state.debugMode) {
216
+ console.log(`[TagadaClient ${this.instanceId}] Persisting query token to storage...`);
217
+ }
218
+ setClientToken(tokenToUse);
219
+ }
220
+ const decodedSession = decodeJWTClient(tokenToUse);
221
+ if (decodedSession) {
222
+ this.updateState({ session: decodedSession });
223
+ await this.initializeSession(decodedSession);
224
+ }
225
+ else {
226
+ console.error('[TagadaClient] Failed to decode token');
227
+ this.updateState({ isInitialized: true, isLoading: false });
228
+ }
229
+ }
230
+ else {
231
+ // Create anonymous token
232
+ const storeId = this.state.pluginConfig.storeId;
233
+ if (storeId) {
234
+ if (this.state.debugMode) {
235
+ console.log(`[TagadaClient ${this.instanceId}] Creating anonymous token for store:`, storeId);
236
+ }
237
+ await this.createAnonymousToken(storeId);
238
+ }
239
+ else {
240
+ this.updateState({ isInitialized: true, isLoading: false });
241
+ }
242
+ }
243
+ }
244
+ /**
245
+ * Set token and resolve waiting requests
246
+ */
247
+ setToken(token) {
248
+ this.apiClient.updateToken(token);
249
+ this.updateState({ token });
250
+ // Notify waiting requests
251
+ if (this.tokenResolver) {
252
+ this.tokenResolver(token);
253
+ this.tokenPromise = null;
254
+ this.tokenResolver = null;
255
+ }
256
+ else {
257
+ // If we set a token but no one was waiting, ensure future requests don't wait unnecessarily
258
+ // Actually, if we have a token, the provider won't be called by ApiClient logic if we handle it right.
259
+ // But to be safe, if we set a token, we can pre-resolve the promise if it exists.
260
+ // Wait, if tokenPromise exists, tokenResolver must exist.
261
+ }
262
+ }
263
+ /**
264
+ * Wait for token to be available
265
+ */
266
+ waitForToken() {
267
+ if (this.apiClient.getCurrentToken()) {
268
+ return Promise.resolve(this.apiClient.getCurrentToken());
269
+ }
270
+ if (!this.tokenPromise) {
271
+ this.tokenPromise = new Promise((resolve) => {
272
+ this.tokenResolver = resolve;
273
+ });
274
+ }
275
+ return this.tokenPromise;
276
+ }
277
+ /**
278
+ * Create anonymous token
279
+ */
280
+ async createAnonymousToken(storeId) {
281
+ // Prevent concurrent anonymous token creation
282
+ if (this.isInitializingSession) {
283
+ if (this.state.debugMode) {
284
+ console.log(`[TagadaClient ${this.instanceId}] Session initialization in progress, skipping anonymous token creation`);
285
+ }
286
+ return;
287
+ }
288
+ try {
289
+ if (this.state.debugMode)
290
+ console.log('[TagadaClient] Creating anonymous token for store:', storeId);
291
+ // We use fetch directly or ApiClient with skipAuth to avoid waiting for itself
292
+ const response = await this.apiClient.post('/api/v1/cms/session/anonymous', { storeId, role: 'anonymous' }, { skipAuth: true });
293
+ this.setToken(response.token);
294
+ setClientToken(response.token);
295
+ const decodedSession = decodeJWTClient(response.token);
296
+ if (decodedSession) {
297
+ this.updateState({ session: decodedSession });
298
+ await this.initializeSession(decodedSession);
299
+ }
300
+ this.updateState({ isSessionInitialized: true });
301
+ }
302
+ catch (error) {
303
+ console.error('[TagadaClient] Failed to create anonymous token:', error);
304
+ this.updateState({ isInitialized: true, isLoading: false });
305
+ }
306
+ }
307
+ /**
308
+ * Initialize session
309
+ */
310
+ async initializeSession(sessionData) {
311
+ // Prevent concurrent initialization attempts
312
+ if (this.isInitializingSession) {
313
+ if (this.state.debugMode) {
314
+ console.log(`[TagadaClient ${this.instanceId}] Session initialization already in progress, skipping`);
315
+ }
316
+ return;
317
+ }
318
+ this.isInitializingSession = true;
319
+ try {
320
+ if (this.state.debugMode) {
321
+ console.log(`[TagadaClient ${this.instanceId}] Initializing session...`, { sessionId: sessionData.sessionId });
322
+ }
323
+ const deviceInfo = collectDeviceInfo();
324
+ const urlParams = getUrlParams();
325
+ const browserLocale = getBrowserLocale();
326
+ const sessionInitData = {
327
+ storeId: sessionData.storeId,
328
+ accountId: sessionData.accountId,
329
+ customerId: sessionData.customerId,
330
+ role: sessionData.role,
331
+ browserLocale,
332
+ queryLocale: urlParams.locale,
333
+ queryCurrency: urlParams.currency,
334
+ utmSource: urlParams.utmSource,
335
+ utmMedium: urlParams.utmMedium,
336
+ utmCampaign: urlParams.utmCampaign,
337
+ browser: deviceInfo.userAgent.browser.name,
338
+ browserVersion: deviceInfo.userAgent.browser.version,
339
+ os: deviceInfo.userAgent.os.name,
340
+ osVersion: deviceInfo.userAgent.os.version,
341
+ deviceType: deviceInfo.userAgent.device?.type,
342
+ deviceModel: deviceInfo.userAgent.device?.model,
343
+ screenWidth: deviceInfo.screenResolution.width,
344
+ screenHeight: deviceInfo.screenResolution.height,
345
+ timeZone: deviceInfo.timeZone,
346
+ };
347
+ const response = await this.apiClient.post('/api/v1/cms/session/init', sessionInitData);
348
+ // Success - reset error tracking
349
+ this.lastSessionInitError = null;
350
+ this.sessionInitRetryCount = 0;
351
+ // Update state with session data
352
+ this.updateSessionState(response, sessionData);
353
+ this.updateState({
354
+ isInitialized: true,
355
+ isSessionInitialized: true,
356
+ isLoading: false,
357
+ });
358
+ if (this.state.debugMode)
359
+ console.log('[TagadaClient] Session initialized successfully');
360
+ }
361
+ catch (error) {
362
+ // Track error and increment retry count
363
+ this.lastSessionInitError = error;
364
+ this.sessionInitRetryCount++;
365
+ console.error(`[TagadaClient] Error initializing session (attempt ${this.sessionInitRetryCount}/${this.MAX_SESSION_INIT_RETRIES}):`, error);
366
+ this.updateState({
367
+ isInitialized: true,
368
+ isLoading: false,
369
+ });
370
+ }
371
+ finally {
372
+ // Always release the lock
373
+ this.isInitializingSession = false;
374
+ }
375
+ }
376
+ updateSessionState(response, sessionData) {
377
+ // Update Store
378
+ if (response.store) {
379
+ const storeData = response.store;
380
+ const storeConfig = {
381
+ ...response.store,
382
+ presentmentCurrencies: storeData.presentmentCurrencies || [response.store.currency || 'USD'],
383
+ chargeCurrencies: storeData.chargeCurrencies || [response.store.currency || 'USD'],
384
+ };
385
+ this.updateState({ store: storeConfig });
386
+ }
387
+ // Update Locale
388
+ const localeConfig = {
389
+ locale: response.locale,
390
+ language: response.locale.split('-')[0],
391
+ region: response.locale.split('-')[1] ?? 'US',
392
+ messages: response.messages ?? {},
393
+ };
394
+ this.updateState({ locale: localeConfig });
395
+ // Update Currency
396
+ if (response.store) {
397
+ const currencyConfig = {
398
+ code: response.store.currency,
399
+ symbol: this.getCurrencySymbol(response.store.currency),
400
+ name: this.getCurrencyName(response.store.currency),
401
+ };
402
+ this.updateState({ currency: currencyConfig });
403
+ }
404
+ // Update Customer & Auth
405
+ const authState = {
406
+ isAuthenticated: response.customer?.isAuthenticated ?? false,
407
+ isLoading: false,
408
+ customer: response.customer ?? null,
409
+ session: sessionData,
410
+ };
411
+ this.updateState({
412
+ customer: response.customer ?? null,
413
+ auth: authState
414
+ });
415
+ }
416
+ // Helper methods
417
+ getCurrencySymbol(code) {
418
+ const symbols = {
419
+ USD: '$', EUR: '€', GBP: '£', JPY: '¥', CAD: 'C$', AUD: 'A$',
420
+ };
421
+ return symbols[code] || code;
422
+ }
423
+ getCurrencyName(code) {
424
+ const names = {
425
+ USD: 'US Dollar', EUR: 'Euro', GBP: 'British Pound',
426
+ JPY: 'Japanese Yen', CAD: 'Canadian Dollar', AUD: 'Australian Dollar',
427
+ };
428
+ return names[code] || code;
429
+ }
430
+ }
@@ -0,0 +1,36 @@
1
+ import { ApiConfig, Environment, EnvironmentConfig } from '../types';
2
+ /**
3
+ * ⚠️ IMPORTANT: Runtime Environment Detection
4
+ *
5
+ * This SDK uses RUNTIME hostname detection, NOT build-time environment variables.
6
+ * This ensures the SDK always connects to the correct API based on where it's deployed.
7
+ *
8
+ * - Production domains → production API
9
+ * - Dev/staging domains → development API
10
+ * - Localhost/local IPs → local API (with optional override via window.__TAGADA_ENV__)
11
+ *
12
+ * Build-time .env variables (VITE_*, REACT_APP_*, NEXT_PUBLIC_*) are IGNORED
13
+ * to prevent incorrect API connections when plugins are deployed to different environments.
14
+ */
15
+ /**
16
+ * Environment configurations for different deployment environments
17
+ */
18
+ export declare const ENVIRONMENT_CONFIGS: Record<Environment, ApiConfig>;
19
+ /**
20
+ * Get the environment configuration based on the current environment
21
+ */
22
+ export declare function getEnvironmentConfig(environment?: Environment): EnvironmentConfig;
23
+ /**
24
+ * Build a complete API URL from environment config and endpoint path
25
+ */
26
+ export declare function buildApiUrl(config: EnvironmentConfig, endpointPath: string): string;
27
+ /**
28
+ * Get a specific endpoint URL
29
+ */
30
+ export declare function getEndpointUrl(config: EnvironmentConfig, category: keyof ApiConfig['endpoints'], endpoint: string): string;
31
+ /**
32
+ * Auto-detect environment based on hostname and URL patterns at RUNTIME
33
+ * ⚠️ IMPORTANT: Ignores build-time .env variables to ensure correct detection in all environments
34
+ * .env variables are ONLY used for local development via window.__TAGADA_ENV__
35
+ */
36
+ export declare function detectEnvironment(): Environment;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * ⚠️ IMPORTANT: Runtime Environment Detection
3
+ *
4
+ * This SDK uses RUNTIME hostname detection, NOT build-time environment variables.
5
+ * This ensures the SDK always connects to the correct API based on where it's deployed.
6
+ *
7
+ * - Production domains → production API
8
+ * - Dev/staging domains → development API
9
+ * - Localhost/local IPs → local API (with optional override via window.__TAGADA_ENV__)
10
+ *
11
+ * Build-time .env variables (VITE_*, REACT_APP_*, NEXT_PUBLIC_*) are IGNORED
12
+ * to prevent incorrect API connections when plugins are deployed to different environments.
13
+ */
14
+ /**
15
+ * Environment configurations for different deployment environments
16
+ */
17
+ export const ENVIRONMENT_CONFIGS = {
18
+ production: {
19
+ baseUrl: 'https://app.tagadapay.com',
20
+ endpoints: {
21
+ checkout: {
22
+ sessionInit: '/api/v1/checkout/session/init',
23
+ sessionStatus: '/api/v1/checkout/session/status',
24
+ },
25
+ customer: {
26
+ profile: '/api/v1/customer/profile',
27
+ session: '/api/v1/customer/session',
28
+ },
29
+ store: {
30
+ config: '/api/v1/store/config',
31
+ },
32
+ },
33
+ },
34
+ development: {
35
+ baseUrl: 'https://app.tagadapay.dev',
36
+ endpoints: {
37
+ checkout: {
38
+ sessionInit: '/api/v1/checkout/session/init',
39
+ sessionStatus: '/api/v1/checkout/session/status',
40
+ },
41
+ customer: {
42
+ profile: '/api/v1/customer/profile',
43
+ session: '/api/v1/customer/session',
44
+ },
45
+ store: {
46
+ config: '/api/v1/store/config',
47
+ },
48
+ },
49
+ },
50
+ local: {
51
+ baseUrl: 'http://app.localhost:3000',
52
+ endpoints: {
53
+ checkout: {
54
+ sessionInit: '/api/v1/checkout/session/init',
55
+ sessionStatus: '/api/v1/checkout/session/status',
56
+ },
57
+ customer: {
58
+ profile: '/api/v1/customer/profile',
59
+ session: '/api/v1/customer/session',
60
+ },
61
+ store: {
62
+ config: '/api/v1/store/config',
63
+ },
64
+ },
65
+ },
66
+ };
67
+ /**
68
+ * Get the environment configuration based on the current environment
69
+ */
70
+ export function getEnvironmentConfig(environment = 'local') {
71
+ const apiConfig = ENVIRONMENT_CONFIGS[environment];
72
+ if (!apiConfig) {
73
+ console.warn(`Unknown environment: ${environment}. Falling back to local.`);
74
+ return {
75
+ environment: 'local',
76
+ apiConfig: ENVIRONMENT_CONFIGS.local,
77
+ };
78
+ }
79
+ return {
80
+ environment,
81
+ apiConfig,
82
+ };
83
+ }
84
+ /**
85
+ * Build a complete API URL from environment config and endpoint path
86
+ */
87
+ export function buildApiUrl(config, endpointPath) {
88
+ return `${config.apiConfig.baseUrl}${endpointPath}`;
89
+ }
90
+ /**
91
+ * Get a specific endpoint URL
92
+ */
93
+ export function getEndpointUrl(config, category, endpoint) {
94
+ const categoryEndpoints = config.apiConfig.endpoints[category];
95
+ const endpointPath = categoryEndpoints[endpoint];
96
+ if (!endpointPath) {
97
+ throw new Error(`Endpoint not found: ${category}.${endpoint}`);
98
+ }
99
+ return buildApiUrl(config, endpointPath);
100
+ }
101
+ /**
102
+ * Auto-detect environment based on hostname and URL patterns at RUNTIME
103
+ * ⚠️ IMPORTANT: Ignores build-time .env variables to ensure correct detection in all environments
104
+ * .env variables are ONLY used for local development via window.__TAGADA_ENV__
105
+ */
106
+ export function detectEnvironment() {
107
+ // Check if we're in browser
108
+ if (typeof window === 'undefined') {
109
+ return 'local'; // SSR fallback
110
+ }
111
+ const hostname = window.location.hostname;
112
+ const href = window.location.href;
113
+ // 1. Check for LOCAL environment first (highest priority for dev)
114
+ // Local: localhost, local IPs, or local domains
115
+ if (hostname === 'localhost' ||
116
+ hostname.startsWith('127.') ||
117
+ hostname.startsWith('192.168.') ||
118
+ hostname.startsWith('10.') ||
119
+ hostname.includes('.local') ||
120
+ hostname === '' ||
121
+ hostname === '0.0.0.0') {
122
+ // For local development, allow override via window.__TAGADA_ENV__ (injected by dev server)
123
+ if (typeof window !== 'undefined' && window?.__TAGADA_ENV__?.TAGADA_ENVIRONMENT) {
124
+ const override = window.__TAGADA_ENV__.TAGADA_ENVIRONMENT.toLowerCase();
125
+ if (override === 'production' || override === 'development' || override === 'local') {
126
+ console.log(`[SDK] Local override detected: ${override}`);
127
+ return override;
128
+ }
129
+ }
130
+ return 'local';
131
+ }
132
+ // 2. Production: deployed to production domains
133
+ if (hostname === 'app.tagadapay.com' ||
134
+ hostname.includes('tagadapay.com') ||
135
+ hostname.includes('yourproductiondomain.com')) {
136
+ return 'production';
137
+ }
138
+ // 3. Development: deployed to staging/dev domains or has dev indicators
139
+ if (hostname === 'app.tagadapay.dev' ||
140
+ hostname.includes('tagadapay.dev') ||
141
+ hostname.includes('vercel.app') ||
142
+ hostname.includes('netlify.app') ||
143
+ hostname.includes('surge.sh') ||
144
+ hostname.includes('github.io') ||
145
+ hostname.includes('herokuapp.com') ||
146
+ hostname.includes('railway.app') ||
147
+ href.includes('?env=dev') ||
148
+ href.includes('?dev=true') ||
149
+ href.includes('#dev')) {
150
+ return 'development';
151
+ }
152
+ // 4. Default fallback for unknown domains (production is safest)
153
+ console.warn(`[SDK] Unknown domain: ${hostname}, defaulting to production`);
154
+ return 'production';
155
+ }