@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.
@@ -58,6 +58,7 @@ export declare class TagadaClient {
58
58
  private tokenPromise;
59
59
  private tokenResolver;
60
60
  private boundHandleStorageChange;
61
+ private boundHandlePageshow;
61
62
  private readonly config;
62
63
  private instanceId;
63
64
  private isInitializingSession;
@@ -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
- // URL params
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
@@ -430,6 +430,7 @@ export interface SimpleFunnelContext<TCustom = {}> {
430
430
  * For backward compatibility and flexible unstructured data
431
431
  */
432
432
  metadata?: Record<string, any>;
433
+ script?: string;
433
434
  }
434
435
  export interface FunnelInitializeRequest {
435
436
  cmsSession: {
@@ -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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagadapay/plugin-sdk",
3
- "version": "3.0.14",
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",