@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.
- package/README.md +1129 -1129
- package/build-cdn.js +220 -113
- package/dist/external-tracker.js +1225 -558
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/hooks/useApplePay.js +25 -36
- package/dist/react/hooks/usePaymentPolling.d.ts +9 -3
- package/dist/react/providers/TagadaProvider.js +5 -5
- package/dist/react/utils/money.d.ts +4 -3
- package/dist/react/utils/money.js +39 -6
- package/dist/react/utils/trackingUtils.js +1 -0
- package/dist/tagada-sdk.js +10142 -0
- package/dist/tagada-sdk.min.js +43 -0
- package/dist/tagada-sdk.min.js.map +7 -0
- package/dist/v2/core/client.js +34 -2
- package/dist/v2/core/config/environment.js +9 -2
- package/dist/v2/core/funnelClient.d.ts +180 -2
- package/dist/v2/core/funnelClient.js +289 -6
- package/dist/v2/core/resources/apiClient.js +1 -1
- package/dist/v2/core/resources/checkout.d.ts +68 -0
- package/dist/v2/core/resources/funnel.d.ts +25 -0
- package/dist/v2/core/resources/payments.d.ts +70 -3
- package/dist/v2/core/resources/payments.js +72 -7
- package/dist/v2/core/utils/index.d.ts +1 -0
- package/dist/v2/core/utils/index.js +2 -0
- package/dist/v2/core/utils/pluginConfig.d.ts +8 -0
- package/dist/v2/core/utils/pluginConfig.js +68 -5
- package/dist/v2/core/utils/previewMode.d.ts +7 -0
- package/dist/v2/core/utils/previewMode.js +72 -14
- package/dist/v2/core/utils/previewModeIndicator.d.ts +19 -0
- package/dist/v2/core/utils/previewModeIndicator.js +414 -0
- package/dist/v2/core/utils/tokenStorage.d.ts +4 -0
- package/dist/v2/core/utils/tokenStorage.js +15 -1
- package/dist/v2/index.d.ts +9 -3
- package/dist/v2/index.js +8 -3
- package/dist/v2/react/components/ApplePayButton.d.ts +22 -123
- package/dist/v2/react/components/ApplePayButton.js +247 -317
- package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
- package/dist/v2/react/components/FunnelScriptInjector.js +255 -162
- package/dist/v2/react/components/GooglePayButton.d.ts +2 -0
- package/dist/v2/react/components/GooglePayButton.js +80 -64
- package/dist/v2/react/components/PreviewModeIndicator.d.ts +46 -0
- package/dist/v2/react/components/PreviewModeIndicator.js +113 -0
- package/dist/v2/react/hooks/useApplePayCheckout.d.ts +16 -0
- package/dist/v2/react/hooks/useApplePayCheckout.js +193 -0
- package/dist/v2/react/hooks/useFunnel.d.ts +48 -6
- package/dist/v2/react/hooks/useFunnel.js +25 -5
- package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +10 -0
- package/dist/v2/react/hooks/useGoogleAutocomplete.js +48 -0
- package/dist/v2/react/hooks/useGooglePayCheckout.d.ts +21 -0
- package/dist/v2/react/hooks/useGooglePayCheckout.js +198 -0
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +15 -3
- package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
- package/dist/v2/react/hooks/usePaymentQuery.d.ts +34 -2
- package/dist/v2/react/hooks/usePaymentQuery.js +731 -7
- package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +26 -0
- package/dist/v2/react/hooks/usePaymentRetrieve.js +175 -0
- package/dist/v2/react/hooks/usePixelTracking.d.ts +56 -0
- package/dist/v2/react/hooks/usePixelTracking.js +508 -0
- package/dist/v2/react/hooks/useStepConfig.d.ts +64 -0
- package/dist/v2/react/hooks/useStepConfig.js +53 -0
- package/dist/v2/react/index.d.ts +15 -5
- package/dist/v2/react/index.js +8 -2
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +1 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +41 -13
- package/dist/v2/react/providers/TagadaProvider.js +24 -23
- package/dist/v2/standalone/external-tracker.d.ts +2 -0
- package/dist/v2/standalone/external-tracker.js +6 -3
- package/package.json +112 -112
- package/dist/v2/react/hooks/useApplePay.d.ts +0 -16
- package/dist/v2/react/hooks/useApplePay.js +0 -247
package/dist/v2/core/client.js
CHANGED
|
@@ -334,10 +334,22 @@ export class TagadaClient {
|
|
|
334
334
|
* Normal token initialization flow (no cross-domain handoff)
|
|
335
335
|
*/
|
|
336
336
|
async fallbackToNormalFlow() {
|
|
337
|
-
//
|
|
338
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
16
|
+
timeout: config.timeout || 60000, // 60 seconds for payment operations
|
|
17
17
|
headers: {
|
|
18
18
|
'Content-Type': 'application/json',
|
|
19
19
|
...config.headers,
|