@tagadapay/plugin-sdk 3.0.12 → 3.0.14
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/dist/external-tracker.js +3593 -111
- package/dist/external-tracker.min.js +25 -2
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/types.d.ts +2 -0
- package/dist/v2/core/client.d.ts +4 -0
- package/dist/v2/core/client.js +110 -26
- package/dist/v2/core/config/environment.js +6 -0
- package/dist/v2/core/funnelClient.d.ts +18 -1
- package/dist/v2/core/funnelClient.js +90 -17
- package/dist/v2/core/resources/checkout.d.ts +44 -1
- package/dist/v2/core/resources/checkout.js +48 -1
- package/dist/v2/core/resources/funnel.d.ts +44 -4
- package/dist/v2/core/resources/offers.d.ts +26 -0
- package/dist/v2/core/resources/offers.js +37 -0
- package/dist/v2/core/types.d.ts +3 -1
- package/dist/v2/core/utils/authHandoff.d.ts +60 -0
- package/dist/v2/core/utils/authHandoff.js +154 -0
- package/dist/v2/core/utils/deviceInfo.d.ts +20 -3
- package/dist/v2/core/utils/deviceInfo.js +62 -94
- package/dist/v2/core/utils/previewMode.d.ts +4 -0
- package/dist/v2/core/utils/previewMode.js +4 -0
- package/dist/v2/react/hooks/useCheckoutQuery.d.ts +0 -1
- package/dist/v2/react/hooks/useCheckoutQuery.js +12 -4
- package/dist/v2/react/hooks/useFunnelLegacy.js +39 -11
- package/dist/v2/react/hooks/usePreviewOffer.d.ts +3 -3
- package/dist/v2/react/hooks/usePreviewOffer.js +20 -15
- package/dist/v2/react/hooks/useTranslation.js +12 -4
- package/dist/v2/standalone/index.d.ts +2 -1
- package/dist/v2/standalone/index.js +2 -1
- package/package.json +3 -1
package/dist/react/types.d.ts
CHANGED
package/dist/v2/core/client.d.ts
CHANGED
|
@@ -101,6 +101,10 @@ export declare class TagadaClient {
|
|
|
101
101
|
* Initialize token and session
|
|
102
102
|
*/
|
|
103
103
|
private initializeToken;
|
|
104
|
+
/**
|
|
105
|
+
* Normal token initialization flow (no cross-domain handoff)
|
|
106
|
+
*/
|
|
107
|
+
private fallbackToNormalFlow;
|
|
104
108
|
/**
|
|
105
109
|
* Set token and resolve waiting requests
|
|
106
110
|
*/
|
package/dist/v2/core/client.js
CHANGED
|
@@ -8,6 +8,7 @@ import { decodeJWTClient, isTokenExpired } from './utils/jwtDecoder';
|
|
|
8
8
|
import { loadPluginConfig } from './utils/pluginConfig';
|
|
9
9
|
import { handlePreviewMode, isDraftMode, setDraftMode } from './utils/previewMode';
|
|
10
10
|
import { getClientToken, setClientToken } from './utils/tokenStorage';
|
|
11
|
+
import { shouldResolveAuthCode, resolveAuthHandoff } from './utils/authHandoff';
|
|
11
12
|
export class TagadaClient {
|
|
12
13
|
constructor(config = {}) {
|
|
13
14
|
/**
|
|
@@ -25,9 +26,13 @@ export class TagadaClient {
|
|
|
25
26
|
this.config = config;
|
|
26
27
|
this.instanceId = Math.random().toString(36).substr(2, 9);
|
|
27
28
|
this.boundHandleStorageChange = this.handleStorageChange.bind(this);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
console.log(`[TagadaClient ${this.instanceId}] Initializing...`);
|
|
30
|
+
console.log(`[TagadaClient ${this.instanceId}] Config:`, {
|
|
31
|
+
debugMode: config.debugMode,
|
|
32
|
+
hasRawPluginConfig: !!config.rawPluginConfig,
|
|
33
|
+
rawPluginConfig: config.rawPluginConfig,
|
|
34
|
+
features: config.features,
|
|
35
|
+
});
|
|
31
36
|
// Handle preview mode FIRST - clears state if needed
|
|
32
37
|
// This ensures clean state when CRM previews pages
|
|
33
38
|
const previewModeActive = handlePreviewMode(this.config.debugMode);
|
|
@@ -77,10 +82,14 @@ export class TagadaClient {
|
|
|
77
82
|
isInitialized: false,
|
|
78
83
|
isSessionInitialized: false,
|
|
79
84
|
pluginConfig: { basePath: '/', config: {} },
|
|
80
|
-
pluginConfigLoading:
|
|
85
|
+
pluginConfigLoading: true, // Always true - loadPluginConfig will process rawPluginConfig
|
|
81
86
|
debugMode: config.debugMode ?? env !== 'production',
|
|
82
87
|
token: null,
|
|
83
88
|
};
|
|
89
|
+
console.log(`[TagadaClient ${this.instanceId}] Initial state:`, {
|
|
90
|
+
pluginConfigLoading: this.state.pluginConfigLoading,
|
|
91
|
+
hasRawPluginConfig: !!config.rawPluginConfig,
|
|
92
|
+
});
|
|
84
93
|
// Initialize API Client
|
|
85
94
|
this.apiClient = new ApiClient({
|
|
86
95
|
baseURL: envConfig.apiConfig.baseUrl,
|
|
@@ -96,6 +105,9 @@ export class TagadaClient {
|
|
|
96
105
|
pluginConfig: this.state.pluginConfig,
|
|
97
106
|
environment: this.state.environment,
|
|
98
107
|
autoRedirect: funnelConfig.autoRedirect,
|
|
108
|
+
// Pass funnelId and stepId from rawPluginConfig to enable config-based initialization
|
|
109
|
+
funnelId: config.rawPluginConfig?.funnelId,
|
|
110
|
+
stepId: config.rawPluginConfig?.stepId,
|
|
99
111
|
});
|
|
100
112
|
}
|
|
101
113
|
// Setup token waiting mechanism
|
|
@@ -206,11 +218,19 @@ export class TagadaClient {
|
|
|
206
218
|
* Load plugin configuration
|
|
207
219
|
*/
|
|
208
220
|
async initializePluginConfig() {
|
|
209
|
-
|
|
221
|
+
console.log(`[TagadaClient ${this.instanceId}] initializePluginConfig called`, {
|
|
222
|
+
pluginConfigLoading: this.state.pluginConfigLoading,
|
|
223
|
+
hasRawPluginConfig: !!this.config.rawPluginConfig,
|
|
224
|
+
});
|
|
225
|
+
if (!this.state.pluginConfigLoading) {
|
|
226
|
+
console.log(`[TagadaClient ${this.instanceId}] Plugin config already loading or loaded, skipping...`);
|
|
210
227
|
return;
|
|
228
|
+
}
|
|
211
229
|
try {
|
|
212
230
|
const configVariant = this.config.localConfig || 'default';
|
|
231
|
+
console.log(`[TagadaClient ${this.instanceId}] Loading plugin config with variant: ${configVariant}`);
|
|
213
232
|
const config = await loadPluginConfig(configVariant, this.config.rawPluginConfig);
|
|
233
|
+
console.log(`[TagadaClient ${this.instanceId}] Plugin config loaded:`, config);
|
|
214
234
|
this.updateState({
|
|
215
235
|
pluginConfig: config,
|
|
216
236
|
pluginConfigLoading: false,
|
|
@@ -222,9 +242,6 @@ export class TagadaClient {
|
|
|
222
242
|
environment: this.state.environment,
|
|
223
243
|
});
|
|
224
244
|
}
|
|
225
|
-
if (this.state.debugMode) {
|
|
226
|
-
console.log('[TagadaClient] Plugin config loaded:', config);
|
|
227
|
-
}
|
|
228
245
|
}
|
|
229
246
|
catch (error) {
|
|
230
247
|
console.error('[TagadaClient] Failed to load plugin config:', error);
|
|
@@ -238,16 +255,69 @@ export class TagadaClient {
|
|
|
238
255
|
* Initialize token and session
|
|
239
256
|
*/
|
|
240
257
|
async initializeToken() {
|
|
258
|
+
// 🔐 PRIORITY 1: Check for authCode (cross-domain handoff)
|
|
259
|
+
// This ALWAYS takes precedence over existing tokens
|
|
260
|
+
if (shouldResolveAuthCode()) {
|
|
261
|
+
const storeId = this.state.pluginConfig.storeId;
|
|
262
|
+
if (!storeId) {
|
|
263
|
+
console.error('[TagadaClient] Cannot resolve authCode: storeId not found in config');
|
|
264
|
+
return this.fallbackToNormalFlow();
|
|
265
|
+
}
|
|
266
|
+
console.log(`[TagadaClient ${this.instanceId}] 🔐 Cross-domain auth detected, resolving...`);
|
|
267
|
+
try {
|
|
268
|
+
const authCode = new URLSearchParams(window.location.search).get('authCode');
|
|
269
|
+
if (!authCode) {
|
|
270
|
+
return this.fallbackToNormalFlow();
|
|
271
|
+
}
|
|
272
|
+
// Resolve the handoff
|
|
273
|
+
const handoffData = await resolveAuthHandoff(authCode, storeId, this.state.environment.apiConfig.baseUrl, this.state.debugMode);
|
|
274
|
+
console.log(`[TagadaClient ${this.instanceId}] ✅ Auth handoff resolved:`, {
|
|
275
|
+
customerId: handoffData.customer.id,
|
|
276
|
+
role: handoffData.customer.role,
|
|
277
|
+
hasContext: Object.keys(handoffData.context).length > 0,
|
|
278
|
+
});
|
|
279
|
+
// Set the new token (already stored by resolveAuthHandoff)
|
|
280
|
+
this.setToken(handoffData.token);
|
|
281
|
+
// Decode session from token
|
|
282
|
+
const decodedSession = decodeJWTClient(handoffData.token);
|
|
283
|
+
if (decodedSession) {
|
|
284
|
+
this.updateState({ session: decodedSession });
|
|
285
|
+
await this.initializeSession(decodedSession);
|
|
286
|
+
// If context has funnelSessionId, restore it
|
|
287
|
+
if (handoffData.context?.funnelSessionId && this.funnel) {
|
|
288
|
+
if (this.state.debugMode) {
|
|
289
|
+
console.log(`[TagadaClient ${this.instanceId}] Restoring funnel session from handoff context:`, handoffData.context.funnelSessionId);
|
|
290
|
+
}
|
|
291
|
+
// The funnel client will pick this up during auto-initialization
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
console.error('[TagadaClient] Failed to decode token from handoff');
|
|
296
|
+
this.updateState({ isInitialized: true, isLoading: false });
|
|
297
|
+
}
|
|
298
|
+
return; // ✅ Auth handoff resolved successfully, exit early
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
console.error(`[TagadaClient ${this.instanceId}] ❌ Auth handoff failed, falling back to normal flow:`, error);
|
|
302
|
+
// Fall through to normal initialization
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Continue with normal flow if no authCode or resolution failed
|
|
306
|
+
await this.fallbackToNormalFlow();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Normal token initialization flow (no cross-domain handoff)
|
|
310
|
+
*/
|
|
311
|
+
async fallbackToNormalFlow() {
|
|
241
312
|
// Check for existing token in URL or storage
|
|
242
313
|
const existingToken = getClientToken();
|
|
243
314
|
const urlParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
|
|
244
315
|
const queryToken = urlParams.get('token');
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
316
|
+
console.log(`[TagadaClient ${this.instanceId}] Initializing token (normal flow)...`, {
|
|
317
|
+
hasExistingToken: !!existingToken,
|
|
318
|
+
hasQueryToken: !!queryToken,
|
|
319
|
+
storeId: this.state.pluginConfig.storeId,
|
|
320
|
+
});
|
|
251
321
|
let tokenToUse = null;
|
|
252
322
|
let shouldPersist = false;
|
|
253
323
|
if (queryToken) {
|
|
@@ -280,13 +350,15 @@ export class TagadaClient {
|
|
|
280
350
|
else {
|
|
281
351
|
// Create anonymous token
|
|
282
352
|
const storeId = this.state.pluginConfig.storeId;
|
|
353
|
+
console.log(`[TagadaClient ${this.instanceId}] No existing token, creating anonymous token...`, {
|
|
354
|
+
hasStoreId: !!storeId,
|
|
355
|
+
storeId,
|
|
356
|
+
});
|
|
283
357
|
if (storeId) {
|
|
284
|
-
if (this.state.debugMode) {
|
|
285
|
-
console.log(`[TagadaClient ${this.instanceId}] Creating anonymous token for store:`, storeId);
|
|
286
|
-
}
|
|
287
358
|
await this.createAnonymousToken(storeId);
|
|
288
359
|
}
|
|
289
360
|
else {
|
|
361
|
+
console.warn(`[TagadaClient ${this.instanceId}] No storeId in plugin config, skipping anonymous token creation`);
|
|
290
362
|
this.updateState({ isInitialized: true, isLoading: false });
|
|
291
363
|
}
|
|
292
364
|
}
|
|
@@ -404,12 +476,22 @@ export class TagadaClient {
|
|
|
404
476
|
osVersion: deviceInfo.userAgent.os.version,
|
|
405
477
|
deviceType: deviceInfo.userAgent.device?.type,
|
|
406
478
|
deviceModel: deviceInfo.userAgent.device?.model,
|
|
479
|
+
deviceVendor: deviceInfo.userAgent.device?.vendor,
|
|
480
|
+
userAgent: deviceInfo.userAgent.name,
|
|
481
|
+
engineName: deviceInfo.userAgent.engine.name,
|
|
482
|
+
engineVersion: deviceInfo.userAgent.engine.version,
|
|
483
|
+
cpuArchitecture: deviceInfo.userAgent.cpu.architecture,
|
|
484
|
+
isBot: deviceInfo.flags?.isBot ?? false,
|
|
485
|
+
isChromeFamily: deviceInfo.flags?.isChromeFamily ?? false,
|
|
486
|
+
isStandalonePWA: deviceInfo.flags?.isStandalonePWA ?? false,
|
|
487
|
+
isAppleSilicon: deviceInfo.flags?.isAppleSilicon ?? false,
|
|
407
488
|
screenWidth: deviceInfo.screenResolution.width,
|
|
408
489
|
screenHeight: deviceInfo.screenResolution.height,
|
|
409
490
|
timeZone: deviceInfo.timeZone,
|
|
410
491
|
draft, // 🎯 Pass draft mode to session init
|
|
492
|
+
fetchMessages: false,
|
|
411
493
|
};
|
|
412
|
-
const response = await this.apiClient.post('/api/v1/cms/session/init', sessionInitData);
|
|
494
|
+
const response = await this.apiClient.post('/api/v1/cms/session/v2/init', sessionInitData);
|
|
413
495
|
// Success - reset error tracking
|
|
414
496
|
this.lastSessionInitError = null;
|
|
415
497
|
this.sessionInitRetryCount = 0;
|
|
@@ -451,14 +533,16 @@ export class TagadaClient {
|
|
|
451
533
|
};
|
|
452
534
|
this.updateState({ store: storeConfig });
|
|
453
535
|
}
|
|
454
|
-
// Update Locale
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
536
|
+
// Update Locale (only if provided - V2 endpoint doesn't return locale)
|
|
537
|
+
if (response.locale) {
|
|
538
|
+
const localeConfig = {
|
|
539
|
+
locale: response.locale,
|
|
540
|
+
language: response.locale.split('-')[0],
|
|
541
|
+
region: response.locale.split('-')[1] ?? 'US',
|
|
542
|
+
messages: response.messages ?? {},
|
|
543
|
+
};
|
|
544
|
+
this.updateState({ locale: localeConfig });
|
|
545
|
+
}
|
|
462
546
|
// Update Currency
|
|
463
547
|
if (response.store) {
|
|
464
548
|
const currencyConfig = {
|
|
@@ -37,7 +37,9 @@ export const ENVIRONMENT_CONFIGS = {
|
|
|
37
37
|
endpoints: {
|
|
38
38
|
checkout: {
|
|
39
39
|
sessionInit: '/api/v1/checkout/session/init',
|
|
40
|
+
sessionInitAsync: '/api/v1/checkout/session/init-async',
|
|
40
41
|
sessionStatus: '/api/v1/checkout/session/status',
|
|
42
|
+
asyncStatus: '/api/public/v1/checkout/async-status',
|
|
41
43
|
},
|
|
42
44
|
customer: {
|
|
43
45
|
profile: '/api/v1/customer/profile',
|
|
@@ -53,7 +55,9 @@ export const ENVIRONMENT_CONFIGS = {
|
|
|
53
55
|
endpoints: {
|
|
54
56
|
checkout: {
|
|
55
57
|
sessionInit: '/api/v1/checkout/session/init',
|
|
58
|
+
sessionInitAsync: '/api/v1/checkout/session/init-async',
|
|
56
59
|
sessionStatus: '/api/v1/checkout/session/status',
|
|
60
|
+
asyncStatus: '/api/public/v1/checkout/async-status',
|
|
57
61
|
},
|
|
58
62
|
customer: {
|
|
59
63
|
profile: '/api/v1/customer/profile',
|
|
@@ -69,7 +73,9 @@ export const ENVIRONMENT_CONFIGS = {
|
|
|
69
73
|
endpoints: {
|
|
70
74
|
checkout: {
|
|
71
75
|
sessionInit: '/api/v1/checkout/session/init',
|
|
76
|
+
sessionInitAsync: '/api/v1/checkout/session/init-async',
|
|
72
77
|
sessionStatus: '/api/v1/checkout/session/status',
|
|
78
|
+
asyncStatus: '/api/public/v1/checkout/async-status',
|
|
73
79
|
},
|
|
74
80
|
customer: {
|
|
75
81
|
profile: '/api/v1/customer/profile',
|
|
@@ -13,6 +13,14 @@ export interface FunnelClientConfig {
|
|
|
13
13
|
* Set to false if you want to handle navigation manually
|
|
14
14
|
*/
|
|
15
15
|
autoRedirect?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Override funnelId from rawPluginConfig
|
|
18
|
+
*/
|
|
19
|
+
funnelId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Override stepId from rawPluginConfig
|
|
22
|
+
*/
|
|
23
|
+
stepId?: string;
|
|
16
24
|
}
|
|
17
25
|
export interface FunnelState {
|
|
18
26
|
context: SimpleFunnelContext | null;
|
|
@@ -64,8 +72,17 @@ export declare class FunnelClient {
|
|
|
64
72
|
}, funnelId?: string, entryStepId?: string): Promise<SimpleFunnelContext<{}>>;
|
|
65
73
|
/**
|
|
66
74
|
* Navigate
|
|
75
|
+
* @param event - Navigation event/action
|
|
76
|
+
* @param options - Navigation options
|
|
77
|
+
* @param options.fireAndForget - If true, queues navigation to QStash and returns immediately without waiting for result
|
|
78
|
+
* @param options.customerTags - Customer tags to set (merged with existing customer tags)
|
|
79
|
+
* @param options.deviceId - Device ID for geo/device tag enrichment (optional, rarely needed)
|
|
67
80
|
*/
|
|
68
|
-
navigate(event: FunnelAction
|
|
81
|
+
navigate(event: FunnelAction, options?: {
|
|
82
|
+
fireAndForget?: boolean;
|
|
83
|
+
customerTags?: string[];
|
|
84
|
+
deviceId?: string;
|
|
85
|
+
}): Promise<FunnelNavigationResult>;
|
|
69
86
|
/**
|
|
70
87
|
* Go to a specific step (direct navigation)
|
|
71
88
|
*/
|
|
@@ -118,19 +118,24 @@ export class FunnelClient {
|
|
|
118
118
|
// 🎯 Read funnel tracking data from injected HTML
|
|
119
119
|
const injectedFunnelId = getAssignedFunnelId(); // Funnel ID from server
|
|
120
120
|
const funnelVariantId = getAssignedFunnelVariant(); // A/B test variant ID
|
|
121
|
-
const
|
|
121
|
+
const injectedStepId = getAssignedFunnelStep(); // Current step ID
|
|
122
122
|
// 🎯 Get SDK override parameters (draft, funnelTracking, etc.)
|
|
123
123
|
const sdkParams = getSDKParams();
|
|
124
|
-
//
|
|
125
|
-
const finalFunnelId = injectedFunnelId || effectiveFunnelId;
|
|
124
|
+
// Priority: config override > injected > URL/prop
|
|
125
|
+
const finalFunnelId = this.config.funnelId || injectedFunnelId || effectiveFunnelId;
|
|
126
|
+
const finalStepId = this.config.stepId || injectedStepId;
|
|
126
127
|
if (this.config.debugMode) {
|
|
127
128
|
console.log('🚀 [FunnelClient] Auto-initializing...', {
|
|
128
129
|
existingSessionId,
|
|
129
130
|
effectiveFunnelId: finalFunnelId,
|
|
130
131
|
funnelVariantId, // 🎯 Log variant ID for debugging
|
|
131
|
-
funnelStepId, // 🎯 Log step ID for debugging
|
|
132
|
+
funnelStepId: finalStepId, // 🎯 Log step ID for debugging
|
|
132
133
|
draft: sdkParams.draft, // 🎯 Log draft mode
|
|
133
134
|
funnelTracking: sdkParams.funnelTracking, // 🎯 Log tracking flag
|
|
135
|
+
source: {
|
|
136
|
+
funnelId: this.config.funnelId ? 'config' : injectedFunnelId ? 'injected' : effectiveFunnelId ? 'url/prop' : 'none',
|
|
137
|
+
stepId: this.config.stepId ? 'config' : injectedStepId ? 'injected' : 'none',
|
|
138
|
+
},
|
|
134
139
|
});
|
|
135
140
|
}
|
|
136
141
|
// Note: We proceed even without funnelId/sessionId - the backend will create a new anonymous session if needed
|
|
@@ -145,7 +150,7 @@ export class FunnelClient {
|
|
|
145
150
|
existingSessionId: existingSessionId || undefined,
|
|
146
151
|
currentUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
147
152
|
funnelVariantId, // 🎯 Pass A/B test variant ID to backend
|
|
148
|
-
funnelStepId, // 🎯 Pass step ID to backend
|
|
153
|
+
funnelStepId: finalStepId, // 🎯 Pass step ID to backend (with config override)
|
|
149
154
|
draft: sdkParams.draft, // 🎯 Pass draft mode explicitly (more robust than URL parsing)
|
|
150
155
|
funnelTracking: sdkParams.funnelTracking, // 🎯 Pass funnel tracking flag explicitly
|
|
151
156
|
});
|
|
@@ -215,32 +220,100 @@ export class FunnelClient {
|
|
|
215
220
|
}
|
|
216
221
|
/**
|
|
217
222
|
* Navigate
|
|
223
|
+
* @param event - Navigation event/action
|
|
224
|
+
* @param options - Navigation options
|
|
225
|
+
* @param options.fireAndForget - If true, queues navigation to QStash and returns immediately without waiting for result
|
|
226
|
+
* @param options.customerTags - Customer tags to set (merged with existing customer tags)
|
|
227
|
+
* @param options.deviceId - Device ID for geo/device tag enrichment (optional, rarely needed)
|
|
218
228
|
*/
|
|
219
|
-
async navigate(event) {
|
|
229
|
+
async navigate(event, options) {
|
|
220
230
|
if (!this.state.context?.sessionId)
|
|
221
231
|
throw new Error('No active session');
|
|
222
232
|
this.updateState({ isNavigating: true, isLoading: true });
|
|
223
233
|
try {
|
|
234
|
+
// Get current funnel state from injected HTML/meta tags
|
|
235
|
+
let funnelVariantId = getAssignedFunnelVariant();
|
|
236
|
+
let funnelStepId = getAssignedFunnelStep();
|
|
237
|
+
const currentUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
238
|
+
// ✅ FALLBACK: Use config values if injection not available (e.g., on Shopify storefront)
|
|
239
|
+
if (!funnelStepId && this.config.stepId) {
|
|
240
|
+
funnelStepId = this.config.stepId;
|
|
241
|
+
if (this.config.debugMode) {
|
|
242
|
+
console.log('🔍 [FunnelClient.navigate] Using stepId from config (no injection):', funnelStepId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!funnelVariantId && this.config.variantId) {
|
|
246
|
+
funnelVariantId = this.config.variantId;
|
|
247
|
+
if (this.config.debugMode) {
|
|
248
|
+
console.log('🔍 [FunnelClient.navigate] Using variantId from config (no injection):', funnelVariantId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// ✅ DEBUG: Log what we're sending
|
|
252
|
+
if (this.config.debugMode) {
|
|
253
|
+
console.log('🔍 [FunnelClient.navigate] Sending to backend:', {
|
|
254
|
+
sessionId: this.state.context.sessionId,
|
|
255
|
+
currentUrl,
|
|
256
|
+
funnelStepId: funnelStepId || '(not found)',
|
|
257
|
+
funnelVariantId: funnelVariantId || '(not found)',
|
|
258
|
+
hasInjectedStepId: !!getAssignedFunnelStep(),
|
|
259
|
+
hasInjectedVariantId: !!getAssignedFunnelVariant(),
|
|
260
|
+
usedConfigFallback: !getAssignedFunnelStep() && !!this.config.stepId,
|
|
261
|
+
customerTags: options?.customerTags || '(none)',
|
|
262
|
+
deviceId: options?.deviceId || '(none)',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const fireAndForget = options?.fireAndForget || false;
|
|
224
266
|
const response = await this.resource.navigate({
|
|
225
267
|
sessionId: this.state.context.sessionId,
|
|
226
|
-
event
|
|
268
|
+
event,
|
|
269
|
+
currentUrl,
|
|
270
|
+
funnelStepId,
|
|
271
|
+
funnelVariantId,
|
|
272
|
+
fireAndForget,
|
|
273
|
+
customerTags: options?.customerTags,
|
|
274
|
+
deviceId: options?.deviceId,
|
|
227
275
|
});
|
|
228
|
-
if (response.success
|
|
229
|
-
|
|
230
|
-
|
|
276
|
+
if (!response.success || !response.result) {
|
|
277
|
+
throw new Error(response.error || 'Navigation failed');
|
|
278
|
+
}
|
|
279
|
+
const result = response.result;
|
|
280
|
+
// 🔥 Fire-and-forget mode: Just return acknowledgment, skip everything else
|
|
281
|
+
if (result.queued) {
|
|
231
282
|
this.updateState({ isNavigating: false, isLoading: false });
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const shouldAutoRedirect = this.config.autoRedirect !== false; // Default to true
|
|
235
|
-
if (shouldAutoRedirect && result?.url && typeof window !== 'undefined') {
|
|
283
|
+
// Update session ID if it changed (session recovery)
|
|
284
|
+
if (result.sessionId && result.sessionId !== this.state.context?.sessionId) {
|
|
236
285
|
if (this.config.debugMode) {
|
|
237
|
-
console.log(
|
|
286
|
+
console.log(`🔥 [FunnelClient] Session ID updated: ${this.state.context?.sessionId} → ${result.sessionId}`);
|
|
287
|
+
}
|
|
288
|
+
// Update context session ID
|
|
289
|
+
if (this.state.context) {
|
|
290
|
+
this.state.context.sessionId = result.sessionId;
|
|
238
291
|
}
|
|
239
|
-
|
|
292
|
+
}
|
|
293
|
+
if (this.config.debugMode) {
|
|
294
|
+
console.log('🔥 [FunnelClient] Navigation queued (fire-and-forget mode)');
|
|
240
295
|
}
|
|
241
296
|
return result;
|
|
242
297
|
}
|
|
243
|
-
|
|
298
|
+
// Normal navigation: handle redirect
|
|
299
|
+
const shouldAutoRedirect = this.config.autoRedirect !== false; // Default to true
|
|
300
|
+
// Skip refreshSession if auto-redirecting (next page will initialize with fresh state)
|
|
301
|
+
// Only refresh if staying on same page (autoRedirect: false)
|
|
302
|
+
if (!shouldAutoRedirect) {
|
|
303
|
+
if (this.config.debugMode) {
|
|
304
|
+
console.log('🔄 [FunnelClient] Refreshing session (no auto-redirect)');
|
|
305
|
+
}
|
|
306
|
+
await this.refreshSession();
|
|
307
|
+
}
|
|
308
|
+
this.updateState({ isNavigating: false, isLoading: false });
|
|
309
|
+
// Auto-redirect if enabled and result has a URL
|
|
310
|
+
if (shouldAutoRedirect && result?.url && typeof window !== 'undefined') {
|
|
311
|
+
if (this.config.debugMode) {
|
|
312
|
+
console.log('🚀 [FunnelClient] Auto-redirecting to:', result.url, '(skipped session refresh - next page will initialize)');
|
|
313
|
+
}
|
|
314
|
+
window.location.href = result.url;
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
244
317
|
}
|
|
245
318
|
catch (error) {
|
|
246
319
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -180,17 +180,60 @@ export declare class CheckoutResource {
|
|
|
180
180
|
private apiClient;
|
|
181
181
|
constructor(apiClient: ApiClient);
|
|
182
182
|
/**
|
|
183
|
-
* Initialize a new checkout session
|
|
183
|
+
* Initialize a new checkout session (sync mode)
|
|
184
|
+
* Response time: 2-5 seconds (full processing)
|
|
184
185
|
*/
|
|
185
186
|
initCheckout(params: CheckoutInitParams): Promise<{
|
|
186
187
|
checkoutUrl: string;
|
|
187
188
|
checkoutSession: CheckoutSession;
|
|
188
189
|
checkoutToken: string;
|
|
189
190
|
}>;
|
|
191
|
+
/**
|
|
192
|
+
* Initialize a new checkout session (async mode) ⚡
|
|
193
|
+
* Response time: ~50ms (20-50x faster!)
|
|
194
|
+
*
|
|
195
|
+
* Returns checkoutToken immediately, background job completes processing.
|
|
196
|
+
* Use getCheckout() to fetch full session data - it auto-waits for completion.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* // Fast init
|
|
200
|
+
* const { checkoutToken } = await checkout.initCheckoutAsync(params);
|
|
201
|
+
*
|
|
202
|
+
* // Redirect user immediately
|
|
203
|
+
* window.location.href = `/checkout/${checkoutToken}/op`;
|
|
204
|
+
*
|
|
205
|
+
* // By the time page loads, background processing is usually complete
|
|
206
|
+
*/
|
|
207
|
+
initCheckoutAsync(params: CheckoutInitParams): Promise<{
|
|
208
|
+
checkoutToken: string;
|
|
209
|
+
customerId: string;
|
|
210
|
+
status: 'processing';
|
|
211
|
+
}>;
|
|
212
|
+
/**
|
|
213
|
+
* Check async checkout processing status (instant, no waiting)
|
|
214
|
+
* Perfect for polling or checking if background job completed
|
|
215
|
+
*/
|
|
216
|
+
checkAsyncStatus(checkoutToken: string): Promise<{
|
|
217
|
+
isAsync: boolean;
|
|
218
|
+
isProcessing: boolean;
|
|
219
|
+
isComplete: boolean;
|
|
220
|
+
hasError: boolean;
|
|
221
|
+
error?: string;
|
|
222
|
+
}>;
|
|
190
223
|
/**
|
|
191
224
|
* Get checkout session by token
|
|
225
|
+
*
|
|
226
|
+
* SDK automatically waits for async completion (skipAsyncWait=false) for seamless UX.
|
|
227
|
+
* This ensures async checkout sessions are fully processed before returning data.
|
|
228
|
+
*
|
|
229
|
+
* If you need to skip waiting (e.g., for manual polling), use getCheckoutRaw() instead.
|
|
192
230
|
*/
|
|
193
231
|
getCheckout(checkoutToken: string, currency?: string): Promise<CheckoutData>;
|
|
232
|
+
/**
|
|
233
|
+
* Get checkout session by token without waiting for async completion
|
|
234
|
+
* Useful for manual polling scenarios
|
|
235
|
+
*/
|
|
236
|
+
getCheckoutRaw(checkoutToken: string, currency?: string): Promise<CheckoutData>;
|
|
194
237
|
/**
|
|
195
238
|
* Update checkout address
|
|
196
239
|
*/
|
|
@@ -7,20 +7,67 @@ export class CheckoutResource {
|
|
|
7
7
|
this.apiClient = apiClient;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Initialize a new checkout session
|
|
10
|
+
* Initialize a new checkout session (sync mode)
|
|
11
|
+
* Response time: 2-5 seconds (full processing)
|
|
11
12
|
*/
|
|
12
13
|
async initCheckout(params) {
|
|
13
14
|
// Pass all params including customerId to prevent duplicate customer creation
|
|
14
15
|
return this.apiClient.post('/api/v1/checkout/session/init', params);
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Initialize a new checkout session (async mode) ⚡
|
|
19
|
+
* Response time: ~50ms (20-50x faster!)
|
|
20
|
+
*
|
|
21
|
+
* Returns checkoutToken immediately, background job completes processing.
|
|
22
|
+
* Use getCheckout() to fetch full session data - it auto-waits for completion.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Fast init
|
|
26
|
+
* const { checkoutToken } = await checkout.initCheckoutAsync(params);
|
|
27
|
+
*
|
|
28
|
+
* // Redirect user immediately
|
|
29
|
+
* window.location.href = `/checkout/${checkoutToken}/op`;
|
|
30
|
+
*
|
|
31
|
+
* // By the time page loads, background processing is usually complete
|
|
32
|
+
*/
|
|
33
|
+
async initCheckoutAsync(params) {
|
|
34
|
+
return this.apiClient.post('/api/v1/checkout/session/init-async', params);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check async checkout processing status (instant, no waiting)
|
|
38
|
+
* Perfect for polling or checking if background job completed
|
|
39
|
+
*/
|
|
40
|
+
async checkAsyncStatus(checkoutToken) {
|
|
41
|
+
return this.apiClient.get(`/api/public/v1/checkout/async-status/${checkoutToken}`);
|
|
42
|
+
}
|
|
16
43
|
/**
|
|
17
44
|
* Get checkout session by token
|
|
45
|
+
*
|
|
46
|
+
* SDK automatically waits for async completion (skipAsyncWait=false) for seamless UX.
|
|
47
|
+
* This ensures async checkout sessions are fully processed before returning data.
|
|
48
|
+
*
|
|
49
|
+
* If you need to skip waiting (e.g., for manual polling), use getCheckoutRaw() instead.
|
|
18
50
|
*/
|
|
19
51
|
async getCheckout(checkoutToken, currency) {
|
|
20
52
|
const queryParams = new URLSearchParams();
|
|
21
53
|
if (currency) {
|
|
22
54
|
queryParams.set('currency', currency);
|
|
23
55
|
}
|
|
56
|
+
// SDK explicitly waits for async completion (default is skip=true for retro compat)
|
|
57
|
+
queryParams.set('skipAsyncWait', 'false');
|
|
58
|
+
const url = `/api/v1/checkout-sessions/${checkoutToken}/v2${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
|
59
|
+
return this.apiClient.get(url);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get checkout session by token without waiting for async completion
|
|
63
|
+
* Useful for manual polling scenarios
|
|
64
|
+
*/
|
|
65
|
+
async getCheckoutRaw(checkoutToken, currency) {
|
|
66
|
+
const queryParams = new URLSearchParams();
|
|
67
|
+
if (currency) {
|
|
68
|
+
queryParams.set('currency', currency);
|
|
69
|
+
}
|
|
70
|
+
queryParams.set('skipAsyncWait', 'true');
|
|
24
71
|
const url = `/api/v1/checkout-sessions/${checkoutToken}/v2${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
|
25
72
|
return this.apiClient.get(url);
|
|
26
73
|
}
|