@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
|
@@ -1,91 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
4
|
-
function getBrowserInfo() {
|
|
5
|
-
const userAgent = navigator.userAgent;
|
|
6
|
-
// Chrome
|
|
7
|
-
if (userAgent.includes('Chrome')) {
|
|
8
|
-
const match = /Chrome\/(\d+)/.exec(userAgent);
|
|
9
|
-
return { name: 'Chrome', version: match ? match[1] : 'unknown' };
|
|
10
|
-
}
|
|
11
|
-
// Firefox
|
|
12
|
-
if (userAgent.includes('Firefox')) {
|
|
13
|
-
const match = /Firefox\/(\d+)/.exec(userAgent);
|
|
14
|
-
return { name: 'Firefox', version: match ? match[1] : 'unknown' };
|
|
15
|
-
}
|
|
16
|
-
// Safari
|
|
17
|
-
if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) {
|
|
18
|
-
const match = /Version\/(\d+)/.exec(userAgent);
|
|
19
|
-
return { name: 'Safari', version: match ? match[1] : 'unknown' };
|
|
20
|
-
}
|
|
21
|
-
// Edge
|
|
22
|
-
if (userAgent.includes('Edge')) {
|
|
23
|
-
const match = /Edge\/(\d+)/.exec(userAgent);
|
|
24
|
-
return { name: 'Edge', version: match ? match[1] : 'unknown' };
|
|
25
|
-
}
|
|
26
|
-
return { name: 'unknown', version: 'unknown' };
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Get basic OS information from user agent
|
|
30
|
-
*/
|
|
31
|
-
function getOSInfo() {
|
|
32
|
-
const userAgent = navigator.userAgent;
|
|
33
|
-
// Windows
|
|
34
|
-
if (userAgent.includes('Windows')) {
|
|
35
|
-
if (userAgent.includes('Windows NT 10.0'))
|
|
36
|
-
return { name: 'Windows', version: '10' };
|
|
37
|
-
if (userAgent.includes('Windows NT 6.3'))
|
|
38
|
-
return { name: 'Windows', version: '8.1' };
|
|
39
|
-
if (userAgent.includes('Windows NT 6.2'))
|
|
40
|
-
return { name: 'Windows', version: '8' };
|
|
41
|
-
if (userAgent.includes('Windows NT 6.1'))
|
|
42
|
-
return { name: 'Windows', version: '7' };
|
|
43
|
-
return { name: 'Windows', version: 'unknown' };
|
|
44
|
-
}
|
|
45
|
-
// macOS
|
|
46
|
-
if (userAgent.includes('Mac OS X')) {
|
|
47
|
-
const match = /Mac OS X (\d+[._]\d+)/.exec(userAgent);
|
|
48
|
-
return { name: 'macOS', version: match ? match[1].replace('_', '.') : 'unknown' };
|
|
49
|
-
}
|
|
50
|
-
// iOS
|
|
51
|
-
if (userAgent.includes('iPhone') || userAgent.includes('iPad')) {
|
|
52
|
-
const match = /OS (\d+[._]\d+)/.exec(userAgent);
|
|
53
|
-
return { name: 'iOS', version: match ? match[1].replace('_', '.') : 'unknown' };
|
|
54
|
-
}
|
|
55
|
-
// Android
|
|
56
|
-
if (userAgent.includes('Android')) {
|
|
57
|
-
const match = /Android (\d+[.\d]*)/.exec(userAgent);
|
|
58
|
-
return { name: 'Android', version: match ? match[1] : 'unknown' };
|
|
59
|
-
}
|
|
60
|
-
// Linux
|
|
61
|
-
if (userAgent.includes('Linux')) {
|
|
62
|
-
return { name: 'Linux', version: 'unknown' };
|
|
63
|
-
}
|
|
64
|
-
return { name: 'unknown', version: 'unknown' };
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Get device information
|
|
68
|
-
*/
|
|
69
|
-
function getDeviceInfo() {
|
|
70
|
-
const userAgent = navigator.userAgent;
|
|
71
|
-
// Mobile devices
|
|
72
|
-
if (userAgent.includes('iPhone')) {
|
|
73
|
-
return { type: 'mobile', model: 'iPhone' };
|
|
74
|
-
}
|
|
75
|
-
if (userAgent.includes('iPad')) {
|
|
76
|
-
return { type: 'tablet', model: 'iPad' };
|
|
77
|
-
}
|
|
78
|
-
if (userAgent.includes('Android')) {
|
|
79
|
-
if (userAgent.includes('Mobile')) {
|
|
80
|
-
return { type: 'mobile', model: 'Android' };
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
return { type: 'tablet', model: 'Android' };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// Desktop (no specific device info)
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
1
|
+
import { UAParser } from '@ua-parser-js/pro-enterprise';
|
|
2
|
+
import { isBot, isChromeFamily, isStandalonePWA, isAppleSilicon, } from '@ua-parser-js/pro-enterprise/helpers';
|
|
89
3
|
/**
|
|
90
4
|
* Get screen resolution
|
|
91
5
|
*/
|
|
@@ -120,28 +34,82 @@ export function getBrowserLocale() {
|
|
|
120
34
|
}
|
|
121
35
|
}
|
|
122
36
|
/**
|
|
123
|
-
* Collect all device information
|
|
37
|
+
* Collect all device information using UAParser
|
|
124
38
|
*/
|
|
125
39
|
export function collectDeviceInfo() {
|
|
126
40
|
if (typeof window === 'undefined') {
|
|
127
41
|
// Server-side fallback
|
|
128
42
|
return {
|
|
129
43
|
userAgent: {
|
|
130
|
-
|
|
131
|
-
|
|
44
|
+
name: '',
|
|
45
|
+
browser: { major: '', name: '', version: '' },
|
|
46
|
+
os: { name: '', version: '' },
|
|
47
|
+
device: undefined,
|
|
48
|
+
engine: { name: '', version: '' },
|
|
49
|
+
cpu: { architecture: '' },
|
|
132
50
|
},
|
|
133
51
|
screenResolution: { width: 0, height: 0 },
|
|
134
52
|
timeZone: 'UTC',
|
|
53
|
+
flags: {
|
|
54
|
+
isBot: false,
|
|
55
|
+
isChromeFamily: false,
|
|
56
|
+
isStandalonePWA: false,
|
|
57
|
+
isAppleSilicon: false,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const parser = new UAParser();
|
|
62
|
+
const result = parser.getResult();
|
|
63
|
+
// Enhanced detection using UAParser official helpers
|
|
64
|
+
let flags;
|
|
65
|
+
try {
|
|
66
|
+
flags = {
|
|
67
|
+
isBot: isBot(result),
|
|
68
|
+
isChromeFamily: isChromeFamily(result),
|
|
69
|
+
isStandalonePWA: isStandalonePWA(),
|
|
70
|
+
isAppleSilicon: isAppleSilicon(result),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error('Failed to compute device flags:', error);
|
|
75
|
+
flags = {
|
|
76
|
+
isBot: false,
|
|
77
|
+
isChromeFamily: false,
|
|
78
|
+
isStandalonePWA: false,
|
|
79
|
+
isAppleSilicon: false,
|
|
135
80
|
};
|
|
136
81
|
}
|
|
137
82
|
return {
|
|
138
83
|
userAgent: {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
84
|
+
name: result.ua,
|
|
85
|
+
browser: {
|
|
86
|
+
major: result.browser.major || '',
|
|
87
|
+
name: result.browser.name || '',
|
|
88
|
+
version: result.browser.version || '',
|
|
89
|
+
type: result.browser.type,
|
|
90
|
+
},
|
|
91
|
+
os: {
|
|
92
|
+
name: result.os.name || '',
|
|
93
|
+
version: result.os.version || '',
|
|
94
|
+
},
|
|
95
|
+
device: result.device.model || result.device.type || result.device.vendor
|
|
96
|
+
? {
|
|
97
|
+
model: result.device.model,
|
|
98
|
+
type: result.device.type,
|
|
99
|
+
vendor: result.device.vendor,
|
|
100
|
+
}
|
|
101
|
+
: undefined,
|
|
102
|
+
engine: {
|
|
103
|
+
name: result.engine.name || '',
|
|
104
|
+
version: result.engine.version || '',
|
|
105
|
+
},
|
|
106
|
+
cpu: {
|
|
107
|
+
architecture: result.cpu.architecture || '',
|
|
108
|
+
},
|
|
142
109
|
},
|
|
143
110
|
screenResolution: getScreenResolution(),
|
|
144
111
|
timeZone: getTimeZone(),
|
|
112
|
+
flags,
|
|
145
113
|
};
|
|
146
114
|
}
|
|
147
115
|
/**
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* - token: Authentication token (URL > localStorage)
|
|
14
14
|
* - funnelSessionId: Active funnel session (URL > cookie)
|
|
15
15
|
*
|
|
16
|
+
* ⚠️ Note: authCode is NOT handled here - it has highest priority and is handled
|
|
17
|
+
* separately in client.ts before all other initialization logic.
|
|
18
|
+
*
|
|
16
19
|
* Usage examples:
|
|
17
20
|
* - Force production API: ?tagadaClientEnv=production
|
|
18
21
|
* - Force development API: ?tagadaClientEnv=development
|
|
@@ -20,6 +23,7 @@
|
|
|
20
23
|
* - Custom API URL: ?tagadaClientBaseUrl=https://tagada.loclx.io
|
|
21
24
|
* - Combined: ?tagadaClientEnv=local&tagadaClientBaseUrl=https://tagada.loclx.io
|
|
22
25
|
* - Hard reset + production: ?forceReset=true&tagadaClientEnv=production
|
|
26
|
+
* - Cross-domain auth: ?authCode=ah_... (automatically handled, highest priority)
|
|
23
27
|
*/
|
|
24
28
|
/**
|
|
25
29
|
* SDK Override Parameters - centralized across all SDK functions
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* - token: Authentication token (URL > localStorage)
|
|
14
14
|
* - funnelSessionId: Active funnel session (URL > cookie)
|
|
15
15
|
*
|
|
16
|
+
* ⚠️ Note: authCode is NOT handled here - it has highest priority and is handled
|
|
17
|
+
* separately in client.ts before all other initialization logic.
|
|
18
|
+
*
|
|
16
19
|
* Usage examples:
|
|
17
20
|
* - Force production API: ?tagadaClientEnv=production
|
|
18
21
|
* - Force development API: ?tagadaClientEnv=development
|
|
@@ -20,6 +23,7 @@
|
|
|
20
23
|
* - Custom API URL: ?tagadaClientBaseUrl=https://tagada.loclx.io
|
|
21
24
|
* - Combined: ?tagadaClientEnv=local&tagadaClientBaseUrl=https://tagada.loclx.io
|
|
22
25
|
* - Hard reset + production: ?forceReset=true&tagadaClientEnv=production
|
|
26
|
+
* - Cross-domain auth: ?authCode=ah_... (automatically handled, highest priority)
|
|
23
27
|
*/
|
|
24
28
|
import { clearClientToken, setClientToken, getClientToken } from './tokenStorage';
|
|
25
29
|
import { clearFunnelSessionCookie } from './sessionStorage';
|
|
@@ -87,9 +87,9 @@ export function useCheckoutQuery(options = {}) {
|
|
|
87
87
|
await refetch();
|
|
88
88
|
}
|
|
89
89
|
}, [refetch, checkoutToken]);
|
|
90
|
-
// Initialize checkout mutation
|
|
90
|
+
// Initialize checkout mutation (async mode for fast response)
|
|
91
91
|
const initMutation = useMutation({
|
|
92
|
-
mutationFn: (params) => {
|
|
92
|
+
mutationFn: async (params) => {
|
|
93
93
|
const requestBody = {
|
|
94
94
|
...params,
|
|
95
95
|
storeId: params.storeId || storeId,
|
|
@@ -101,7 +101,12 @@ export function useCheckoutQuery(options = {}) {
|
|
|
101
101
|
currency: params.customer?.currency ?? currency.code,
|
|
102
102
|
},
|
|
103
103
|
};
|
|
104
|
-
|
|
104
|
+
// Use async mode for fast response (~50ms vs 2-5s)
|
|
105
|
+
const asyncResponse = await checkoutResource.initCheckoutAsync(requestBody);
|
|
106
|
+
return {
|
|
107
|
+
checkoutToken: asyncResponse.checkoutToken,
|
|
108
|
+
checkoutSession: {}, // Will be populated when getCheckout is called
|
|
109
|
+
};
|
|
105
110
|
},
|
|
106
111
|
onSuccess: (response) => {
|
|
107
112
|
// Update URL with checkout token
|
|
@@ -257,9 +262,12 @@ export function useCheckoutQuery(options = {}) {
|
|
|
257
262
|
await waitForSession();
|
|
258
263
|
const result = await initMutation.mutateAsync(params);
|
|
259
264
|
// Update internal token state so the query can fetch the checkout data
|
|
265
|
+
// The query will automatically refetch when token changes, and getCheckout()
|
|
266
|
+
// will automatically wait for async completion (via SDK skipAsyncWait=false)
|
|
260
267
|
setInternalToken(result.checkoutToken);
|
|
268
|
+
// Return immediately with token
|
|
269
|
+
// checkoutSession will be populated by the query once background processing completes
|
|
261
270
|
return {
|
|
262
|
-
checkoutUrl: result.checkoutUrl,
|
|
263
271
|
checkoutSession: checkout?.checkoutSession ?? {},
|
|
264
272
|
checkoutToken: result.checkoutToken,
|
|
265
273
|
};
|
|
@@ -339,12 +339,31 @@ export function useFunnel(options = {}) {
|
|
|
339
339
|
onSuccess: (result) => {
|
|
340
340
|
if (!context)
|
|
341
341
|
return;
|
|
342
|
+
// 🔥 Fire-and-forget mode: Just acknowledge, no navigation
|
|
343
|
+
if (result.queued) {
|
|
344
|
+
// Update session ID if changed
|
|
345
|
+
if (result.sessionId && result.sessionId !== context.sessionId) {
|
|
346
|
+
const newContext = {
|
|
347
|
+
...context,
|
|
348
|
+
sessionId: result.sessionId,
|
|
349
|
+
};
|
|
350
|
+
const enrichedContext = enrichContext(newContext);
|
|
351
|
+
setContext(enrichedContext);
|
|
352
|
+
document.cookie = `funnelSessionId=${result.sessionId}; path=/; max-age=86400; SameSite=Lax`;
|
|
353
|
+
}
|
|
354
|
+
return; // Early return for fire-and-forget
|
|
355
|
+
}
|
|
342
356
|
// 🔄 Handle session recovery (if backend created a new session)
|
|
343
357
|
let recoveredSessionId;
|
|
344
358
|
if (result.sessionId && result.sessionId !== context.sessionId) {
|
|
345
359
|
console.warn(`🔄 Funnel: Session recovered! Old: ${context.sessionId}, New: ${result.sessionId}`);
|
|
346
360
|
recoveredSessionId = result.sessionId;
|
|
347
361
|
}
|
|
362
|
+
// Validate required fields for normal navigation
|
|
363
|
+
if (!result.stepId) {
|
|
364
|
+
console.warn('Funnel: Navigation result missing stepId');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
348
367
|
// Update local context
|
|
349
368
|
const newContext = {
|
|
350
369
|
...context,
|
|
@@ -366,13 +385,13 @@ export function useFunnel(options = {}) {
|
|
|
366
385
|
document.cookie = `funnelSessionId=${recoveredSessionId}; path=/; max-age=86400; SameSite=Lax`;
|
|
367
386
|
console.log(`🍪 Funnel: Updated cookie with recovered session ID: ${recoveredSessionId}`);
|
|
368
387
|
}
|
|
369
|
-
// Create typed navigation result
|
|
388
|
+
// Create typed navigation result (only if we have URL)
|
|
370
389
|
const navigationResult = {
|
|
371
390
|
stepId: result.stepId,
|
|
372
|
-
action: {
|
|
391
|
+
action: result.url ? {
|
|
373
392
|
type: 'redirect', // Default action type
|
|
374
393
|
url: result.url
|
|
375
|
-
},
|
|
394
|
+
} : undefined,
|
|
376
395
|
context: enrichedContext,
|
|
377
396
|
tracking: result.tracking
|
|
378
397
|
};
|
|
@@ -384,18 +403,27 @@ export function useFunnel(options = {}) {
|
|
|
384
403
|
shouldPerformDefaultNavigation = false;
|
|
385
404
|
}
|
|
386
405
|
}
|
|
387
|
-
// Perform default navigation if not overridden
|
|
388
|
-
if (shouldPerformDefaultNavigation && navigationResult.action
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
406
|
+
// Perform default navigation if not overridden and we have an action with URL
|
|
407
|
+
if (shouldPerformDefaultNavigation && navigationResult.action?.url) {
|
|
408
|
+
const action = navigationResult.action; // Type narrowing
|
|
409
|
+
const actionUrl = action.url; // Extract URL for type narrowing
|
|
410
|
+
if (actionUrl) {
|
|
411
|
+
// Add URL parameters for cross-domain session continuity
|
|
412
|
+
const urlWithParams = addSessionParams(actionUrl, enrichedContext.sessionId, effectiveFunnelId || options.funnelId);
|
|
413
|
+
const updatedAction = {
|
|
414
|
+
type: action.type || 'redirect', // Ensure type is defined
|
|
415
|
+
url: urlWithParams,
|
|
416
|
+
data: action.data,
|
|
417
|
+
};
|
|
418
|
+
performNavigation(updatedAction);
|
|
419
|
+
}
|
|
393
420
|
}
|
|
394
421
|
// Skip background refreshes if we are navigating away (full page reload)
|
|
395
422
|
// This prevents "lingering" requests from the old page context
|
|
423
|
+
const action = navigationResult.action;
|
|
396
424
|
const isFullNavigation = shouldPerformDefaultNavigation &&
|
|
397
|
-
|
|
398
|
-
(
|
|
425
|
+
action?.url &&
|
|
426
|
+
(action.type === 'redirect' || action.type === 'replace');
|
|
399
427
|
if (!isFullNavigation) {
|
|
400
428
|
// Fetch debug data if in debug mode
|
|
401
429
|
if (debugMode) {
|
|
@@ -76,9 +76,9 @@ export interface UsePreviewOfferResult {
|
|
|
76
76
|
checkoutUrl: string;
|
|
77
77
|
}>;
|
|
78
78
|
toCheckout: (mainOrderId?: string) => Promise<{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
checkoutToken: string;
|
|
80
|
+
customerId: string;
|
|
81
|
+
status: 'processing';
|
|
82
82
|
}>;
|
|
83
83
|
}
|
|
84
84
|
export declare function usePreviewOffer(options: UsePreviewOfferOptions): UsePreviewOfferResult;
|
|
@@ -7,12 +7,16 @@
|
|
|
7
7
|
* 3. Recalculating totals on-the-fly without backend calls
|
|
8
8
|
* 4. NO checkout session creation - perfect for browsing/previewing
|
|
9
9
|
*/
|
|
10
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
10
|
import { useQuery } from '@tanstack/react-query';
|
|
11
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
12
12
|
import { OffersResource } from '../../core/resources/offers';
|
|
13
13
|
import { getGlobalApiClient } from './useApiQuery';
|
|
14
14
|
export function usePreviewOffer(options) {
|
|
15
15
|
const { offerId, currency: requestedCurrency = 'USD', initialSelections = {} } = options;
|
|
16
|
+
const queryParamsCurrency = typeof window !== 'undefined'
|
|
17
|
+
? new URLSearchParams(window.location.search).get('currency') || undefined
|
|
18
|
+
: undefined;
|
|
19
|
+
const effectiveCurrency = queryParamsCurrency || requestedCurrency;
|
|
16
20
|
const offersResource = useMemo(() => new OffersResource(getGlobalApiClient()), []);
|
|
17
21
|
// Track user selections (variant + quantity per product)
|
|
18
22
|
const [selections, setSelections] = useState(initialSelections);
|
|
@@ -35,14 +39,14 @@ export function usePreviewOffer(options) {
|
|
|
35
39
|
}, [selections]);
|
|
36
40
|
// 🎯 ONE API CALL - Fetch offer + preview in one request
|
|
37
41
|
const { data, isLoading, isFetching, error } = useQuery({
|
|
38
|
-
queryKey: ['offer-preview', offerId,
|
|
42
|
+
queryKey: ['offer-preview', offerId, effectiveCurrency, lineItemsForPreview],
|
|
39
43
|
queryFn: async () => {
|
|
40
44
|
console.log('🔍 [usePreviewOffer] Fetching preview:', {
|
|
41
45
|
offerId,
|
|
42
|
-
currency:
|
|
46
|
+
currency: effectiveCurrency,
|
|
43
47
|
lineItems: lineItemsForPreview,
|
|
44
48
|
});
|
|
45
|
-
const result = await offersResource.previewOffer(offerId,
|
|
49
|
+
const result = await offersResource.previewOffer(offerId, effectiveCurrency, lineItemsForPreview);
|
|
46
50
|
console.log('✅ [usePreviewOffer] Preview result:', result);
|
|
47
51
|
// Extract offer and preview from unified response
|
|
48
52
|
const offer = result.offer || null;
|
|
@@ -66,7 +70,7 @@ export function usePreviewOffer(options) {
|
|
|
66
70
|
totalAmount: preview.totalAmount || 0,
|
|
67
71
|
totalAdjustedAmount: preview.totalAdjustedAmount || 0,
|
|
68
72
|
totalPromotionAmount: preview.totalPromotionAmount || 0,
|
|
69
|
-
currency: preview.currency ||
|
|
73
|
+
currency: preview.currency || effectiveCurrency,
|
|
70
74
|
options: preview.options || {},
|
|
71
75
|
};
|
|
72
76
|
return { offer, summary };
|
|
@@ -200,7 +204,7 @@ export function usePreviewOffer(options) {
|
|
|
200
204
|
return [];
|
|
201
205
|
const currentItem = summary.items.find((i) => i.productId === productId);
|
|
202
206
|
const activePriceId = currentItem?.priceId;
|
|
203
|
-
const currency = summary.currency ||
|
|
207
|
+
const currency = summary.currency || effectiveCurrency;
|
|
204
208
|
return summary.options[productId].map((variant) => {
|
|
205
209
|
// Find matching price or use first
|
|
206
210
|
let unitAmount = 0;
|
|
@@ -222,7 +226,7 @@ export function usePreviewOffer(options) {
|
|
|
222
226
|
currency,
|
|
223
227
|
};
|
|
224
228
|
});
|
|
225
|
-
}, [summary,
|
|
229
|
+
}, [summary, effectiveCurrency]);
|
|
226
230
|
// Pay for the offer with current selections
|
|
227
231
|
const pay = useCallback(async (mainOrderId) => {
|
|
228
232
|
if (isPaying) {
|
|
@@ -230,7 +234,7 @@ export function usePreviewOffer(options) {
|
|
|
230
234
|
}
|
|
231
235
|
setIsPaying(true);
|
|
232
236
|
try {
|
|
233
|
-
const result = await offersResource.payPreviewedOffer(offerId,
|
|
237
|
+
const result = await offersResource.payPreviewedOffer(offerId, effectiveCurrency, lineItemsForPreview, typeof window !== 'undefined' ? window.location.href : undefined, mainOrderId);
|
|
234
238
|
console.log('[usePreviewOffer] Payment initiated:', result);
|
|
235
239
|
return {
|
|
236
240
|
checkoutUrl: result.checkout.checkoutUrl,
|
|
@@ -243,20 +247,21 @@ export function usePreviewOffer(options) {
|
|
|
243
247
|
finally {
|
|
244
248
|
setIsPaying(false);
|
|
245
249
|
}
|
|
246
|
-
}, [offerId,
|
|
247
|
-
// Create checkout session without paying (for landing pages)
|
|
250
|
+
}, [offerId, effectiveCurrency, lineItemsForPreview, offersResource, isPaying]);
|
|
251
|
+
// Create checkout session without paying (for landing pages) - async mode for fast response
|
|
248
252
|
const toCheckout = useCallback(async (mainOrderId) => {
|
|
249
253
|
if (isPaying) {
|
|
250
254
|
throw new Error('Operation already in progress');
|
|
251
255
|
}
|
|
252
256
|
setIsPaying(true);
|
|
253
257
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
// Use async mode for fast response (~50ms vs 2-5s)
|
|
259
|
+
const result = await offersResource.toCheckoutAsync(offerId, effectiveCurrency, lineItemsForPreview, typeof window !== 'undefined' ? window.location.href : undefined, mainOrderId);
|
|
260
|
+
console.log('[usePreviewOffer] Checkout session created (async):', result);
|
|
256
261
|
return {
|
|
257
|
-
checkoutSessionId: result.checkoutSessionId,
|
|
258
262
|
checkoutToken: result.checkoutToken,
|
|
259
|
-
|
|
263
|
+
customerId: result.customerId,
|
|
264
|
+
status: result.status,
|
|
260
265
|
};
|
|
261
266
|
}
|
|
262
267
|
catch (error) {
|
|
@@ -266,7 +271,7 @@ export function usePreviewOffer(options) {
|
|
|
266
271
|
finally {
|
|
267
272
|
setIsPaying(false);
|
|
268
273
|
}
|
|
269
|
-
}, [offerId,
|
|
274
|
+
}, [offerId, effectiveCurrency, lineItemsForPreview, offersResource, isPaying]);
|
|
270
275
|
return {
|
|
271
276
|
offer,
|
|
272
277
|
isLoading,
|
|
@@ -54,19 +54,27 @@ export const useTranslation = (options = {}) => {
|
|
|
54
54
|
const { defaultLanguage = 'en', currentLanguage } = options;
|
|
55
55
|
// Get the current language from query params, browser, or fallback to default
|
|
56
56
|
const locale = useMemo(() => {
|
|
57
|
+
// Normalizes language codes like 'fr-FR' or 'pt_BR' to just 'fr' / 'pt'
|
|
58
|
+
const normalizeLanguageCode = (code) => {
|
|
59
|
+
if (!code)
|
|
60
|
+
return undefined;
|
|
61
|
+
return code.split(/[-_]/)[0].toLowerCase();
|
|
62
|
+
};
|
|
57
63
|
if (currentLanguage)
|
|
58
64
|
return currentLanguage;
|
|
59
65
|
// Check for language query parameter
|
|
60
66
|
if (typeof window !== 'undefined') {
|
|
61
67
|
const urlParams = new URLSearchParams(window.location.search);
|
|
62
68
|
const langFromQuery = urlParams.get('locale');
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
const normalizedFromQuery = normalizeLanguageCode(langFromQuery);
|
|
70
|
+
if (normalizedFromQuery)
|
|
71
|
+
return normalizedFromQuery;
|
|
65
72
|
}
|
|
66
73
|
// Try to get browser language
|
|
67
74
|
if (typeof navigator !== 'undefined') {
|
|
68
|
-
const browserLang = navigator.language
|
|
69
|
-
|
|
75
|
+
const browserLang = normalizeLanguageCode(navigator.language); // e.g., 'en-US' -> 'en'
|
|
76
|
+
if (browserLang)
|
|
77
|
+
return browserLang;
|
|
70
78
|
}
|
|
71
79
|
return defaultLanguage;
|
|
72
80
|
}, [defaultLanguage, currentLanguage]);
|
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
* TagadaProvider - Main provider component for the Tagada Pay React SDK
|
|
5
5
|
*/
|
|
6
6
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
7
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
|
|
7
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
8
8
|
import { ApiService } from '../../../react/services/apiService';
|
|
9
9
|
import { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits, } from '../../../react/utils/money';
|
|
10
10
|
import { TagadaClient } from '../../core/client';
|
|
@@ -211,6 +211,8 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
|
|
|
211
211
|
formatSimpleMoney,
|
|
212
212
|
}), []);
|
|
213
213
|
const [isDebugDrawerOpen, setIsDebugDrawerOpen] = useState(false);
|
|
214
|
+
// Track last injected script to prevent duplicate execution
|
|
215
|
+
const lastInjectedScriptRef = useRef(null);
|
|
214
216
|
// Funnel Methods
|
|
215
217
|
const funnelMethods = useMemo(() => {
|
|
216
218
|
if (!client.funnel) {
|
|
@@ -283,6 +285,63 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
|
|
|
283
285
|
},
|
|
284
286
|
};
|
|
285
287
|
}, [client, state.auth.session, state.store, funnelId, onNavigate]);
|
|
288
|
+
// Inject funnel script into the page
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
// Only run in browser environment
|
|
291
|
+
if (typeof document === 'undefined') {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const scriptContent = funnelState.context?.script;
|
|
295
|
+
const scriptId = 'tagada-funnel-script';
|
|
296
|
+
if (!scriptContent || !scriptContent.trim()) {
|
|
297
|
+
// Clear ref if script is removed
|
|
298
|
+
lastInjectedScriptRef.current = null;
|
|
299
|
+
// Remove existing script if it exists
|
|
300
|
+
const existingScript = document.getElementById(scriptId);
|
|
301
|
+
if (existingScript) {
|
|
302
|
+
existingScript.remove();
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Extract script content (remove <script> tags if present)
|
|
307
|
+
let scriptBody = scriptContent.trim();
|
|
308
|
+
// Check if script is wrapped in <script> tags
|
|
309
|
+
const scriptTagMatch = scriptBody.match(/^<script[^>]*>([\s\S]*)<\/script>$/i);
|
|
310
|
+
if (scriptTagMatch) {
|
|
311
|
+
scriptBody = scriptTagMatch[1].trim();
|
|
312
|
+
}
|
|
313
|
+
// Skip if script body is empty after extraction
|
|
314
|
+
if (!scriptBody) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// Prevent duplicate injection of the same script content
|
|
318
|
+
// This handles React StrictMode double-execution in development
|
|
319
|
+
if (lastInjectedScriptRef.current === scriptBody) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Remove existing script if it exists (for script updates)
|
|
323
|
+
const existingScript = document.getElementById(scriptId);
|
|
324
|
+
if (existingScript) {
|
|
325
|
+
existingScript.remove();
|
|
326
|
+
}
|
|
327
|
+
// Create and inject new script element
|
|
328
|
+
const scriptElement = document.createElement('script');
|
|
329
|
+
scriptElement.id = scriptId;
|
|
330
|
+
scriptElement.textContent = scriptBody;
|
|
331
|
+
document.body.appendChild(scriptElement);
|
|
332
|
+
// Track this script content to prevent re-injection (handles React StrictMode double-execution)
|
|
333
|
+
lastInjectedScriptRef.current = scriptBody;
|
|
334
|
+
// Cleanup: remove script element but keep ref to prevent re-injection on StrictMode second run
|
|
335
|
+
return () => {
|
|
336
|
+
const scriptToRemove = document.getElementById(scriptId);
|
|
337
|
+
if (scriptToRemove) {
|
|
338
|
+
scriptToRemove.remove();
|
|
339
|
+
}
|
|
340
|
+
// Note: We intentionally DON'T clear lastInjectedScriptRef here
|
|
341
|
+
// This prevents React StrictMode from re-injecting the same script on the second run
|
|
342
|
+
// The ref will be cleared when script content actually changes (next effect run)
|
|
343
|
+
};
|
|
344
|
+
}, [funnelState.context?.script]);
|
|
286
345
|
const contextValue = {
|
|
287
346
|
client,
|
|
288
347
|
...state,
|
|
@@ -301,6 +360,7 @@ export function TagadaProvider({ children, environment, customApiConfig, debugMo
|
|
|
301
360
|
refreshCoordinator,
|
|
302
361
|
money: moneyUtils,
|
|
303
362
|
};
|
|
363
|
+
console.log('contextValue', contextValue, contextValue.funnel.currentStep);
|
|
304
364
|
// Query Client
|
|
305
365
|
const [queryClient] = useState(() => new QueryClient({
|
|
306
366
|
defaultOptions: {
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { TagadaClient, TagadaClientConfig, TagadaState } from '../core/client';
|
|
10
10
|
import { ApiClient } from '../core/resources/apiClient';
|
|
11
|
+
import { CheckoutResource } from '../core/resources/checkout';
|
|
11
12
|
/**
|
|
12
13
|
* Factory function to create a Tagada Client instance.
|
|
13
14
|
* Features (like funnel) can be toggled via the config.
|
|
14
15
|
*/
|
|
15
16
|
export declare function createTagadaClient(config?: TagadaClientConfig): TagadaClient;
|
|
16
|
-
export { TagadaClient, ApiClient };
|
|
17
|
+
export { TagadaClient, ApiClient, CheckoutResource };
|
|
17
18
|
export type { TagadaClientConfig, TagadaState };
|
|
18
19
|
export { FunnelActionType } from '../core/resources/funnel';
|
|
19
20
|
export type { FunnelAction, FunnelNavigationResult, SimpleFunnelContext } from '../core/resources/funnel';
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { TagadaClient } from '../core/client';
|
|
10
10
|
import { ApiClient } from '../core/resources/apiClient';
|
|
11
|
+
import { CheckoutResource } from '../core/resources/checkout';
|
|
11
12
|
/**
|
|
12
13
|
* Factory function to create a Tagada Client instance.
|
|
13
14
|
* Features (like funnel) can be toggled via the config.
|
|
@@ -16,7 +17,7 @@ export function createTagadaClient(config = {}) {
|
|
|
16
17
|
return new TagadaClient(config);
|
|
17
18
|
}
|
|
18
19
|
// Re-export Core Classes
|
|
19
|
-
export { TagadaClient, ApiClient };
|
|
20
|
+
export { TagadaClient, ApiClient, CheckoutResource };
|
|
20
21
|
export { FunnelActionType } from '../core/resources/funnel';
|
|
21
22
|
// Re-export Utilities
|
|
22
23
|
export * from '../core/utils';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tagadapay/plugin-sdk",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.15",
|
|
4
4
|
"description": "Modern React SDK for building Tagada Pay plugins",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -73,7 +73,9 @@
|
|
|
73
73
|
"@basis-theory/basis-theory-react": "^1.32.5",
|
|
74
74
|
"@basis-theory/web-threeds": "^1.0.1",
|
|
75
75
|
"@google-pay/button-react": "^3.0.10",
|
|
76
|
+
"@tagadapay/plugin-sdk": "link:",
|
|
76
77
|
"@tanstack/react-query": "^5.90.2",
|
|
78
|
+
"@ua-parser-js/pro-enterprise": "^2.0.6",
|
|
77
79
|
"axios": "^1.10.0",
|
|
78
80
|
"iso3166-2-db": "^2.3.11",
|
|
79
81
|
"path-to-regexp": "^8.2.0",
|