@tagadapay/plugin-sdk 3.0.12 โ 3.0.15
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 +3645 -115
- 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 +5 -0
- package/dist/v2/core/client.js +135 -26
- package/dist/v2/core/config/environment.js +6 -0
- package/dist/v2/core/funnelClient.d.ts +27 -1
- package/dist/v2/core/funnelClient.js +124 -23
- package/dist/v2/core/resources/checkout.d.ts +76 -1
- package/dist/v2/core/resources/checkout.js +86 -1
- package/dist/v2/core/resources/funnel.d.ts +45 -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/react/providers/TagadaProvider.js +61 -1
- 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
|
@@ -58,6 +58,7 @@ export declare class TagadaClient {
|
|
|
58
58
|
private tokenPromise;
|
|
59
59
|
private tokenResolver;
|
|
60
60
|
private boundHandleStorageChange;
|
|
61
|
+
private boundHandlePageshow;
|
|
61
62
|
private readonly config;
|
|
62
63
|
private instanceId;
|
|
63
64
|
private isInitializingSession;
|
|
@@ -101,6 +102,10 @@ export declare class TagadaClient {
|
|
|
101
102
|
* Initialize token and session
|
|
102
103
|
*/
|
|
103
104
|
private initializeToken;
|
|
105
|
+
/**
|
|
106
|
+
* Normal token initialization flow (no cross-domain handoff)
|
|
107
|
+
*/
|
|
108
|
+
private fallbackToNormalFlow;
|
|
104
109
|
/**
|
|
105
110
|
* Set token and resolve waiting requests
|
|
106
111
|
*/
|
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,36 @@ 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
|
+
this.boundHandlePageshow = (event) => {
|
|
30
|
+
if (event.persisted) {
|
|
31
|
+
if (this.state.debugMode) {
|
|
32
|
+
console.log(`[TagadaClient ${this.instanceId}] Page restored from BFcache (back button), re-initializing funnel...`);
|
|
33
|
+
}
|
|
34
|
+
// If we have an active session and store, we only need to re-initialize the funnel
|
|
35
|
+
// This ensures tracking is correct and the session is fresh on the backend
|
|
36
|
+
if (this.funnel && this.state.session && this.state.store) {
|
|
37
|
+
this.funnel.resetInitialization();
|
|
38
|
+
const accountId = this.getAccountId();
|
|
39
|
+
const urlParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
|
|
40
|
+
const funnelId = urlParams.get('funnelId') || undefined;
|
|
41
|
+
this.funnel.autoInitialize({ customerId: this.state.session.customerId, sessionId: this.state.session.sessionId }, { id: this.state.store.id, accountId }, funnelId).catch((err) => {
|
|
42
|
+
console.error('[TagadaClient] Funnel re-initialization failed:', err);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// If state is missing, perform a full initialization
|
|
47
|
+
this.sessionInitRetryCount = 0;
|
|
48
|
+
this.initialize();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
console.log(`[TagadaClient ${this.instanceId}] Initializing...`);
|
|
53
|
+
console.log(`[TagadaClient ${this.instanceId}] Config:`, {
|
|
54
|
+
debugMode: config.debugMode,
|
|
55
|
+
hasRawPluginConfig: !!config.rawPluginConfig,
|
|
56
|
+
rawPluginConfig: config.rawPluginConfig,
|
|
57
|
+
features: config.features,
|
|
58
|
+
});
|
|
31
59
|
// Handle preview mode FIRST - clears state if needed
|
|
32
60
|
// This ensures clean state when CRM previews pages
|
|
33
61
|
const previewModeActive = handlePreviewMode(this.config.debugMode);
|
|
@@ -77,10 +105,14 @@ export class TagadaClient {
|
|
|
77
105
|
isInitialized: false,
|
|
78
106
|
isSessionInitialized: false,
|
|
79
107
|
pluginConfig: { basePath: '/', config: {} },
|
|
80
|
-
pluginConfigLoading:
|
|
108
|
+
pluginConfigLoading: true, // Always true - loadPluginConfig will process rawPluginConfig
|
|
81
109
|
debugMode: config.debugMode ?? env !== 'production',
|
|
82
110
|
token: null,
|
|
83
111
|
};
|
|
112
|
+
console.log(`[TagadaClient ${this.instanceId}] Initial state:`, {
|
|
113
|
+
pluginConfigLoading: this.state.pluginConfigLoading,
|
|
114
|
+
hasRawPluginConfig: !!config.rawPluginConfig,
|
|
115
|
+
});
|
|
84
116
|
// Initialize API Client
|
|
85
117
|
this.apiClient = new ApiClient({
|
|
86
118
|
baseURL: envConfig.apiConfig.baseUrl,
|
|
@@ -96,6 +128,9 @@ export class TagadaClient {
|
|
|
96
128
|
pluginConfig: this.state.pluginConfig,
|
|
97
129
|
environment: this.state.environment,
|
|
98
130
|
autoRedirect: funnelConfig.autoRedirect,
|
|
131
|
+
// Pass funnelId and stepId from rawPluginConfig to enable config-based initialization
|
|
132
|
+
funnelId: config.rawPluginConfig?.funnelId,
|
|
133
|
+
stepId: config.rawPluginConfig?.stepId,
|
|
99
134
|
});
|
|
100
135
|
}
|
|
101
136
|
// Setup token waiting mechanism
|
|
@@ -103,6 +138,7 @@ export class TagadaClient {
|
|
|
103
138
|
// Listen for storage changes (cross-tab sync)
|
|
104
139
|
if (typeof window !== 'undefined') {
|
|
105
140
|
window.addEventListener('storage', this.boundHandleStorageChange);
|
|
141
|
+
window.addEventListener('pageshow', this.boundHandlePageshow);
|
|
106
142
|
}
|
|
107
143
|
// Setup config hot-reload listener (for live config editing)
|
|
108
144
|
this.setupConfigHotReload();
|
|
@@ -115,6 +151,7 @@ export class TagadaClient {
|
|
|
115
151
|
destroy() {
|
|
116
152
|
if (typeof window !== 'undefined') {
|
|
117
153
|
window.removeEventListener('storage', this.boundHandleStorageChange);
|
|
154
|
+
window.removeEventListener('pageshow', this.boundHandlePageshow);
|
|
118
155
|
}
|
|
119
156
|
if (this.state.debugMode) {
|
|
120
157
|
console.log(`[TagadaClient ${this.instanceId}] Destroyed`);
|
|
@@ -206,11 +243,19 @@ export class TagadaClient {
|
|
|
206
243
|
* Load plugin configuration
|
|
207
244
|
*/
|
|
208
245
|
async initializePluginConfig() {
|
|
209
|
-
|
|
246
|
+
console.log(`[TagadaClient ${this.instanceId}] initializePluginConfig called`, {
|
|
247
|
+
pluginConfigLoading: this.state.pluginConfigLoading,
|
|
248
|
+
hasRawPluginConfig: !!this.config.rawPluginConfig,
|
|
249
|
+
});
|
|
250
|
+
if (!this.state.pluginConfigLoading) {
|
|
251
|
+
console.log(`[TagadaClient ${this.instanceId}] Plugin config already loading or loaded, skipping...`);
|
|
210
252
|
return;
|
|
253
|
+
}
|
|
211
254
|
try {
|
|
212
255
|
const configVariant = this.config.localConfig || 'default';
|
|
256
|
+
console.log(`[TagadaClient ${this.instanceId}] Loading plugin config with variant: ${configVariant}`);
|
|
213
257
|
const config = await loadPluginConfig(configVariant, this.config.rawPluginConfig);
|
|
258
|
+
console.log(`[TagadaClient ${this.instanceId}] Plugin config loaded:`, config);
|
|
214
259
|
this.updateState({
|
|
215
260
|
pluginConfig: config,
|
|
216
261
|
pluginConfigLoading: false,
|
|
@@ -222,9 +267,6 @@ export class TagadaClient {
|
|
|
222
267
|
environment: this.state.environment,
|
|
223
268
|
});
|
|
224
269
|
}
|
|
225
|
-
if (this.state.debugMode) {
|
|
226
|
-
console.log('[TagadaClient] Plugin config loaded:', config);
|
|
227
|
-
}
|
|
228
270
|
}
|
|
229
271
|
catch (error) {
|
|
230
272
|
console.error('[TagadaClient] Failed to load plugin config:', error);
|
|
@@ -238,16 +280,69 @@ export class TagadaClient {
|
|
|
238
280
|
* Initialize token and session
|
|
239
281
|
*/
|
|
240
282
|
async initializeToken() {
|
|
283
|
+
// ๐ PRIORITY 1: Check for authCode (cross-domain handoff)
|
|
284
|
+
// This ALWAYS takes precedence over existing tokens
|
|
285
|
+
if (shouldResolveAuthCode()) {
|
|
286
|
+
const storeId = this.state.pluginConfig.storeId;
|
|
287
|
+
if (!storeId) {
|
|
288
|
+
console.error('[TagadaClient] Cannot resolve authCode: storeId not found in config');
|
|
289
|
+
return this.fallbackToNormalFlow();
|
|
290
|
+
}
|
|
291
|
+
console.log(`[TagadaClient ${this.instanceId}] ๐ Cross-domain auth detected, resolving...`);
|
|
292
|
+
try {
|
|
293
|
+
const authCode = new URLSearchParams(window.location.search).get('authCode');
|
|
294
|
+
if (!authCode) {
|
|
295
|
+
return this.fallbackToNormalFlow();
|
|
296
|
+
}
|
|
297
|
+
// Resolve the handoff
|
|
298
|
+
const handoffData = await resolveAuthHandoff(authCode, storeId, this.state.environment.apiConfig.baseUrl, this.state.debugMode);
|
|
299
|
+
console.log(`[TagadaClient ${this.instanceId}] โ
Auth handoff resolved:`, {
|
|
300
|
+
customerId: handoffData.customer.id,
|
|
301
|
+
role: handoffData.customer.role,
|
|
302
|
+
hasContext: Object.keys(handoffData.context).length > 0,
|
|
303
|
+
});
|
|
304
|
+
// Set the new token (already stored by resolveAuthHandoff)
|
|
305
|
+
this.setToken(handoffData.token);
|
|
306
|
+
// Decode session from token
|
|
307
|
+
const decodedSession = decodeJWTClient(handoffData.token);
|
|
308
|
+
if (decodedSession) {
|
|
309
|
+
this.updateState({ session: decodedSession });
|
|
310
|
+
await this.initializeSession(decodedSession);
|
|
311
|
+
// If context has funnelSessionId, restore it
|
|
312
|
+
if (handoffData.context?.funnelSessionId && this.funnel) {
|
|
313
|
+
if (this.state.debugMode) {
|
|
314
|
+
console.log(`[TagadaClient ${this.instanceId}] Restoring funnel session from handoff context:`, handoffData.context.funnelSessionId);
|
|
315
|
+
}
|
|
316
|
+
// The funnel client will pick this up during auto-initialization
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
console.error('[TagadaClient] Failed to decode token from handoff');
|
|
321
|
+
this.updateState({ isInitialized: true, isLoading: false });
|
|
322
|
+
}
|
|
323
|
+
return; // โ
Auth handoff resolved successfully, exit early
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
console.error(`[TagadaClient ${this.instanceId}] โ Auth handoff failed, falling back to normal flow:`, error);
|
|
327
|
+
// Fall through to normal initialization
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Continue with normal flow if no authCode or resolution failed
|
|
331
|
+
await this.fallbackToNormalFlow();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Normal token initialization flow (no cross-domain handoff)
|
|
335
|
+
*/
|
|
336
|
+
async fallbackToNormalFlow() {
|
|
241
337
|
// Check for existing token in URL or storage
|
|
242
338
|
const existingToken = getClientToken();
|
|
243
339
|
const urlParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
|
|
244
340
|
const queryToken = urlParams.get('token');
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
341
|
+
console.log(`[TagadaClient ${this.instanceId}] Initializing token (normal flow)...`, {
|
|
342
|
+
hasExistingToken: !!existingToken,
|
|
343
|
+
hasQueryToken: !!queryToken,
|
|
344
|
+
storeId: this.state.pluginConfig.storeId,
|
|
345
|
+
});
|
|
251
346
|
let tokenToUse = null;
|
|
252
347
|
let shouldPersist = false;
|
|
253
348
|
if (queryToken) {
|
|
@@ -280,13 +375,15 @@ export class TagadaClient {
|
|
|
280
375
|
else {
|
|
281
376
|
// Create anonymous token
|
|
282
377
|
const storeId = this.state.pluginConfig.storeId;
|
|
378
|
+
console.log(`[TagadaClient ${this.instanceId}] No existing token, creating anonymous token...`, {
|
|
379
|
+
hasStoreId: !!storeId,
|
|
380
|
+
storeId,
|
|
381
|
+
});
|
|
283
382
|
if (storeId) {
|
|
284
|
-
if (this.state.debugMode) {
|
|
285
|
-
console.log(`[TagadaClient ${this.instanceId}] Creating anonymous token for store:`, storeId);
|
|
286
|
-
}
|
|
287
383
|
await this.createAnonymousToken(storeId);
|
|
288
384
|
}
|
|
289
385
|
else {
|
|
386
|
+
console.warn(`[TagadaClient ${this.instanceId}] No storeId in plugin config, skipping anonymous token creation`);
|
|
290
387
|
this.updateState({ isInitialized: true, isLoading: false });
|
|
291
388
|
}
|
|
292
389
|
}
|
|
@@ -404,12 +501,22 @@ export class TagadaClient {
|
|
|
404
501
|
osVersion: deviceInfo.userAgent.os.version,
|
|
405
502
|
deviceType: deviceInfo.userAgent.device?.type,
|
|
406
503
|
deviceModel: deviceInfo.userAgent.device?.model,
|
|
504
|
+
deviceVendor: deviceInfo.userAgent.device?.vendor,
|
|
505
|
+
userAgent: deviceInfo.userAgent.name,
|
|
506
|
+
engineName: deviceInfo.userAgent.engine.name,
|
|
507
|
+
engineVersion: deviceInfo.userAgent.engine.version,
|
|
508
|
+
cpuArchitecture: deviceInfo.userAgent.cpu.architecture,
|
|
509
|
+
isBot: deviceInfo.flags?.isBot ?? false,
|
|
510
|
+
isChromeFamily: deviceInfo.flags?.isChromeFamily ?? false,
|
|
511
|
+
isStandalonePWA: deviceInfo.flags?.isStandalonePWA ?? false,
|
|
512
|
+
isAppleSilicon: deviceInfo.flags?.isAppleSilicon ?? false,
|
|
407
513
|
screenWidth: deviceInfo.screenResolution.width,
|
|
408
514
|
screenHeight: deviceInfo.screenResolution.height,
|
|
409
515
|
timeZone: deviceInfo.timeZone,
|
|
410
516
|
draft, // ๐ฏ Pass draft mode to session init
|
|
517
|
+
fetchMessages: false,
|
|
411
518
|
};
|
|
412
|
-
const response = await this.apiClient.post('/api/v1/cms/session/init', sessionInitData);
|
|
519
|
+
const response = await this.apiClient.post('/api/v1/cms/session/v2/init', sessionInitData);
|
|
413
520
|
// Success - reset error tracking
|
|
414
521
|
this.lastSessionInitError = null;
|
|
415
522
|
this.sessionInitRetryCount = 0;
|
|
@@ -451,14 +558,16 @@ export class TagadaClient {
|
|
|
451
558
|
};
|
|
452
559
|
this.updateState({ store: storeConfig });
|
|
453
560
|
}
|
|
454
|
-
// Update Locale
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
561
|
+
// Update Locale (only if provided - V2 endpoint doesn't return locale)
|
|
562
|
+
if (response.locale) {
|
|
563
|
+
const localeConfig = {
|
|
564
|
+
locale: response.locale,
|
|
565
|
+
language: response.locale.split('-')[0],
|
|
566
|
+
region: response.locale.split('-')[1] ?? 'US',
|
|
567
|
+
messages: response.messages ?? {},
|
|
568
|
+
};
|
|
569
|
+
this.updateState({ locale: localeConfig });
|
|
570
|
+
}
|
|
462
571
|
// Update Currency
|
|
463
572
|
if (response.store) {
|
|
464
573
|
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;
|
|
@@ -42,6 +50,15 @@ export declare class FunnelClient {
|
|
|
42
50
|
* Get current state
|
|
43
51
|
*/
|
|
44
52
|
getState(): FunnelState;
|
|
53
|
+
/**
|
|
54
|
+
* Get the session ID that would be used for initialization (URL params or cookie)
|
|
55
|
+
* This allows getting the session ID even before the client is fully initialized.
|
|
56
|
+
*/
|
|
57
|
+
getDetectedSessionId(): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* Reset initialization state (used for back-button restores)
|
|
60
|
+
*/
|
|
61
|
+
resetInitialization(): void;
|
|
45
62
|
/**
|
|
46
63
|
* Initialize session with automatic detection (cookies, URL, etc.)
|
|
47
64
|
*/
|
|
@@ -64,8 +81,17 @@ export declare class FunnelClient {
|
|
|
64
81
|
}, funnelId?: string, entryStepId?: string): Promise<SimpleFunnelContext<{}>>;
|
|
65
82
|
/**
|
|
66
83
|
* Navigate
|
|
84
|
+
* @param event - Navigation event/action
|
|
85
|
+
* @param options - Navigation options
|
|
86
|
+
* @param options.fireAndForget - If true, queues navigation to QStash and returns immediately without waiting for result
|
|
87
|
+
* @param options.customerTags - Customer tags to set (merged with existing customer tags)
|
|
88
|
+
* @param options.deviceId - Device ID for geo/device tag enrichment (optional, rarely needed)
|
|
67
89
|
*/
|
|
68
|
-
navigate(event: FunnelAction
|
|
90
|
+
navigate(event: FunnelAction, options?: {
|
|
91
|
+
fireAndForget?: boolean;
|
|
92
|
+
customerTags?: string[];
|
|
93
|
+
deviceId?: string;
|
|
94
|
+
}): Promise<FunnelNavigationResult>;
|
|
69
95
|
/**
|
|
70
96
|
* Go to a specific step (direct navigation)
|
|
71
97
|
*/
|
|
@@ -92,6 +92,37 @@ export class FunnelClient {
|
|
|
92
92
|
getState() {
|
|
93
93
|
return this.state;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Get the session ID that would be used for initialization (URL params or cookie)
|
|
97
|
+
* This allows getting the session ID even before the client is fully initialized.
|
|
98
|
+
*/
|
|
99
|
+
getDetectedSessionId() {
|
|
100
|
+
// Priority 1: Already initialized session
|
|
101
|
+
if (this.state.context?.sessionId) {
|
|
102
|
+
return this.state.context.sessionId;
|
|
103
|
+
}
|
|
104
|
+
if (typeof window === 'undefined')
|
|
105
|
+
return null;
|
|
106
|
+
// Priority 2: URL params
|
|
107
|
+
const params = new URLSearchParams(window.location.search);
|
|
108
|
+
const urlSessionId = params.get('funnelSessionId');
|
|
109
|
+
if (urlSessionId)
|
|
110
|
+
return urlSessionId;
|
|
111
|
+
// Priority 3: Cookie
|
|
112
|
+
return getFunnelSessionCookie() || null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Reset initialization state (used for back-button restores)
|
|
116
|
+
*/
|
|
117
|
+
resetInitialization() {
|
|
118
|
+
this.initializationAttempted = false;
|
|
119
|
+
this.isInitializing = false;
|
|
120
|
+
// Clear context to force a fresh autoInitialize call to hit the backend
|
|
121
|
+
this.updateState({
|
|
122
|
+
context: null,
|
|
123
|
+
isInitialized: false,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
95
126
|
/**
|
|
96
127
|
* Initialize session with automatic detection (cookies, URL, etc.)
|
|
97
128
|
*/
|
|
@@ -106,31 +137,33 @@ export class FunnelClient {
|
|
|
106
137
|
this.isInitializing = true;
|
|
107
138
|
this.updateState({ isLoading: true, error: null });
|
|
108
139
|
try {
|
|
109
|
-
//
|
|
140
|
+
// ๐ฏ Get detected session ID
|
|
141
|
+
const existingSessionId = this.getDetectedSessionId();
|
|
142
|
+
// URL params for funnelId
|
|
110
143
|
const params = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
|
|
111
144
|
const urlFunnelId = params.get('funnelId');
|
|
112
145
|
const effectiveFunnelId = urlFunnelId || funnelId;
|
|
113
|
-
let existingSessionId = params.get('funnelSessionId');
|
|
114
|
-
// Cookie fallback
|
|
115
|
-
if (!existingSessionId) {
|
|
116
|
-
existingSessionId = getFunnelSessionCookie() || null;
|
|
117
|
-
}
|
|
118
146
|
// ๐ฏ Read funnel tracking data from injected HTML
|
|
119
147
|
const injectedFunnelId = getAssignedFunnelId(); // Funnel ID from server
|
|
120
148
|
const funnelVariantId = getAssignedFunnelVariant(); // A/B test variant ID
|
|
121
|
-
const
|
|
149
|
+
const injectedStepId = getAssignedFunnelStep(); // Current step ID
|
|
122
150
|
// ๐ฏ Get SDK override parameters (draft, funnelTracking, etc.)
|
|
123
151
|
const sdkParams = getSDKParams();
|
|
124
|
-
//
|
|
125
|
-
const finalFunnelId = injectedFunnelId || effectiveFunnelId;
|
|
152
|
+
// Priority: config override > injected > URL/prop
|
|
153
|
+
const finalFunnelId = this.config.funnelId || injectedFunnelId || effectiveFunnelId;
|
|
154
|
+
const finalStepId = this.config.stepId || injectedStepId;
|
|
126
155
|
if (this.config.debugMode) {
|
|
127
156
|
console.log('๐ [FunnelClient] Auto-initializing...', {
|
|
128
157
|
existingSessionId,
|
|
129
158
|
effectiveFunnelId: finalFunnelId,
|
|
130
159
|
funnelVariantId, // ๐ฏ Log variant ID for debugging
|
|
131
|
-
funnelStepId, // ๐ฏ Log step ID for debugging
|
|
160
|
+
funnelStepId: finalStepId, // ๐ฏ Log step ID for debugging
|
|
132
161
|
draft: sdkParams.draft, // ๐ฏ Log draft mode
|
|
133
162
|
funnelTracking: sdkParams.funnelTracking, // ๐ฏ Log tracking flag
|
|
163
|
+
source: {
|
|
164
|
+
funnelId: this.config.funnelId ? 'config' : injectedFunnelId ? 'injected' : effectiveFunnelId ? 'url/prop' : 'none',
|
|
165
|
+
stepId: this.config.stepId ? 'config' : injectedStepId ? 'injected' : 'none',
|
|
166
|
+
},
|
|
134
167
|
});
|
|
135
168
|
}
|
|
136
169
|
// Note: We proceed even without funnelId/sessionId - the backend will create a new anonymous session if needed
|
|
@@ -145,7 +178,7 @@ export class FunnelClient {
|
|
|
145
178
|
existingSessionId: existingSessionId || undefined,
|
|
146
179
|
currentUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
147
180
|
funnelVariantId, // ๐ฏ Pass A/B test variant ID to backend
|
|
148
|
-
funnelStepId, // ๐ฏ Pass step ID to backend
|
|
181
|
+
funnelStepId: finalStepId, // ๐ฏ Pass step ID to backend (with config override)
|
|
149
182
|
draft: sdkParams.draft, // ๐ฏ Pass draft mode explicitly (more robust than URL parsing)
|
|
150
183
|
funnelTracking: sdkParams.funnelTracking, // ๐ฏ Pass funnel tracking flag explicitly
|
|
151
184
|
});
|
|
@@ -215,32 +248,100 @@ export class FunnelClient {
|
|
|
215
248
|
}
|
|
216
249
|
/**
|
|
217
250
|
* Navigate
|
|
251
|
+
* @param event - Navigation event/action
|
|
252
|
+
* @param options - Navigation options
|
|
253
|
+
* @param options.fireAndForget - If true, queues navigation to QStash and returns immediately without waiting for result
|
|
254
|
+
* @param options.customerTags - Customer tags to set (merged with existing customer tags)
|
|
255
|
+
* @param options.deviceId - Device ID for geo/device tag enrichment (optional, rarely needed)
|
|
218
256
|
*/
|
|
219
|
-
async navigate(event) {
|
|
257
|
+
async navigate(event, options) {
|
|
220
258
|
if (!this.state.context?.sessionId)
|
|
221
259
|
throw new Error('No active session');
|
|
222
260
|
this.updateState({ isNavigating: true, isLoading: true });
|
|
223
261
|
try {
|
|
262
|
+
// Get current funnel state from injected HTML/meta tags
|
|
263
|
+
let funnelVariantId = getAssignedFunnelVariant();
|
|
264
|
+
let funnelStepId = getAssignedFunnelStep();
|
|
265
|
+
const currentUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
266
|
+
// โ
FALLBACK: Use config values if injection not available (e.g., on Shopify storefront)
|
|
267
|
+
if (!funnelStepId && this.config.stepId) {
|
|
268
|
+
funnelStepId = this.config.stepId;
|
|
269
|
+
if (this.config.debugMode) {
|
|
270
|
+
console.log('๐ [FunnelClient.navigate] Using stepId from config (no injection):', funnelStepId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!funnelVariantId && this.config.variantId) {
|
|
274
|
+
funnelVariantId = this.config.variantId;
|
|
275
|
+
if (this.config.debugMode) {
|
|
276
|
+
console.log('๐ [FunnelClient.navigate] Using variantId from config (no injection):', funnelVariantId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// โ
DEBUG: Log what we're sending
|
|
280
|
+
if (this.config.debugMode) {
|
|
281
|
+
console.log('๐ [FunnelClient.navigate] Sending to backend:', {
|
|
282
|
+
sessionId: this.state.context.sessionId,
|
|
283
|
+
currentUrl,
|
|
284
|
+
funnelStepId: funnelStepId || '(not found)',
|
|
285
|
+
funnelVariantId: funnelVariantId || '(not found)',
|
|
286
|
+
hasInjectedStepId: !!getAssignedFunnelStep(),
|
|
287
|
+
hasInjectedVariantId: !!getAssignedFunnelVariant(),
|
|
288
|
+
usedConfigFallback: !getAssignedFunnelStep() && !!this.config.stepId,
|
|
289
|
+
customerTags: options?.customerTags || '(none)',
|
|
290
|
+
deviceId: options?.deviceId || '(none)',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const fireAndForget = options?.fireAndForget || false;
|
|
224
294
|
const response = await this.resource.navigate({
|
|
225
295
|
sessionId: this.state.context.sessionId,
|
|
226
|
-
event
|
|
296
|
+
event,
|
|
297
|
+
currentUrl,
|
|
298
|
+
funnelStepId,
|
|
299
|
+
funnelVariantId,
|
|
300
|
+
fireAndForget,
|
|
301
|
+
customerTags: options?.customerTags,
|
|
302
|
+
deviceId: options?.deviceId,
|
|
227
303
|
});
|
|
228
|
-
if (response.success
|
|
229
|
-
|
|
230
|
-
|
|
304
|
+
if (!response.success || !response.result) {
|
|
305
|
+
throw new Error(response.error || 'Navigation failed');
|
|
306
|
+
}
|
|
307
|
+
const result = response.result;
|
|
308
|
+
// ๐ฅ Fire-and-forget mode: Just return acknowledgment, skip everything else
|
|
309
|
+
if (result.queued) {
|
|
231
310
|
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') {
|
|
311
|
+
// Update session ID if it changed (session recovery)
|
|
312
|
+
if (result.sessionId && result.sessionId !== this.state.context?.sessionId) {
|
|
236
313
|
if (this.config.debugMode) {
|
|
237
|
-
console.log(
|
|
314
|
+
console.log(`๐ฅ [FunnelClient] Session ID updated: ${this.state.context?.sessionId} โ ${result.sessionId}`);
|
|
315
|
+
}
|
|
316
|
+
// Update context session ID
|
|
317
|
+
if (this.state.context) {
|
|
318
|
+
this.state.context.sessionId = result.sessionId;
|
|
238
319
|
}
|
|
239
|
-
|
|
320
|
+
}
|
|
321
|
+
if (this.config.debugMode) {
|
|
322
|
+
console.log('๐ฅ [FunnelClient] Navigation queued (fire-and-forget mode)');
|
|
240
323
|
}
|
|
241
324
|
return result;
|
|
242
325
|
}
|
|
243
|
-
|
|
326
|
+
// Normal navigation: handle redirect
|
|
327
|
+
const shouldAutoRedirect = this.config.autoRedirect !== false; // Default to true
|
|
328
|
+
// Skip refreshSession if auto-redirecting (next page will initialize with fresh state)
|
|
329
|
+
// Only refresh if staying on same page (autoRedirect: false)
|
|
330
|
+
if (!shouldAutoRedirect) {
|
|
331
|
+
if (this.config.debugMode) {
|
|
332
|
+
console.log('๐ [FunnelClient] Refreshing session (no auto-redirect)');
|
|
333
|
+
}
|
|
334
|
+
await this.refreshSession();
|
|
335
|
+
}
|
|
336
|
+
this.updateState({ isNavigating: false, isLoading: false });
|
|
337
|
+
// Auto-redirect if enabled and result has a URL
|
|
338
|
+
if (shouldAutoRedirect && result?.url && typeof window !== 'undefined') {
|
|
339
|
+
if (this.config.debugMode) {
|
|
340
|
+
console.log('๐ [FunnelClient] Auto-redirecting to:', result.url, '(skipped session refresh - next page will initialize)');
|
|
341
|
+
}
|
|
342
|
+
window.location.href = result.url;
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
244
345
|
}
|
|
245
346
|
catch (error) {
|
|
246
347
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -180,17 +180,92 @@ 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
|
+
* Preload checkout session (ultra-fast background pre-computation) โกโกโก
|
|
214
|
+
*
|
|
215
|
+
* This is the recommended way to handle cart changes or "Buy Now" intent.
|
|
216
|
+
* It pre-computes everything (checkoutToken, navigation URL, CMS session)
|
|
217
|
+
* before the user even clicks the checkout button.
|
|
218
|
+
*
|
|
219
|
+
* The SDK automatically gets funnelSessionId from FunnelClient if provided.
|
|
220
|
+
* Only FunnelClient knows how to properly extract funnelSessionId (from state, URL, cookies, etc.)
|
|
221
|
+
*
|
|
222
|
+
* @param params - Checkout and funnel parameters
|
|
223
|
+
* @param getFunnelSessionId - Optional function to get funnelSessionId from FunnelClient
|
|
224
|
+
* This maintains separation of concerns - only FunnelClient knows how to get it
|
|
225
|
+
*
|
|
226
|
+
* @returns { checkoutToken, customerId, navigationUrl }
|
|
227
|
+
*/
|
|
228
|
+
preloadCheckout(params: CheckoutInitParams & {
|
|
229
|
+
funnelSessionId?: string;
|
|
230
|
+
currentUrl?: string;
|
|
231
|
+
funnelStepId?: string;
|
|
232
|
+
funnelVariantId?: string;
|
|
233
|
+
navigationEvent?: string | {
|
|
234
|
+
type: string;
|
|
235
|
+
data?: any;
|
|
236
|
+
};
|
|
237
|
+
navigationOptions?: any;
|
|
238
|
+
}, getFunnelSessionId?: () => string | null | undefined): Promise<{
|
|
239
|
+
checkoutToken: string;
|
|
240
|
+
customerId: string;
|
|
241
|
+
navigationUrl: string | null;
|
|
242
|
+
funnelStepId?: string;
|
|
243
|
+
}>;
|
|
244
|
+
/**
|
|
245
|
+
* Check async checkout processing status (instant, no waiting)
|
|
246
|
+
* Perfect for polling or checking if background job completed
|
|
247
|
+
*/
|
|
248
|
+
checkAsyncStatus(checkoutToken: string): Promise<{
|
|
249
|
+
isAsync: boolean;
|
|
250
|
+
isProcessing: boolean;
|
|
251
|
+
isComplete: boolean;
|
|
252
|
+
hasError: boolean;
|
|
253
|
+
error?: string;
|
|
254
|
+
}>;
|
|
190
255
|
/**
|
|
191
256
|
* Get checkout session by token
|
|
257
|
+
*
|
|
258
|
+
* SDK automatically waits for async completion (skipAsyncWait=false) for seamless UX.
|
|
259
|
+
* This ensures async checkout sessions are fully processed before returning data.
|
|
260
|
+
*
|
|
261
|
+
* If you need to skip waiting (e.g., for manual polling), use getCheckoutRaw() instead.
|
|
192
262
|
*/
|
|
193
263
|
getCheckout(checkoutToken: string, currency?: string): Promise<CheckoutData>;
|
|
264
|
+
/**
|
|
265
|
+
* Get checkout session by token without waiting for async completion
|
|
266
|
+
* Useful for manual polling scenarios
|
|
267
|
+
*/
|
|
268
|
+
getCheckoutRaw(checkoutToken: string, currency?: string): Promise<CheckoutData>;
|
|
194
269
|
/**
|
|
195
270
|
* Update checkout address
|
|
196
271
|
*/
|