@tagadapay/plugin-sdk 3.0.9 → 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 +3802 -195
- 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 +314 -123
- 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/components/DebugDrawer.js +68 -46
- 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
|
@@ -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
|
}
|
|
@@ -343,16 +343,25 @@ export interface FunnelNavigationAction {
|
|
|
343
343
|
data?: any;
|
|
344
344
|
}
|
|
345
345
|
export interface FunnelNavigationResult {
|
|
346
|
-
stepId
|
|
346
|
+
stepId?: string;
|
|
347
347
|
url?: string;
|
|
348
|
-
action
|
|
349
|
-
context
|
|
348
|
+
action?: FunnelNavigationAction;
|
|
349
|
+
context?: SimpleFunnelContext;
|
|
350
350
|
tracking?: {
|
|
351
351
|
from: string;
|
|
352
352
|
to: string;
|
|
353
353
|
event: string;
|
|
354
354
|
timestamp: string;
|
|
355
355
|
};
|
|
356
|
+
/**
|
|
357
|
+
* Fire-and-forget response: indicates navigation was queued
|
|
358
|
+
* When true, stepId, url, action, and context will be undefined
|
|
359
|
+
*/
|
|
360
|
+
queued?: boolean;
|
|
361
|
+
/**
|
|
362
|
+
* Session ID (may be different if session was recovered)
|
|
363
|
+
*/
|
|
364
|
+
sessionId?: string;
|
|
356
365
|
}
|
|
357
366
|
/**
|
|
358
367
|
* Funnel context available to plugins
|
|
@@ -480,11 +489,37 @@ export interface FunnelNavigateRequest {
|
|
|
480
489
|
* If session is not found and funnelId is provided, a new session will be created
|
|
481
490
|
*/
|
|
482
491
|
funnelId?: string;
|
|
492
|
+
/**
|
|
493
|
+
* Funnel step ID (from SDK injection/URL params)
|
|
494
|
+
* Used to sync session state before navigation
|
|
495
|
+
*/
|
|
496
|
+
funnelStepId?: string;
|
|
497
|
+
/**
|
|
498
|
+
* Funnel variant ID (from SDK injection/URL params for A/B testing)
|
|
499
|
+
* Used to sync session state before navigation
|
|
500
|
+
*/
|
|
501
|
+
funnelVariantId?: string;
|
|
502
|
+
/**
|
|
503
|
+
* Fire and forget mode - queues navigation to QStash and returns immediately
|
|
504
|
+
* No response data needed, just acknowledgment that request was queued
|
|
505
|
+
* When true, result will only contain queued status and sessionId (no URL or stepId)
|
|
506
|
+
*/
|
|
507
|
+
fireAndForget?: boolean;
|
|
508
|
+
/**
|
|
509
|
+
* ✅ Customer tags to set (merged with existing customer tags)
|
|
510
|
+
* @example ['segment:vip', 'cart_value:high']
|
|
511
|
+
*/
|
|
512
|
+
customerTags?: string[];
|
|
513
|
+
/**
|
|
514
|
+
* ✅ Device ID for geo/device tag enrichment (optional)
|
|
515
|
+
* @example 'dev_abc123xyz'
|
|
516
|
+
*/
|
|
517
|
+
deviceId?: string;
|
|
483
518
|
}
|
|
484
519
|
export interface FunnelNavigateResponse {
|
|
485
520
|
success: boolean;
|
|
486
521
|
result?: {
|
|
487
|
-
stepId
|
|
522
|
+
stepId?: string;
|
|
488
523
|
url?: string;
|
|
489
524
|
/**
|
|
490
525
|
* New session ID if session was recovered (expired/removed)
|
|
@@ -497,6 +532,11 @@ export interface FunnelNavigateResponse {
|
|
|
497
532
|
event: string;
|
|
498
533
|
timestamp: string;
|
|
499
534
|
};
|
|
535
|
+
/**
|
|
536
|
+
* Fire-and-forget response: indicates navigation was queued
|
|
537
|
+
* When present, stepId and url will be undefined
|
|
538
|
+
*/
|
|
539
|
+
queued?: boolean;
|
|
500
540
|
};
|
|
501
541
|
error?: string;
|
|
502
542
|
}
|
|
@@ -303,6 +303,32 @@ export declare class OffersResource {
|
|
|
303
303
|
checkoutSessionId?: string;
|
|
304
304
|
customerId?: string;
|
|
305
305
|
}>;
|
|
306
|
+
/**
|
|
307
|
+
* Transform offer to checkout session (async mode) ⚡
|
|
308
|
+
* Response time: ~50ms (20-50x faster!)
|
|
309
|
+
*
|
|
310
|
+
* Returns checkoutToken immediately, background job completes processing.
|
|
311
|
+
* Use getCheckout() to fetch full session data - it auto-waits for completion.
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* // Fast transform
|
|
315
|
+
* const { checkoutToken } = await offers.toCheckoutAsync(offerId, currency, lineItems, returnUrl, mainOrderId);
|
|
316
|
+
*
|
|
317
|
+
* // Redirect user immediately
|
|
318
|
+
* window.location.href = `/checkout/${checkoutToken}/op`;
|
|
319
|
+
*
|
|
320
|
+
* // By the time page loads, background processing is usually complete
|
|
321
|
+
*/
|
|
322
|
+
toCheckoutAsync(offerId: string, currency?: string, lineItems?: Array<{
|
|
323
|
+
lineItemId?: string;
|
|
324
|
+
productId?: string;
|
|
325
|
+
variantId: string;
|
|
326
|
+
quantity: number;
|
|
327
|
+
}>, returnUrl?: string, mainOrderId?: string): Promise<{
|
|
328
|
+
checkoutToken: string;
|
|
329
|
+
customerId: string;
|
|
330
|
+
status: 'processing';
|
|
331
|
+
}>;
|
|
306
332
|
/**
|
|
307
333
|
* @deprecated Use transformToCheckoutSession instead
|
|
308
334
|
* Transform offer to checkout session with dynamic variant selection
|
|
@@ -202,6 +202,43 @@ export class OffersResource {
|
|
|
202
202
|
customerId: response.customerId,
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Transform offer to checkout session (async mode) ⚡
|
|
207
|
+
* Response time: ~50ms (20-50x faster!)
|
|
208
|
+
*
|
|
209
|
+
* Returns checkoutToken immediately, background job completes processing.
|
|
210
|
+
* Use getCheckout() to fetch full session data - it auto-waits for completion.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* // Fast transform
|
|
214
|
+
* const { checkoutToken } = await offers.toCheckoutAsync(offerId, currency, lineItems, returnUrl, mainOrderId);
|
|
215
|
+
*
|
|
216
|
+
* // Redirect user immediately
|
|
217
|
+
* window.location.href = `/checkout/${checkoutToken}/op`;
|
|
218
|
+
*
|
|
219
|
+
* // By the time page loads, background processing is usually complete
|
|
220
|
+
*/
|
|
221
|
+
async toCheckoutAsync(offerId, currency = 'USD', lineItems, returnUrl, mainOrderId) {
|
|
222
|
+
console.log('🛒 [OffersResource] Calling to-checkout-async API:', {
|
|
223
|
+
offerId,
|
|
224
|
+
currency,
|
|
225
|
+
lineItems,
|
|
226
|
+
returnUrl,
|
|
227
|
+
mainOrderId,
|
|
228
|
+
endpoint: `/api/v1/offers/${offerId}/to-checkout-async`,
|
|
229
|
+
});
|
|
230
|
+
const response = await this.apiClient.post(`/api/v1/offers/${offerId}/to-checkout-async`, {
|
|
231
|
+
offerId,
|
|
232
|
+
lineItems: lineItems?.map(item => ({
|
|
233
|
+
variantId: item.variantId,
|
|
234
|
+
quantity: item.quantity,
|
|
235
|
+
})) || [],
|
|
236
|
+
returnUrl: returnUrl || (typeof window !== 'undefined' ? window.location.href : ''),
|
|
237
|
+
mainOrderId: mainOrderId || '',
|
|
238
|
+
});
|
|
239
|
+
console.log('📥 [OffersResource] To-checkout-async API response:', response);
|
|
240
|
+
return response;
|
|
241
|
+
}
|
|
205
242
|
/**
|
|
206
243
|
* @deprecated Use transformToCheckoutSession instead
|
|
207
244
|
* Transform offer to checkout session with dynamic variant selection
|
package/dist/v2/core/types.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export interface ApiConfig {
|
|
|
7
7
|
endpoints: {
|
|
8
8
|
checkout: {
|
|
9
9
|
sessionInit: string;
|
|
10
|
+
sessionInitAsync: string;
|
|
10
11
|
sessionStatus: string;
|
|
12
|
+
asyncStatus: string;
|
|
11
13
|
};
|
|
12
14
|
customer: {
|
|
13
15
|
profile: string;
|
|
@@ -262,7 +264,7 @@ export interface CustomerInfos {
|
|
|
262
264
|
}
|
|
263
265
|
export interface SessionInitResponse {
|
|
264
266
|
store: Store;
|
|
265
|
-
locale
|
|
267
|
+
locale?: string;
|
|
266
268
|
messages?: Record<string, string>;
|
|
267
269
|
customer?: Customer;
|
|
268
270
|
session?: Session;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Domain Auth Handoff Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles automatic resolution of authCode query parameters for seamless
|
|
5
|
+
* cross-domain authentication.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Check URL for authCode parameter
|
|
9
|
+
* 2. If present, resolve it with backend
|
|
10
|
+
* 3. Store the returned token (overrides any existing token)
|
|
11
|
+
* 4. Clean the URL (remove authCode)
|
|
12
|
+
* 5. Return resolved customer and context
|
|
13
|
+
*/
|
|
14
|
+
export interface AuthHandoffResolveResponse {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
token: string;
|
|
17
|
+
customer: {
|
|
18
|
+
id: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
firstName?: string;
|
|
21
|
+
lastName?: string;
|
|
22
|
+
role: 'authenticated' | 'anonymous';
|
|
23
|
+
};
|
|
24
|
+
context: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if authCode is present in URL
|
|
28
|
+
*/
|
|
29
|
+
export declare function hasAuthCode(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Get authCode from URL
|
|
32
|
+
*/
|
|
33
|
+
export declare function getAuthCode(): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Check if a code has already been resolved
|
|
36
|
+
*/
|
|
37
|
+
export declare function isCodeAlreadyResolved(code: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve auth handoff and return token + customer info
|
|
40
|
+
*
|
|
41
|
+
* This function:
|
|
42
|
+
* 1. Calls POST /api/v1/cms/auth/resolve-handoff
|
|
43
|
+
* 2. Stores the returned token (overrides existing)
|
|
44
|
+
* 3. Cleans the URL (removes authCode)
|
|
45
|
+
* 4. Returns customer and context data
|
|
46
|
+
*
|
|
47
|
+
* 🔒 Deduplication: Multiple calls with the same code will return the same promise
|
|
48
|
+
* to prevent duplicate API requests (e.g., React StrictMode double-mounting)
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveAuthHandoff(authCode: string, storeId: string, apiBaseUrl: string, debugMode?: boolean): Promise<AuthHandoffResolveResponse>;
|
|
51
|
+
/**
|
|
52
|
+
* Remove authCode from URL without page reload
|
|
53
|
+
* Uses history.replaceState to update URL cleanly
|
|
54
|
+
*/
|
|
55
|
+
export declare function cleanAuthCodeFromUrl(debugMode?: boolean): void;
|
|
56
|
+
/**
|
|
57
|
+
* Check if we should resolve authCode
|
|
58
|
+
* Returns true if authCode is present and valid format
|
|
59
|
+
*/
|
|
60
|
+
export declare function shouldResolveAuthCode(): boolean;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Domain Auth Handoff Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles automatic resolution of authCode query parameters for seamless
|
|
5
|
+
* cross-domain authentication.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Check URL for authCode parameter
|
|
9
|
+
* 2. If present, resolve it with backend
|
|
10
|
+
* 3. Store the returned token (overrides any existing token)
|
|
11
|
+
* 4. Clean the URL (remove authCode)
|
|
12
|
+
* 5. Return resolved customer and context
|
|
13
|
+
*/
|
|
14
|
+
import { setClientToken } from './tokenStorage';
|
|
15
|
+
// Track in-flight and completed resolutions to prevent duplicates
|
|
16
|
+
const resolutionCache = new Map();
|
|
17
|
+
const resolvedCodes = new Set();
|
|
18
|
+
/**
|
|
19
|
+
* Check if authCode is present in URL
|
|
20
|
+
*/
|
|
21
|
+
export function hasAuthCode() {
|
|
22
|
+
if (typeof window === 'undefined')
|
|
23
|
+
return false;
|
|
24
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
25
|
+
return urlParams.has('authCode');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get authCode from URL
|
|
29
|
+
*/
|
|
30
|
+
export function getAuthCode() {
|
|
31
|
+
if (typeof window === 'undefined')
|
|
32
|
+
return null;
|
|
33
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
34
|
+
return urlParams.get('authCode');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if a code has already been resolved
|
|
38
|
+
*/
|
|
39
|
+
export function isCodeAlreadyResolved(code) {
|
|
40
|
+
return resolvedCodes.has(code);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve auth handoff and return token + customer info
|
|
44
|
+
*
|
|
45
|
+
* This function:
|
|
46
|
+
* 1. Calls POST /api/v1/cms/auth/resolve-handoff
|
|
47
|
+
* 2. Stores the returned token (overrides existing)
|
|
48
|
+
* 3. Cleans the URL (removes authCode)
|
|
49
|
+
* 4. Returns customer and context data
|
|
50
|
+
*
|
|
51
|
+
* 🔒 Deduplication: Multiple calls with the same code will return the same promise
|
|
52
|
+
* to prevent duplicate API requests (e.g., React StrictMode double-mounting)
|
|
53
|
+
*/
|
|
54
|
+
export async function resolveAuthHandoff(authCode, storeId, apiBaseUrl, debugMode = false) {
|
|
55
|
+
// Check if already resolved
|
|
56
|
+
if (resolvedCodes.has(authCode)) {
|
|
57
|
+
if (debugMode) {
|
|
58
|
+
console.log('[AuthHandoff] Code already resolved, skipping duplicate request');
|
|
59
|
+
}
|
|
60
|
+
throw new Error('Auth code already resolved');
|
|
61
|
+
}
|
|
62
|
+
// Check if resolution is in-flight
|
|
63
|
+
const inFlightResolution = resolutionCache.get(authCode);
|
|
64
|
+
if (inFlightResolution) {
|
|
65
|
+
if (debugMode) {
|
|
66
|
+
console.log('[AuthHandoff] Resolution already in progress, waiting for existing request');
|
|
67
|
+
}
|
|
68
|
+
return inFlightResolution;
|
|
69
|
+
}
|
|
70
|
+
if (debugMode) {
|
|
71
|
+
console.log('[AuthHandoff] Resolving authCode:', authCode.substring(0, 15) + '...');
|
|
72
|
+
}
|
|
73
|
+
// Create resolution promise
|
|
74
|
+
const resolutionPromise = (async () => {
|
|
75
|
+
try {
|
|
76
|
+
// Call resolve endpoint (no authentication required)
|
|
77
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/cms/auth/resolve-handoff`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
code: authCode,
|
|
84
|
+
storeId,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
|
|
89
|
+
throw new Error(errorData.message || `Failed to resolve auth handoff: ${response.status}`);
|
|
90
|
+
}
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
if (debugMode) {
|
|
93
|
+
console.log('[AuthHandoff] ✅ Resolved successfully:', {
|
|
94
|
+
customerId: data.customer.id,
|
|
95
|
+
role: data.customer.role,
|
|
96
|
+
hasContext: Object.keys(data.context).length > 0,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Store token (overrides any existing token)
|
|
100
|
+
if (debugMode) {
|
|
101
|
+
console.log('[AuthHandoff] Storing new token (overriding existing)');
|
|
102
|
+
}
|
|
103
|
+
setClientToken(data.token);
|
|
104
|
+
// Clean URL (remove authCode parameter)
|
|
105
|
+
cleanAuthCodeFromUrl(debugMode);
|
|
106
|
+
// Mark as resolved
|
|
107
|
+
resolvedCodes.add(authCode);
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error('[AuthHandoff] ❌ Failed to resolve:', error);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
// Remove from in-flight cache after completion (success or failure)
|
|
116
|
+
resolutionCache.delete(authCode);
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
// Cache the in-flight promise
|
|
120
|
+
resolutionCache.set(authCode, resolutionPromise);
|
|
121
|
+
return resolutionPromise;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Remove authCode from URL without page reload
|
|
125
|
+
* Uses history.replaceState to update URL cleanly
|
|
126
|
+
*/
|
|
127
|
+
export function cleanAuthCodeFromUrl(debugMode = false) {
|
|
128
|
+
if (typeof window === 'undefined')
|
|
129
|
+
return;
|
|
130
|
+
const url = new URL(window.location.href);
|
|
131
|
+
if (url.searchParams.has('authCode')) {
|
|
132
|
+
url.searchParams.delete('authCode');
|
|
133
|
+
// Use replaceState to update URL without reload
|
|
134
|
+
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
135
|
+
if (debugMode) {
|
|
136
|
+
console.log('[AuthHandoff] Cleaned authCode from URL');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Check if we should resolve authCode
|
|
142
|
+
* Returns true if authCode is present and valid format
|
|
143
|
+
*/
|
|
144
|
+
export function shouldResolveAuthCode() {
|
|
145
|
+
const authCode = getAuthCode();
|
|
146
|
+
if (!authCode || !authCode.startsWith('ah_')) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
// Don't resolve if already resolved
|
|
150
|
+
if (resolvedCodes.has(authCode)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|