@tagadapay/plugin-sdk 3.1.5 → 3.1.9

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 (71) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +220 -113
  3. package/dist/external-tracker.js +1225 -558
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/hooks/useApplePay.js +25 -36
  7. package/dist/react/hooks/usePaymentPolling.d.ts +9 -3
  8. package/dist/react/providers/TagadaProvider.js +5 -5
  9. package/dist/react/utils/money.d.ts +4 -3
  10. package/dist/react/utils/money.js +39 -6
  11. package/dist/react/utils/trackingUtils.js +1 -0
  12. package/dist/tagada-sdk.js +10142 -0
  13. package/dist/tagada-sdk.min.js +43 -0
  14. package/dist/tagada-sdk.min.js.map +7 -0
  15. package/dist/v2/core/client.js +34 -2
  16. package/dist/v2/core/config/environment.js +9 -2
  17. package/dist/v2/core/funnelClient.d.ts +180 -2
  18. package/dist/v2/core/funnelClient.js +289 -6
  19. package/dist/v2/core/resources/apiClient.js +1 -1
  20. package/dist/v2/core/resources/checkout.d.ts +68 -0
  21. package/dist/v2/core/resources/funnel.d.ts +25 -0
  22. package/dist/v2/core/resources/payments.d.ts +70 -3
  23. package/dist/v2/core/resources/payments.js +72 -7
  24. package/dist/v2/core/utils/index.d.ts +1 -0
  25. package/dist/v2/core/utils/index.js +2 -0
  26. package/dist/v2/core/utils/pluginConfig.d.ts +8 -0
  27. package/dist/v2/core/utils/pluginConfig.js +68 -5
  28. package/dist/v2/core/utils/previewMode.d.ts +7 -0
  29. package/dist/v2/core/utils/previewMode.js +72 -14
  30. package/dist/v2/core/utils/previewModeIndicator.d.ts +19 -0
  31. package/dist/v2/core/utils/previewModeIndicator.js +414 -0
  32. package/dist/v2/core/utils/tokenStorage.d.ts +4 -0
  33. package/dist/v2/core/utils/tokenStorage.js +15 -1
  34. package/dist/v2/index.d.ts +9 -3
  35. package/dist/v2/index.js +8 -3
  36. package/dist/v2/react/components/ApplePayButton.d.ts +22 -123
  37. package/dist/v2/react/components/ApplePayButton.js +247 -317
  38. package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
  39. package/dist/v2/react/components/FunnelScriptInjector.js +255 -162
  40. package/dist/v2/react/components/GooglePayButton.d.ts +2 -0
  41. package/dist/v2/react/components/GooglePayButton.js +80 -64
  42. package/dist/v2/react/components/PreviewModeIndicator.d.ts +46 -0
  43. package/dist/v2/react/components/PreviewModeIndicator.js +113 -0
  44. package/dist/v2/react/hooks/useApplePayCheckout.d.ts +16 -0
  45. package/dist/v2/react/hooks/useApplePayCheckout.js +193 -0
  46. package/dist/v2/react/hooks/useFunnel.d.ts +48 -6
  47. package/dist/v2/react/hooks/useFunnel.js +25 -5
  48. package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +10 -0
  49. package/dist/v2/react/hooks/useGoogleAutocomplete.js +48 -0
  50. package/dist/v2/react/hooks/useGooglePayCheckout.d.ts +21 -0
  51. package/dist/v2/react/hooks/useGooglePayCheckout.js +198 -0
  52. package/dist/v2/react/hooks/usePaymentPolling.d.ts +15 -3
  53. package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
  54. package/dist/v2/react/hooks/usePaymentQuery.d.ts +34 -2
  55. package/dist/v2/react/hooks/usePaymentQuery.js +731 -7
  56. package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +26 -0
  57. package/dist/v2/react/hooks/usePaymentRetrieve.js +175 -0
  58. package/dist/v2/react/hooks/usePixelTracking.d.ts +56 -0
  59. package/dist/v2/react/hooks/usePixelTracking.js +508 -0
  60. package/dist/v2/react/hooks/useStepConfig.d.ts +64 -0
  61. package/dist/v2/react/hooks/useStepConfig.js +53 -0
  62. package/dist/v2/react/index.d.ts +15 -5
  63. package/dist/v2/react/index.js +8 -2
  64. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +1 -0
  65. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +41 -13
  66. package/dist/v2/react/providers/TagadaProvider.js +24 -23
  67. package/dist/v2/standalone/external-tracker.d.ts +2 -0
  68. package/dist/v2/standalone/external-tracker.js +6 -3
  69. package/package.json +112 -112
  70. package/dist/v2/react/hooks/useApplePay.d.ts +0 -16
  71. package/dist/v2/react/hooks/useApplePay.js +0 -247
@@ -334,10 +334,22 @@ export class TagadaClient {
334
334
  * Normal token initialization flow (no cross-domain handoff)
335
335
  */
336
336
  async fallbackToNormalFlow() {
337
- // Check for existing token in URL or storage
338
- const existingToken = getClientToken();
337
+ // 🔒 CRITICAL: Read URL token FIRST before any async operations
338
+ // This prevents race conditions where anonymous token creation overwrites URL token
339
339
  const urlParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
340
340
  const queryToken = urlParams.get('token');
341
+ // If URL has token, set it IMMEDIATELY to prevent any race condition
342
+ if (queryToken) {
343
+ if (this.state.debugMode) {
344
+ console.log(`[TagadaClient ${this.instanceId}] 🔒 URL token detected, setting immediately to prevent race condition`);
345
+ }
346
+ // Set token on API client immediately
347
+ this.apiClient.updateToken(queryToken);
348
+ // Also persist to localStorage
349
+ setClientToken(queryToken);
350
+ }
351
+ // Now check storage (which should have the URL token if we just set it)
352
+ const existingToken = getClientToken();
341
353
  console.log(`[TagadaClient ${this.instanceId}] Initializing token (normal flow)...`, {
342
354
  hasExistingToken: !!existingToken,
343
355
  hasQueryToken: !!queryToken,
@@ -425,6 +437,26 @@ export class TagadaClient {
425
437
  * Create anonymous token
426
438
  */
427
439
  async createAnonymousToken(storeId) {
440
+ // 🔒 CRITICAL: Never create anonymous token if URL has a token
441
+ // This prevents race conditions during forceReset
442
+ if (typeof window !== 'undefined') {
443
+ const urlParams = new URLSearchParams(window.location.search);
444
+ const urlToken = urlParams.get('token');
445
+ if (urlToken) {
446
+ if (this.state.debugMode) {
447
+ console.log(`[TagadaClient ${this.instanceId}] 🔒 URL has token, skipping anonymous token creation`);
448
+ }
449
+ // Use the URL token instead
450
+ this.setToken(urlToken);
451
+ setClientToken(urlToken);
452
+ const decodedSession = decodeJWTClient(urlToken);
453
+ if (decodedSession) {
454
+ this.updateState({ session: decodedSession });
455
+ await this.initializeSession(decodedSession);
456
+ }
457
+ return;
458
+ }
459
+ }
428
460
  // Prevent concurrent anonymous token creation
429
461
  if (this.isInitializingSession) {
430
462
  if (this.state.debugMode) {
@@ -160,6 +160,7 @@ export function getEndpointUrl(config, category, endpoint) {
160
160
  * 4. Default fallback - Production (safest)
161
161
  */
162
162
  export function detectEnvironment() {
163
+ console.log('[SDK] detectEnvironment() called');
163
164
  // Check if we're in browser
164
165
  if (typeof window === 'undefined') {
165
166
  return 'local'; // SSR fallback
@@ -185,15 +186,21 @@ export function detectEnvironment() {
185
186
  }
186
187
  const hostname = window.location.hostname;
187
188
  const href = window.location.href;
189
+ console.log(`[SDK] detectEnvironment() - hostname: "${hostname}"`);
188
190
  // 1. Check for LOCAL environment first (highest priority for dev)
189
- // Local: localhost, local IPs, or local domains
191
+ // Local: localhost, local IPs, local domains, or ngrok tunnels (used for local dev)
190
192
  if (hostname === 'localhost' ||
191
193
  hostname.startsWith('127.') ||
192
194
  hostname.startsWith('192.168.') ||
193
195
  hostname.startsWith('10.') ||
194
196
  hostname.includes('.local') ||
195
197
  hostname === '' ||
196
- hostname === '0.0.0.0') {
198
+ hostname === '0.0.0.0' ||
199
+ hostname.includes('ngrok-free.dev') ||
200
+ hostname.includes('ngrok-free.app') ||
201
+ hostname.includes('ngrok.io') ||
202
+ hostname.includes('ngrok.app')) {
203
+ console.log('[SDK] detectEnvironment() - returning LOCAL');
197
204
  // For local development, allow override via window.__TAGADA_ENV__ (injected by dev server)
198
205
  if (typeof window !== 'undefined' && window?.__TAGADA_ENV__?.TAGADA_ENVIRONMENT) {
199
206
  const override = window.__TAGADA_ENV__.TAGADA_ENVIRONMENT.toLowerCase();
@@ -1,6 +1,177 @@
1
1
  import { ApiClient } from './resources/apiClient';
2
- import { SimpleFunnelContext, FunnelAction, FunnelNavigationResult } from './resources/funnel';
2
+ import { FunnelAction, FunnelNavigationResult, SimpleFunnelContext } from './resources/funnel';
3
3
  import { PluginConfig } from './utils/pluginConfig';
4
+ /**
5
+ * Tracking providers supported by the SDK.
6
+ * Mirrors `TrackingType` in `apps/crm-app/.../types/tracking.ts`.
7
+ */
8
+ export declare enum TrackingProvider {
9
+ FACEBOOK = "facebook",
10
+ TIKTOK = "tiktok",
11
+ SNAPCHAT = "snapchat",
12
+ META_CONVERSION = "meta_conversion",
13
+ GTM = "gtm"
14
+ }
15
+ /**
16
+ * Base tracking config (all providers)
17
+ */
18
+ export interface BaseTrackingConfig {
19
+ enabled: boolean;
20
+ }
21
+ /**
22
+ * Pixel-based providers (Facebook, TikTok)
23
+ */
24
+ export interface PixelTrackingConfig extends BaseTrackingConfig {
25
+ pixelId: string;
26
+ events: {
27
+ PageView: boolean;
28
+ InitiateCheckout: boolean;
29
+ Purchase: boolean;
30
+ };
31
+ }
32
+ /**
33
+ * Snapchat pixel config (extends base pixel events)
34
+ */
35
+ export interface SnapchatTrackingConfig extends BaseTrackingConfig {
36
+ pixelId: string;
37
+ events: {
38
+ PageView: boolean;
39
+ InitiateCheckout: boolean;
40
+ Purchase: boolean;
41
+ AddToCart: boolean;
42
+ ViewContent: boolean;
43
+ Search: boolean;
44
+ AddToWishlist: boolean;
45
+ CompleteRegistration: boolean;
46
+ };
47
+ }
48
+ /**
49
+ * Meta Conversion API config
50
+ */
51
+ export interface MetaConversionTrackingConfig extends BaseTrackingConfig {
52
+ accessToken: string;
53
+ pixelId: string;
54
+ publishPurchaseIfNewCustomerOnly: boolean;
55
+ }
56
+ /**
57
+ * Google Tag Manager config
58
+ */
59
+ export interface GTMTrackingConfig extends BaseTrackingConfig {
60
+ containerId: string;
61
+ events: {
62
+ PageView: boolean;
63
+ InitiateCheckout: boolean;
64
+ Purchase: boolean;
65
+ AddToCart: boolean;
66
+ ViewContent: boolean;
67
+ };
68
+ }
69
+ /**
70
+ * Union of all tracking configs.
71
+ * This mirrors `TrackingFormValues` from the CRM.
72
+ */
73
+ export type PixelConfig = PixelTrackingConfig | SnapchatTrackingConfig | MetaConversionTrackingConfig | GTMTrackingConfig;
74
+ /**
75
+ * Runtime step configuration injected from the CRM
76
+ * Contains payment flows, static resources, scripts, and tracking configs
77
+ */
78
+ export interface RuntimeStepConfig {
79
+ payment?: {
80
+ paymentFlowId?: string;
81
+ };
82
+ staticResources?: Record<string, string>;
83
+ scripts?: Array<{
84
+ name: string;
85
+ enabled: boolean;
86
+ content: string;
87
+ position?: 'head-start' | 'head-end' | 'body-start' | 'body-end';
88
+ }>;
89
+ pixels?: {
90
+ [TrackingProvider.FACEBOOK]?: PixelTrackingConfig[];
91
+ [TrackingProvider.TIKTOK]?: PixelTrackingConfig[];
92
+ [TrackingProvider.SNAPCHAT]?: SnapchatTrackingConfig[];
93
+ [TrackingProvider.META_CONVERSION]?: MetaConversionTrackingConfig[];
94
+ [TrackingProvider.GTM]?: GTMTrackingConfig[];
95
+ };
96
+ }
97
+ /**
98
+ * Local funnel configuration for development
99
+ * Loaded from /config/funnel.local.json
100
+ */
101
+ export interface LocalFunnelConfig {
102
+ /** Funnel ID to use in local dev */
103
+ funnelId?: string;
104
+ /** Step ID to simulate */
105
+ stepId?: string;
106
+ /** Static resources (offer ID, product ID, etc.) */
107
+ staticResources?: Record<string, string>;
108
+ /** Payment flow ID override */
109
+ paymentFlowId?: string;
110
+ /** Custom scripts for local testing */
111
+ scripts?: RuntimeStepConfig['scripts'];
112
+ /** Pixel tracking config */
113
+ pixels?: RuntimeStepConfig['pixels'];
114
+ }
115
+ /**
116
+ * Load local funnel config from /config/funnel.local.json (for local dev only)
117
+ * This replaces the old resources.static.json with a more complete structure
118
+ *
119
+ * Example funnel.local.json:
120
+ * {
121
+ * "funnelId": "funnelv2_xxx",
122
+ * "stepId": "step_checkout",
123
+ * "staticResources": {
124
+ * "offer": "offer_xxx",
125
+ * "product": "product_xxx"
126
+ * },
127
+ * "paymentFlowId": "flow_xxx"
128
+ * }
129
+ */
130
+ export declare function loadLocalFunnelConfig(): Promise<LocalFunnelConfig | null>;
131
+ /**
132
+ * Get the cached local funnel config (sync access after loadLocalFunnelConfig)
133
+ */
134
+ export declare function getLocalFunnelConfig(): LocalFunnelConfig | null;
135
+ /**
136
+ * Get the runtime step configuration
137
+ * Contains payment flow, static resources, scripts, and pixel tracking
138
+ *
139
+ * Priority:
140
+ * 1. Local funnel config (local dev only - /config/funnel.local.json) - HIGHEST in local dev
141
+ * 2. Window variable (production - HTML injection)
142
+ * 3. Meta tag (production - HTML injection fallback)
143
+ *
144
+ * This allows local developers to override injected config for testing.
145
+ *
146
+ * Returns undefined if not available
147
+ */
148
+ export declare function getAssignedStepConfig(): RuntimeStepConfig | undefined;
149
+ /**
150
+ * Get the assigned payment flow ID from step config or legacy injection
151
+ * Returns undefined if not available
152
+ */
153
+ export declare function getAssignedPaymentFlowId(): string | undefined;
154
+ /**
155
+ * Get the assigned static resources from step config
156
+ * Returns undefined if not available
157
+ */
158
+ export declare function getAssignedStaticResources(): Record<string, string> | undefined;
159
+ /**
160
+ * Get the assigned scripts from step config
161
+ * Returns only enabled scripts, filtered by position if specified
162
+ */
163
+ export declare function getAssignedScripts(position?: 'head-start' | 'head-end' | 'body-start' | 'body-end'): RuntimeStepConfig['scripts'];
164
+ /**
165
+ * Get assigned pixel tracking configuration (normalized to arrays)
166
+ * Always returns arrays of PixelConfig for consistent consumption.
167
+ */
168
+ export declare function getAssignedPixels(): {
169
+ [TrackingProvider.FACEBOOK]?: PixelTrackingConfig[];
170
+ [TrackingProvider.TIKTOK]?: PixelTrackingConfig[];
171
+ [TrackingProvider.SNAPCHAT]?: SnapchatTrackingConfig[];
172
+ [TrackingProvider.META_CONVERSION]?: MetaConversionTrackingConfig[];
173
+ [TrackingProvider.GTM]?: GTMTrackingConfig[];
174
+ } | undefined;
4
175
  export interface FunnelClientConfig {
5
176
  apiClient: ApiClient;
6
177
  debugMode?: boolean;
@@ -86,16 +257,23 @@ export declare class FunnelClient {
86
257
  * @param options.fireAndForget - If true, queues navigation to QStash and returns immediately without waiting for result
87
258
  * @param options.customerTags - Customer tags to set (merged with existing customer tags)
88
259
  * @param options.deviceId - Device ID for geo/device tag enrichment (optional, rarely needed)
260
+ * @param options.autoRedirect - Override global autoRedirect setting for this specific call (default: use config)
89
261
  */
90
262
  navigate(event: FunnelAction, options?: {
91
263
  fireAndForget?: boolean;
92
264
  customerTags?: string[];
93
265
  deviceId?: string;
266
+ autoRedirect?: boolean;
267
+ waitForSession?: boolean;
94
268
  }): Promise<FunnelNavigationResult>;
95
269
  /**
96
270
  * Go to a specific step (direct navigation)
271
+ * @param stepId - Target step ID
272
+ * @param options - Navigation options (autoRedirect, etc.)
97
273
  */
98
- goToStep(stepId: string): Promise<FunnelNavigationResult>;
274
+ goToStep(stepId: string, options?: {
275
+ autoRedirect?: boolean;
276
+ }): Promise<FunnelNavigationResult>;
99
277
  /**
100
278
  * Refresh session data
101
279
  */
@@ -1,8 +1,21 @@
1
- import { FunnelResource, FunnelActionType } from './resources/funnel';
2
- import { EventDispatcher } from './utils/eventDispatcher';
3
- import { getFunnelSessionCookie, setFunnelSessionCookie } from './utils/sessionStorage';
4
1
  import { detectEnvironment } from './config/environment';
2
+ import { FunnelActionType, FunnelResource } from './resources/funnel';
3
+ import { EventDispatcher } from './utils/eventDispatcher';
5
4
  import { getSDKParams } from './utils/previewMode';
5
+ import { injectPreviewModeIndicator } from './utils/previewModeIndicator';
6
+ import { getFunnelSessionCookie, setFunnelSessionCookie } from './utils/sessionStorage';
7
+ /**
8
+ * Tracking providers supported by the SDK.
9
+ * Mirrors `TrackingType` in `apps/crm-app/.../types/tracking.ts`.
10
+ */
11
+ export var TrackingProvider;
12
+ (function (TrackingProvider) {
13
+ TrackingProvider["FACEBOOK"] = "facebook";
14
+ TrackingProvider["TIKTOK"] = "tiktok";
15
+ TrackingProvider["SNAPCHAT"] = "snapchat";
16
+ TrackingProvider["META_CONVERSION"] = "meta_conversion";
17
+ TrackingProvider["GTM"] = "gtm";
18
+ })(TrackingProvider || (TrackingProvider = {}));
6
19
  /**
7
20
  * Get the funnel ID from the injected HTML
8
21
  * Returns undefined if not available
@@ -57,6 +70,241 @@ function getAssignedFunnelStep() {
57
70
  }
58
71
  return undefined;
59
72
  }
73
+ /**
74
+ * Parse step config from a string value (handles both JSON and URL-encoded formats)
75
+ * Robust parsing with multiple fallback strategies
76
+ */
77
+ function parseStepConfig(value) {
78
+ if (!value || typeof value !== 'string')
79
+ return undefined;
80
+ const trimmed = value.trim();
81
+ if (!trimmed)
82
+ return undefined;
83
+ // Try parsing strategies in order of likelihood
84
+ const strategies = [
85
+ // Strategy 1: Direct JSON parse (for properly escaped window variable)
86
+ () => JSON.parse(trimmed),
87
+ // Strategy 2: URL decode then JSON parse (for meta tag)
88
+ () => JSON.parse(decodeURIComponent(trimmed)),
89
+ // Strategy 3: Double URL decode (edge case: double-encoded)
90
+ () => JSON.parse(decodeURIComponent(decodeURIComponent(trimmed))),
91
+ ];
92
+ for (const strategy of strategies) {
93
+ try {
94
+ const result = strategy();
95
+ if (result && typeof result === 'object') {
96
+ return result;
97
+ }
98
+ }
99
+ catch {
100
+ // Try next strategy
101
+ }
102
+ }
103
+ // All strategies failed
104
+ if (typeof console !== 'undefined') {
105
+ console.warn('[SDK] Failed to parse stepConfig:', trimmed.substring(0, 100));
106
+ }
107
+ return undefined;
108
+ }
109
+ // Cache for local funnel config (loaded once)
110
+ let localFunnelConfigCache = undefined;
111
+ let localFunnelConfigLoading = false;
112
+ /**
113
+ * Check if we're in true local development (not CDN deployment)
114
+ */
115
+ function isLocalDevelopment() {
116
+ if (typeof window === 'undefined')
117
+ return false;
118
+ const hostname = window.location.hostname;
119
+ // True local: localhost without CDN subdomain pattern
120
+ return hostname === 'localhost' ||
121
+ hostname === '127.0.0.1' ||
122
+ (hostname.endsWith('.localhost') && !hostname.includes('.cdn.'));
123
+ }
124
+ /**
125
+ * Load local funnel config from /config/funnel.local.json (for local dev only)
126
+ * This replaces the old resources.static.json with a more complete structure
127
+ *
128
+ * Example funnel.local.json:
129
+ * {
130
+ * "funnelId": "funnelv2_xxx",
131
+ * "stepId": "step_checkout",
132
+ * "staticResources": {
133
+ * "offer": "offer_xxx",
134
+ * "product": "product_xxx"
135
+ * },
136
+ * "paymentFlowId": "flow_xxx"
137
+ * }
138
+ */
139
+ export async function loadLocalFunnelConfig() {
140
+ // Only in true local development
141
+ if (!isLocalDevelopment())
142
+ return null;
143
+ // Return cached value if already loaded
144
+ if (localFunnelConfigCache !== undefined) {
145
+ return localFunnelConfigCache;
146
+ }
147
+ // Prevent concurrent loads
148
+ if (localFunnelConfigLoading) {
149
+ // Wait for existing load
150
+ await new Promise(resolve => setTimeout(resolve, 100));
151
+ return localFunnelConfigCache ?? null;
152
+ }
153
+ localFunnelConfigLoading = true;
154
+ try {
155
+ console.log('🛠️ [SDK] Loading local funnel config from /config/funnel.local.json...');
156
+ const response = await fetch('/config/funnel.local.json');
157
+ if (!response.ok) {
158
+ console.log('🛠️ [SDK] funnel.local.json not found (this is fine in production)');
159
+ localFunnelConfigCache = null;
160
+ return null;
161
+ }
162
+ const config = await response.json();
163
+ console.log('🛠️ [SDK] ✅ Loaded local funnel config:', config);
164
+ localFunnelConfigCache = config;
165
+ return config;
166
+ }
167
+ catch (error) {
168
+ console.log('🛠️ [SDK] funnel.local.json not available:', error);
169
+ localFunnelConfigCache = null;
170
+ return null;
171
+ }
172
+ finally {
173
+ localFunnelConfigLoading = false;
174
+ }
175
+ }
176
+ /**
177
+ * Get the cached local funnel config (sync access after loadLocalFunnelConfig)
178
+ */
179
+ export function getLocalFunnelConfig() {
180
+ return localFunnelConfigCache ?? null;
181
+ }
182
+ /**
183
+ * Convert LocalFunnelConfig to RuntimeStepConfig
184
+ */
185
+ function localConfigToStepConfig(local) {
186
+ return {
187
+ payment: local.paymentFlowId ? { paymentFlowId: local.paymentFlowId } : undefined,
188
+ staticResources: local.staticResources,
189
+ scripts: local.scripts,
190
+ pixels: local.pixels,
191
+ };
192
+ }
193
+ /**
194
+ * Get the runtime step configuration
195
+ * Contains payment flow, static resources, scripts, and pixel tracking
196
+ *
197
+ * Priority:
198
+ * 1. Local funnel config (local dev only - /config/funnel.local.json) - HIGHEST in local dev
199
+ * 2. Window variable (production - HTML injection)
200
+ * 3. Meta tag (production - HTML injection fallback)
201
+ *
202
+ * This allows local developers to override injected config for testing.
203
+ *
204
+ * Returns undefined if not available
205
+ */
206
+ export function getAssignedStepConfig() {
207
+ if (typeof window === 'undefined')
208
+ return undefined;
209
+ // Method 1: Local dev override (HIGHEST PRIORITY in local dev)
210
+ // Allows developers to test different configurations without redeploying
211
+ const localConfig = getLocalFunnelConfig();
212
+ if (localConfig) {
213
+ console.log('🛠️ [SDK] Using local funnel.local.json (overrides injected)');
214
+ return localConfigToStepConfig(localConfig);
215
+ }
216
+ // Method 2: Window variable (production - HTML injection)
217
+ const windowValue = window.__TGD_STEP_CONFIG__;
218
+ if (windowValue) {
219
+ const parsed = parseStepConfig(windowValue);
220
+ if (parsed)
221
+ return parsed;
222
+ }
223
+ // Method 3: Meta tag fallback (URL-encoded)
224
+ if (typeof document !== 'undefined') {
225
+ const meta = document.querySelector('meta[name="x-step-config"]');
226
+ const content = meta?.getAttribute('content');
227
+ if (content) {
228
+ const parsed = parseStepConfig(content);
229
+ if (parsed)
230
+ return parsed;
231
+ }
232
+ }
233
+ return undefined;
234
+ }
235
+ /**
236
+ * Get the assigned payment flow ID from step config or legacy injection
237
+ * Returns undefined if not available
238
+ */
239
+ export function getAssignedPaymentFlowId() {
240
+ // Method 1: New stepConfig (preferred)
241
+ const stepConfig = getAssignedStepConfig();
242
+ if (stepConfig?.payment?.paymentFlowId) {
243
+ return stepConfig.payment.paymentFlowId;
244
+ }
245
+ // Method 2: Legacy direct injection (backward compatibility)
246
+ if (typeof window !== 'undefined') {
247
+ // Legacy window variable
248
+ if (window.__TGD_PAYMENT_FLOW_ID__) {
249
+ return window.__TGD_PAYMENT_FLOW_ID__;
250
+ }
251
+ // Legacy meta tag
252
+ if (typeof document !== 'undefined') {
253
+ const meta = document.querySelector('meta[name="x-payment-flow-id"]');
254
+ return meta?.getAttribute('content') || undefined;
255
+ }
256
+ }
257
+ return undefined;
258
+ }
259
+ /**
260
+ * Get the assigned static resources from step config
261
+ * Returns undefined if not available
262
+ */
263
+ export function getAssignedStaticResources() {
264
+ const stepConfig = getAssignedStepConfig();
265
+ return stepConfig?.staticResources;
266
+ }
267
+ /**
268
+ * Get the assigned scripts from step config
269
+ * Returns only enabled scripts, filtered by position if specified
270
+ */
271
+ export function getAssignedScripts(position) {
272
+ const stepConfig = getAssignedStepConfig();
273
+ if (!stepConfig?.scripts)
274
+ return undefined;
275
+ // Filter enabled scripts
276
+ let scripts = stepConfig.scripts.filter(s => s.enabled);
277
+ // Filter by position if specified
278
+ if (position) {
279
+ scripts = scripts.filter(s => s.position === position || (!s.position && position === 'head-end'));
280
+ }
281
+ return scripts.length > 0 ? scripts : undefined;
282
+ }
283
+ /**
284
+ * Get assigned pixel tracking configuration (normalized to arrays)
285
+ * Always returns arrays of PixelConfig for consistent consumption.
286
+ */
287
+ export function getAssignedPixels() {
288
+ const stepConfig = getAssignedStepConfig();
289
+ const rawPixels = stepConfig?.pixels;
290
+ if (!rawPixels || typeof rawPixels !== 'object')
291
+ return undefined;
292
+ const normalized = {};
293
+ for (const [key, value] of Object.entries(rawPixels)) {
294
+ if (!value)
295
+ continue;
296
+ if (Array.isArray(value)) {
297
+ // Already an array
298
+ normalized[key] = value;
299
+ }
300
+ else if (typeof value === 'object') {
301
+ // Single object - wrap in array
302
+ normalized[key] = [value];
303
+ }
304
+ // Skip invalid entries
305
+ }
306
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
307
+ }
60
308
  export class FunnelClient {
61
309
  constructor(config) {
62
310
  this.eventDispatcher = new EventDispatcher();
@@ -152,6 +400,9 @@ export class FunnelClient {
152
400
  // Priority: config override > injected > URL/prop
153
401
  const finalFunnelId = this.config.funnelId || injectedFunnelId || effectiveFunnelId;
154
402
  const finalStepId = this.config.stepId || injectedStepId;
403
+ // 🎯 Determine funnelEnv from URL params
404
+ const urlParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null;
405
+ const funnelEnv = urlParams?.get('funnelEnv');
155
406
  if (this.config.debugMode) {
156
407
  console.log('🚀 [FunnelClient] Auto-initializing...', {
157
408
  existingSessionId,
@@ -160,6 +411,9 @@ export class FunnelClient {
160
411
  funnelStepId: finalStepId, // 🎯 Log step ID for debugging
161
412
  draft: sdkParams.draft, // 🎯 Log draft mode
162
413
  funnelTracking: sdkParams.funnelTracking, // 🎯 Log tracking flag
414
+ funnelEnv, // 🎯 Log funnel environment
415
+ tagadaClientEnv: sdkParams.tagadaClientEnv, // 🎯 Log client environment
416
+ tagadaClientBaseUrl: sdkParams.tagadaClientBaseUrl, // 🎯 Log custom API URL
163
417
  source: {
164
418
  funnelId: this.config.funnelId ? 'config' : injectedFunnelId ? 'injected' : effectiveFunnelId ? 'url/prop' : 'none',
165
419
  stepId: this.config.stepId ? 'config' : injectedStepId ? 'injected' : 'none',
@@ -181,10 +435,17 @@ export class FunnelClient {
181
435
  funnelStepId: finalStepId, // 🎯 Pass step ID to backend (with config override)
182
436
  draft: sdkParams.draft, // 🎯 Pass draft mode explicitly (more robust than URL parsing)
183
437
  funnelTracking: sdkParams.funnelTracking, // 🎯 Pass funnel tracking flag explicitly
438
+ funnelEnv: funnelEnv || undefined, // 🎯 Pass funnel environment (staging/production)
439
+ tagadaClientEnv: sdkParams.tagadaClientEnv, // 🎯 Pass client environment override
440
+ tagadaClientBaseUrl: sdkParams.tagadaClientBaseUrl, // 🎯 Pass custom API base URL
441
+ currency: sdkParams.currency, // 🌍 Pass display currency override
442
+ locale: sdkParams.locale, // 🌍 Pass display locale override
184
443
  });
185
444
  if (response.success && response.context) {
186
445
  const enriched = this.enrichContext(response.context);
187
446
  this.handleSessionSuccess(enriched);
447
+ // 🔍 Auto-inject preview mode indicator if in preview/dev mode
448
+ injectPreviewModeIndicator();
188
449
  return enriched;
189
450
  }
190
451
  else {
@@ -234,6 +495,8 @@ export class FunnelClient {
234
495
  if (response.success && response.context) {
235
496
  const enriched = this.enrichContext(response.context);
236
497
  this.handleSessionSuccess(enriched);
498
+ // 🔍 Auto-inject preview mode indicator if in preview/dev mode
499
+ injectPreviewModeIndicator();
237
500
  return enriched;
238
501
  }
239
502
  else {
@@ -253,8 +516,23 @@ export class FunnelClient {
253
516
  * @param options.fireAndForget - If true, queues navigation to QStash and returns immediately without waiting for result
254
517
  * @param options.customerTags - Customer tags to set (merged with existing customer tags)
255
518
  * @param options.deviceId - Device ID for geo/device tag enrichment (optional, rarely needed)
519
+ * @param options.autoRedirect - Override global autoRedirect setting for this specific call (default: use config)
256
520
  */
257
521
  async navigate(event, options) {
522
+ // Wait for session if requested
523
+ if (options?.waitForSession && !this.state.context?.sessionId) {
524
+ if (this.config.debugMode) {
525
+ console.log('⏳ [FunnelClient] Waiting for session before navigation...');
526
+ }
527
+ const maxWaitTime = 5000;
528
+ const startTime = Date.now();
529
+ while (!this.state.context?.sessionId && (Date.now() - startTime < maxWaitTime)) {
530
+ await new Promise(resolve => setTimeout(resolve, 100));
531
+ }
532
+ if (this.state.context?.sessionId && this.config.debugMode) {
533
+ console.log('✅ [FunnelClient] Session ready, proceeding with navigation');
534
+ }
535
+ }
258
536
  if (!this.state.context?.sessionId)
259
537
  throw new Error('No active session');
260
538
  this.updateState({ isNavigating: true, isLoading: true });
@@ -324,7 +602,10 @@ export class FunnelClient {
324
602
  return result;
325
603
  }
326
604
  // Normal navigation: handle redirect
327
- const shouldAutoRedirect = this.config.autoRedirect !== false; // Default to true
605
+ // Per-call option takes precedence over global config
606
+ const shouldAutoRedirect = options?.autoRedirect !== undefined
607
+ ? options.autoRedirect
608
+ : this.config.autoRedirect !== false; // Default to true
328
609
  // Skip refreshSession if auto-redirecting (next page will initialize with fresh state)
329
610
  // Only refresh if staying on same page (autoRedirect: false)
330
611
  if (!shouldAutoRedirect) {
@@ -351,12 +632,14 @@ export class FunnelClient {
351
632
  }
352
633
  /**
353
634
  * Go to a specific step (direct navigation)
635
+ * @param stepId - Target step ID
636
+ * @param options - Navigation options (autoRedirect, etc.)
354
637
  */
355
- async goToStep(stepId) {
638
+ async goToStep(stepId, options) {
356
639
  return this.navigate({
357
640
  type: FunnelActionType.DIRECT_NAVIGATION,
358
641
  data: { targetStepId: stepId },
359
- });
642
+ }, options);
360
643
  }
361
644
  /**
362
645
  * Refresh session data
@@ -13,7 +13,7 @@ export class ApiClient {
13
13
  this.MAX_REQUESTS = 30; // Max 30 requests per endpoint in window
14
14
  this.axios = axios.create({
15
15
  baseURL: config.baseURL,
16
- timeout: config.timeout || 30000,
16
+ timeout: config.timeout || 60000, // 60 seconds for payment operations
17
17
  headers: {
18
18
  'Content-Type': 'application/json',
19
19
  ...config.headers,