@tagadapay/plugin-sdk 3.0.14 → 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 +53 -5
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +3 -3
- package/dist/v2/core/client.d.ts +1 -0
- package/dist/v2/core/client.js +25 -0
- package/dist/v2/core/funnelClient.d.ts +9 -0
- package/dist/v2/core/funnelClient.js +34 -6
- package/dist/v2/core/resources/checkout.d.ts +32 -0
- package/dist/v2/core/resources/checkout.js +38 -0
- package/dist/v2/core/resources/funnel.d.ts +1 -0
- package/dist/v2/react/providers/TagadaProvider.js +61 -1
- package/package.json +1 -1
package/dist/v2/core/client.d.ts
CHANGED
package/dist/v2/core/client.js
CHANGED
|
@@ -26,6 +26,29 @@ export class TagadaClient {
|
|
|
26
26
|
this.config = config;
|
|
27
27
|
this.instanceId = Math.random().toString(36).substr(2, 9);
|
|
28
28
|
this.boundHandleStorageChange = this.handleStorageChange.bind(this);
|
|
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
|
+
};
|
|
29
52
|
console.log(`[TagadaClient ${this.instanceId}] Initializing...`);
|
|
30
53
|
console.log(`[TagadaClient ${this.instanceId}] Config:`, {
|
|
31
54
|
debugMode: config.debugMode,
|
|
@@ -115,6 +138,7 @@ export class TagadaClient {
|
|
|
115
138
|
// Listen for storage changes (cross-tab sync)
|
|
116
139
|
if (typeof window !== 'undefined') {
|
|
117
140
|
window.addEventListener('storage', this.boundHandleStorageChange);
|
|
141
|
+
window.addEventListener('pageshow', this.boundHandlePageshow);
|
|
118
142
|
}
|
|
119
143
|
// Setup config hot-reload listener (for live config editing)
|
|
120
144
|
this.setupConfigHotReload();
|
|
@@ -127,6 +151,7 @@ export class TagadaClient {
|
|
|
127
151
|
destroy() {
|
|
128
152
|
if (typeof window !== 'undefined') {
|
|
129
153
|
window.removeEventListener('storage', this.boundHandleStorageChange);
|
|
154
|
+
window.removeEventListener('pageshow', this.boundHandlePageshow);
|
|
130
155
|
}
|
|
131
156
|
if (this.state.debugMode) {
|
|
132
157
|
console.log(`[TagadaClient ${this.instanceId}] Destroyed`);
|
|
@@ -50,6 +50,15 @@ export declare class FunnelClient {
|
|
|
50
50
|
* Get current state
|
|
51
51
|
*/
|
|
52
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;
|
|
53
62
|
/**
|
|
54
63
|
* Initialize session with automatic detection (cookies, URL, etc.)
|
|
55
64
|
*/
|
|
@@ -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,15 +137,12 @@ 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
|
|
@@ -209,6 +209,38 @@ export declare class CheckoutResource {
|
|
|
209
209
|
customerId: string;
|
|
210
210
|
status: 'processing';
|
|
211
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
|
+
}>;
|
|
212
244
|
/**
|
|
213
245
|
* Check async checkout processing status (instant, no waiting)
|
|
214
246
|
* Perfect for polling or checking if background job completed
|
|
@@ -33,6 +33,44 @@ export class CheckoutResource {
|
|
|
33
33
|
async initCheckoutAsync(params) {
|
|
34
34
|
return this.apiClient.post('/api/v1/checkout/session/init-async', params);
|
|
35
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
|
+
}
|
|
36
74
|
/**
|
|
37
75
|
* Check async checkout processing status (instant, no waiting)
|
|
38
76
|
* Perfect for polling or checking if background job completed
|
|
@@ -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: {
|