@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
|
@@ -7,20 +7,105 @@ 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
|
+
* Preload checkout session (ultra-fast background pre-computation) ⚡⚡⚡
|
|
38
|
+
*
|
|
39
|
+
* This is the recommended way to handle cart changes or "Buy Now" intent.
|
|
40
|
+
* It pre-computes everything (checkoutToken, navigation URL, CMS session)
|
|
41
|
+
* before the user even clicks the checkout button.
|
|
42
|
+
*
|
|
43
|
+
* The SDK automatically gets funnelSessionId from FunnelClient if provided.
|
|
44
|
+
* Only FunnelClient knows how to properly extract funnelSessionId (from state, URL, cookies, etc.)
|
|
45
|
+
*
|
|
46
|
+
* @param params - Checkout and funnel parameters
|
|
47
|
+
* @param getFunnelSessionId - Optional function to get funnelSessionId from FunnelClient
|
|
48
|
+
* This maintains separation of concerns - only FunnelClient knows how to get it
|
|
49
|
+
*
|
|
50
|
+
* @returns { checkoutToken, customerId, navigationUrl }
|
|
51
|
+
*/
|
|
52
|
+
async preloadCheckout(params, getFunnelSessionId) {
|
|
53
|
+
// ⚡ GET FUNNEL SESSION ID: Only FunnelClient knows how to properly get it
|
|
54
|
+
// Priority: explicit param > FunnelClient > backend fallback (via currentUrl)
|
|
55
|
+
let funnelSessionId = params.funnelSessionId;
|
|
56
|
+
if (!funnelSessionId && getFunnelSessionId) {
|
|
57
|
+
// Let FunnelClient handle extraction (from state, URL, cookies, etc.)
|
|
58
|
+
funnelSessionId = getFunnelSessionId() || undefined;
|
|
59
|
+
}
|
|
60
|
+
// Format navigationEvent if it's a string
|
|
61
|
+
const navigationEvent = typeof params.navigationEvent === 'string'
|
|
62
|
+
? { type: params.navigationEvent }
|
|
63
|
+
: params.navigationEvent;
|
|
64
|
+
// Build request - backend will also try to extract from currentUrl if not provided
|
|
65
|
+
const requestParams = {
|
|
66
|
+
...params,
|
|
67
|
+
...(funnelSessionId && { funnelSessionId }),
|
|
68
|
+
...(navigationEvent && { navigationEvent }),
|
|
69
|
+
// Ensure currentUrl is always set for backend extraction fallback
|
|
70
|
+
currentUrl: params.currentUrl || (typeof window !== 'undefined' ? window.location.href : undefined),
|
|
71
|
+
};
|
|
72
|
+
return this.apiClient.post('/api/v1/checkout/session/preload', requestParams);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check async checkout processing status (instant, no waiting)
|
|
76
|
+
* Perfect for polling or checking if background job completed
|
|
77
|
+
*/
|
|
78
|
+
async checkAsyncStatus(checkoutToken) {
|
|
79
|
+
return this.apiClient.get(`/api/public/v1/checkout/async-status/${checkoutToken}`);
|
|
80
|
+
}
|
|
16
81
|
/**
|
|
17
82
|
* Get checkout session by token
|
|
83
|
+
*
|
|
84
|
+
* SDK automatically waits for async completion (skipAsyncWait=false) for seamless UX.
|
|
85
|
+
* This ensures async checkout sessions are fully processed before returning data.
|
|
86
|
+
*
|
|
87
|
+
* If you need to skip waiting (e.g., for manual polling), use getCheckoutRaw() instead.
|
|
18
88
|
*/
|
|
19
89
|
async getCheckout(checkoutToken, currency) {
|
|
20
90
|
const queryParams = new URLSearchParams();
|
|
21
91
|
if (currency) {
|
|
22
92
|
queryParams.set('currency', currency);
|
|
23
93
|
}
|
|
94
|
+
// SDK explicitly waits for async completion (default is skip=true for retro compat)
|
|
95
|
+
queryParams.set('skipAsyncWait', 'false');
|
|
96
|
+
const url = `/api/v1/checkout-sessions/${checkoutToken}/v2${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
|
97
|
+
return this.apiClient.get(url);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get checkout session by token without waiting for async completion
|
|
101
|
+
* Useful for manual polling scenarios
|
|
102
|
+
*/
|
|
103
|
+
async getCheckoutRaw(checkoutToken, currency) {
|
|
104
|
+
const queryParams = new URLSearchParams();
|
|
105
|
+
if (currency) {
|
|
106
|
+
queryParams.set('currency', currency);
|
|
107
|
+
}
|
|
108
|
+
queryParams.set('skipAsyncWait', 'true');
|
|
24
109
|
const url = `/api/v1/checkout-sessions/${checkoutToken}/v2${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
|
25
110
|
return this.apiClient.get(url);
|
|
26
111
|
}
|
|
@@ -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
|
|
@@ -421,6 +430,7 @@ export interface SimpleFunnelContext<TCustom = {}> {
|
|
|
421
430
|
* For backward compatibility and flexible unstructured data
|
|
422
431
|
*/
|
|
423
432
|
metadata?: Record<string, any>;
|
|
433
|
+
script?: string;
|
|
424
434
|
}
|
|
425
435
|
export interface FunnelInitializeRequest {
|
|
426
436
|
cmsSession: {
|
|
@@ -480,11 +490,37 @@ export interface FunnelNavigateRequest {
|
|
|
480
490
|
* If session is not found and funnelId is provided, a new session will be created
|
|
481
491
|
*/
|
|
482
492
|
funnelId?: string;
|
|
493
|
+
/**
|
|
494
|
+
* Funnel step ID (from SDK injection/URL params)
|
|
495
|
+
* Used to sync session state before navigation
|
|
496
|
+
*/
|
|
497
|
+
funnelStepId?: string;
|
|
498
|
+
/**
|
|
499
|
+
* Funnel variant ID (from SDK injection/URL params for A/B testing)
|
|
500
|
+
* Used to sync session state before navigation
|
|
501
|
+
*/
|
|
502
|
+
funnelVariantId?: string;
|
|
503
|
+
/**
|
|
504
|
+
* Fire and forget mode - queues navigation to QStash and returns immediately
|
|
505
|
+
* No response data needed, just acknowledgment that request was queued
|
|
506
|
+
* When true, result will only contain queued status and sessionId (no URL or stepId)
|
|
507
|
+
*/
|
|
508
|
+
fireAndForget?: boolean;
|
|
509
|
+
/**
|
|
510
|
+
* ✅ Customer tags to set (merged with existing customer tags)
|
|
511
|
+
* @example ['segment:vip', 'cart_value:high']
|
|
512
|
+
*/
|
|
513
|
+
customerTags?: string[];
|
|
514
|
+
/**
|
|
515
|
+
* ✅ Device ID for geo/device tag enrichment (optional)
|
|
516
|
+
* @example 'dev_abc123xyz'
|
|
517
|
+
*/
|
|
518
|
+
deviceId?: string;
|
|
483
519
|
}
|
|
484
520
|
export interface FunnelNavigateResponse {
|
|
485
521
|
success: boolean;
|
|
486
522
|
result?: {
|
|
487
|
-
stepId
|
|
523
|
+
stepId?: string;
|
|
488
524
|
url?: string;
|
|
489
525
|
/**
|
|
490
526
|
* New session ID if session was recovered (expired/removed)
|
|
@@ -497,6 +533,11 @@ export interface FunnelNavigateResponse {
|
|
|
497
533
|
event: string;
|
|
498
534
|
timestamp: string;
|
|
499
535
|
};
|
|
536
|
+
/**
|
|
537
|
+
* Fire-and-forget response: indicates navigation was queued
|
|
538
|
+
* When present, stepId and url will be undefined
|
|
539
|
+
*/
|
|
540
|
+
queued?: boolean;
|
|
500
541
|
};
|
|
501
542
|
error?: string;
|
|
502
543
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
export interface DeviceInfo {
|
|
2
2
|
userAgent: {
|
|
3
|
+
name: string;
|
|
3
4
|
browser: {
|
|
5
|
+
major: string;
|
|
4
6
|
name: string;
|
|
5
7
|
version: string;
|
|
8
|
+
type?: string;
|
|
6
9
|
};
|
|
7
10
|
os: {
|
|
8
11
|
name: string;
|
|
9
12
|
version: string;
|
|
10
13
|
};
|
|
11
14
|
device?: {
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
model?: string;
|
|
16
|
+
type?: string;
|
|
17
|
+
vendor?: string;
|
|
18
|
+
};
|
|
19
|
+
engine: {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
};
|
|
23
|
+
cpu: {
|
|
24
|
+
architecture: string;
|
|
14
25
|
};
|
|
15
26
|
};
|
|
16
27
|
screenResolution: {
|
|
@@ -18,13 +29,19 @@ export interface DeviceInfo {
|
|
|
18
29
|
height: number;
|
|
19
30
|
};
|
|
20
31
|
timeZone: string;
|
|
32
|
+
flags?: {
|
|
33
|
+
isBot: boolean;
|
|
34
|
+
isChromeFamily: boolean;
|
|
35
|
+
isStandalonePWA: boolean;
|
|
36
|
+
isAppleSilicon: boolean;
|
|
37
|
+
};
|
|
21
38
|
}
|
|
22
39
|
/**
|
|
23
40
|
* Get browser locale
|
|
24
41
|
*/
|
|
25
42
|
export declare function getBrowserLocale(): string;
|
|
26
43
|
/**
|
|
27
|
-
* Collect all device information
|
|
44
|
+
* Collect all device information using UAParser
|
|
28
45
|
*/
|
|
29
46
|
export declare function collectDeviceInfo(): DeviceInfo;
|
|
30
47
|
/**
|