@tagadapay/plugin-sdk 2.8.8 → 2.8.10
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/react/config/environment.d.ts +1 -22
- package/dist/react/config/environment.js +1 -132
- package/dist/react/utils/deviceInfo.d.ts +1 -39
- package/dist/react/utils/deviceInfo.js +1 -163
- package/dist/react/utils/jwtDecoder.d.ts +1 -14
- package/dist/react/utils/jwtDecoder.js +1 -86
- package/dist/react/utils/tokenStorage.d.ts +1 -16
- package/dist/react/utils/tokenStorage.js +1 -53
- package/dist/v2/core/client.d.ts +96 -0
- package/dist/v2/core/client.js +430 -0
- package/dist/v2/core/config/environment.d.ts +36 -0
- package/dist/v2/core/config/environment.js +155 -0
- package/dist/v2/core/pathRemapping.js +61 -3
- package/dist/v2/core/resources/apiClient.d.ts +13 -0
- package/dist/v2/core/resources/apiClient.js +77 -9
- package/dist/v2/core/resources/funnel.d.ts +21 -0
- package/dist/v2/core/resources/payments.d.ts +23 -0
- package/dist/v2/core/types.d.ts +271 -0
- package/dist/v2/core/types.js +4 -0
- package/dist/v2/core/utils/deviceInfo.d.ts +39 -0
- package/dist/v2/core/utils/deviceInfo.js +162 -0
- package/dist/v2/core/utils/eventDispatcher.d.ts +10 -0
- package/dist/v2/core/utils/eventDispatcher.js +24 -0
- package/dist/v2/core/utils/jwtDecoder.d.ts +14 -0
- package/dist/v2/core/utils/jwtDecoder.js +85 -0
- package/dist/v2/core/utils/pluginConfig.d.ts +1 -0
- package/dist/v2/core/utils/pluginConfig.js +64 -8
- package/dist/v2/core/utils/tokenStorage.d.ts +19 -0
- package/dist/v2/core/utils/tokenStorage.js +52 -0
- package/dist/v2/react/components/ApplePayButton.js +1 -1
- package/dist/v2/react/components/DebugDrawer.js +90 -1
- package/dist/v2/react/hooks/__examples__/FunnelContextExample.d.ts +12 -0
- package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +54 -0
- package/dist/v2/react/hooks/useFunnel.d.ts +2 -1
- package/dist/v2/react/hooks/useFunnel.js +245 -69
- package/dist/v2/react/hooks/useGoogleAutocomplete.js +26 -18
- package/dist/v2/react/hooks/useISOData.js +4 -2
- package/dist/v2/react/hooks/useOffersQuery.d.ts +42 -29
- package/dist/v2/react/hooks/useOffersQuery.js +266 -204
- package/dist/v2/react/hooks/usePaymentQuery.js +99 -6
- package/dist/v2/react/providers/TagadaProvider.d.ts +13 -21
- package/dist/v2/react/providers/TagadaProvider.js +79 -673
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Example: Accessing Funnel Context & Resources
|
|
4
|
+
*
|
|
5
|
+
* This example demonstrates how to access data from previous steps
|
|
6
|
+
* using the funnel context, including order, customer, and other resources.
|
|
7
|
+
*/
|
|
8
|
+
import { useFunnel } from '../useFunnel';
|
|
9
|
+
export function FunnelContextExample() {
|
|
10
|
+
const { context, isLoading, isInitialized } = useFunnel({
|
|
11
|
+
autoInitialize: true,
|
|
12
|
+
});
|
|
13
|
+
if (isLoading) {
|
|
14
|
+
return _jsx("div", { children: "Loading funnel session..." });
|
|
15
|
+
}
|
|
16
|
+
if (!isInitialized || !context) {
|
|
17
|
+
return _jsx("div", { children: "Funnel not initialized" });
|
|
18
|
+
}
|
|
19
|
+
// Access resources from context
|
|
20
|
+
const order = context.resources?.order;
|
|
21
|
+
const customer = context.resources?.customer;
|
|
22
|
+
const checkout = context.resources?.checkout;
|
|
23
|
+
const orders = context.resources?.orders || [];
|
|
24
|
+
return (_jsxs("div", { style: { padding: '20px', fontFamily: 'sans-serif' }, children: [_jsx("h1", { children: "Funnel Context Example" }), _jsxs("section", { style: { marginBottom: '30px' }, children: [_jsx("h2", { children: "Session Info" }), _jsxs("ul", { children: [_jsxs("li", { children: [_jsx("strong", { children: "Session ID:" }), " ", context.sessionId] }), _jsxs("li", { children: [_jsx("strong", { children: "Funnel ID:" }), " ", context.funnelId] }), _jsxs("li", { children: [_jsx("strong", { children: "Current Step:" }), " ", context.currentStepId] }), _jsxs("li", { children: [_jsx("strong", { children: "Previous Step:" }), " ", context.previousStepId || 'None'] }), _jsxs("li", { children: [_jsx("strong", { children: "Furthest Step:" }), " ", context.furthestStepId || 'None'] }), _jsxs("li", { children: [_jsx("strong", { children: "Environment:" }), " ", context.environment || 'Not set'] })] })] }), _jsxs("section", { style: { marginBottom: '30px' }, children: [_jsx("h2", { children: "Resources" }), order && (_jsxs("div", { style: { marginBottom: '20px', padding: '15px', border: '1px solid #ddd', borderRadius: '5px' }, children: [_jsx("h3", { children: "Order (Hot Context)" }), _jsx("pre", { style: { background: '#f5f5f5', padding: '10px', borderRadius: '3px', overflow: 'auto' }, children: JSON.stringify(order, null, 2) })] })), customer && (_jsxs("div", { style: { marginBottom: '20px', padding: '15px', border: '1px solid #ddd', borderRadius: '5px' }, children: [_jsx("h3", { children: "Customer" }), _jsx("pre", { style: { background: '#f5f5f5', padding: '10px', borderRadius: '3px', overflow: 'auto' }, children: JSON.stringify(customer, null, 2) })] })), checkout && (_jsxs("div", { style: { marginBottom: '20px', padding: '15px', border: '1px solid #ddd', borderRadius: '5px' }, children: [_jsx("h3", { children: "Checkout" }), _jsx("pre", { style: { background: '#f5f5f5', padding: '10px', borderRadius: '3px', overflow: 'auto' }, children: JSON.stringify(checkout, null, 2) })] })), orders.length > 0 && (_jsxs("div", { style: { marginBottom: '20px', padding: '15px', border: '1px solid #ddd', borderRadius: '5px' }, children: [_jsxs("h3", { children: ["Orders Collection (", orders.length, " orders)"] }), orders.map((o, index) => (_jsxs("div", { style: { marginBottom: '10px' }, children: [_jsxs("strong", { children: ["Order ", index + 1, ":"] }), _jsx("pre", { style: {
|
|
25
|
+
background: '#f5f5f5',
|
|
26
|
+
padding: '10px',
|
|
27
|
+
borderRadius: '3px',
|
|
28
|
+
marginTop: '5px',
|
|
29
|
+
overflow: 'auto',
|
|
30
|
+
}, children: JSON.stringify(o, null, 2) })] }, o.id || index)))] })), _jsxs("details", { style: { marginTop: '20px' }, children: [_jsx("summary", { style: { cursor: 'pointer', fontWeight: 'bold' }, children: "Show All Resources (Raw)" }), _jsx("pre", { style: {
|
|
31
|
+
background: '#f5f5f5',
|
|
32
|
+
padding: '10px',
|
|
33
|
+
borderRadius: '3px',
|
|
34
|
+
marginTop: '10px',
|
|
35
|
+
overflow: 'auto',
|
|
36
|
+
}, children: JSON.stringify(context.resources, null, 2) })] })] }), context.metadata && Object.keys(context.metadata).length > 0 && (_jsxs("section", { style: { marginBottom: '30px' }, children: [_jsx("h2", { children: "Metadata" }), _jsx("pre", { style: { background: '#f5f5f5', padding: '10px', borderRadius: '3px', overflow: 'auto' }, children: JSON.stringify(context.metadata, null, 2) })] })), _jsxs("details", { children: [_jsx("summary", { style: { cursor: 'pointer', fontWeight: 'bold' }, children: "Show Full Context (Debug)" }), _jsx("pre", { style: {
|
|
37
|
+
background: '#f5f5f5',
|
|
38
|
+
padding: '10px',
|
|
39
|
+
borderRadius: '3px',
|
|
40
|
+
marginTop: '10px',
|
|
41
|
+
overflow: 'auto',
|
|
42
|
+
}, children: JSON.stringify(context, null, 2) })] })] }));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Simple example for documentation
|
|
46
|
+
* This can now be called with or without options
|
|
47
|
+
*/
|
|
48
|
+
export function SimpleContextExample() {
|
|
49
|
+
const funnel = useFunnel(); // Works without params!
|
|
50
|
+
// Access data from previous steps
|
|
51
|
+
const order = funnel.context?.resources?.order;
|
|
52
|
+
const customer = funnel.context?.resources?.customer;
|
|
53
|
+
return (_jsxs("div", { children: [_jsxs("h1", { children: ["Order ", order?.id] }), _jsxs("p", { children: ["Customer: ", customer?.email] }), _jsxs("p", { children: ["Amount: $", ((order?.amount || 0) / 100).toFixed(2)] })] }));
|
|
54
|
+
}
|
|
@@ -23,6 +23,7 @@ export interface UseFunnelResult {
|
|
|
23
23
|
context: SimpleFunnelContext | null;
|
|
24
24
|
isLoading: boolean;
|
|
25
25
|
isInitialized: boolean;
|
|
26
|
+
isNavigating: boolean;
|
|
26
27
|
initializeSession: (entryStepId?: string) => Promise<void>;
|
|
27
28
|
endSession: () => Promise<void>;
|
|
28
29
|
retryInitialization: () => Promise<void>;
|
|
@@ -37,7 +38,7 @@ export interface UseFunnelResult {
|
|
|
37
38
|
* Modern funnel navigation using TanStack Query for state management
|
|
38
39
|
* and the v2 ApiClient architecture.
|
|
39
40
|
*/
|
|
40
|
-
export declare function useFunnel(options
|
|
41
|
+
export declare function useFunnel(options?: UseFunnelOptions): UseFunnelResult;
|
|
41
42
|
/**
|
|
42
43
|
* Simplified funnel hook for basic step tracking (v2)
|
|
43
44
|
*/
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and the v2 ApiClient for API calls.
|
|
6
6
|
*/
|
|
7
7
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
8
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
9
|
import { FunnelActionType, FunnelResource } from '../../core/resources/funnel';
|
|
10
10
|
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
11
11
|
import { getGlobalApiClient } from './useApiQuery';
|
|
@@ -20,19 +20,86 @@ const funnelQueryKeys = {
|
|
|
20
20
|
* Modern funnel navigation using TanStack Query for state management
|
|
21
21
|
* and the v2 ApiClient architecture.
|
|
22
22
|
*/
|
|
23
|
-
export function useFunnel(options) {
|
|
24
|
-
const { auth, store, debugMode, updateFunnelDebugData } = useTagadaContext();
|
|
23
|
+
export function useFunnel(options = {}) {
|
|
24
|
+
const { auth, store, debugMode, updateFunnelDebugData, pluginConfig } = useTagadaContext();
|
|
25
25
|
const queryClient = useQueryClient();
|
|
26
26
|
const apiClient = getGlobalApiClient();
|
|
27
27
|
const funnelResource = useMemo(() => new FunnelResource(apiClient), [apiClient]);
|
|
28
|
+
/**
|
|
29
|
+
* Helper to merge backend context with local static resources
|
|
30
|
+
* Allows mocking static resources in local development via resources.static.json
|
|
31
|
+
*/
|
|
32
|
+
const enrichContext = useCallback((ctx) => {
|
|
33
|
+
// If we have local static resources (from resources.static.json), merge them
|
|
34
|
+
// Backend resources take precedence
|
|
35
|
+
const localStaticResources = pluginConfig?.staticResources || {};
|
|
36
|
+
if (Object.keys(localStaticResources).length === 0) {
|
|
37
|
+
return ctx;
|
|
38
|
+
}
|
|
39
|
+
console.log('🛠️ Funnel: Merging local static resources:', localStaticResources);
|
|
40
|
+
const enriched = {
|
|
41
|
+
...ctx,
|
|
42
|
+
static: {
|
|
43
|
+
...localStaticResources,
|
|
44
|
+
...(ctx.static || {})
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
console.log('🛠️ Funnel: Enriched context.static:', enriched.static);
|
|
48
|
+
return enriched;
|
|
49
|
+
}, [pluginConfig]);
|
|
28
50
|
// Local state
|
|
29
51
|
const [context, setContext] = useState(null);
|
|
30
52
|
const [initializationAttempted, setInitializationAttempted] = useState(false);
|
|
31
53
|
const [initializationError, setInitializationError] = useState(null);
|
|
54
|
+
// Track the last processed URL funnelId to avoid re-processing on context changes
|
|
55
|
+
const lastProcessedUrlFunnelIdRef = useRef(undefined);
|
|
56
|
+
// ✅ Track if initialization is currently in progress to prevent duplicate calls
|
|
57
|
+
const isInitializingRef = useRef(false);
|
|
58
|
+
// Check if we have an existing session in cookies (to avoid unnecessary re-initialization)
|
|
59
|
+
const hasExistingSessionCookie = useMemo(() => {
|
|
60
|
+
if (typeof document === 'undefined')
|
|
61
|
+
return false;
|
|
62
|
+
const funnelSessionCookie = document.cookie
|
|
63
|
+
.split('; ')
|
|
64
|
+
.find(row => row.startsWith('tgd-funnel-session-id='));
|
|
65
|
+
return !!funnelSessionCookie;
|
|
66
|
+
}, []); // Only check once on mount
|
|
32
67
|
const currentStepId = options.currentStepId || context?.currentStepId;
|
|
33
|
-
// Check for URL parameter overrides
|
|
34
|
-
|
|
35
|
-
const
|
|
68
|
+
// Check for URL parameter overrides - recalculate on every render to detect URL changes
|
|
69
|
+
// This is cheap and ensures we always have the latest URL params
|
|
70
|
+
const [currentUrl, setCurrentUrl] = useState(() => typeof window !== 'undefined' ? window.location.href : '');
|
|
71
|
+
// Listen for URL changes (navigation)
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (typeof window === 'undefined')
|
|
74
|
+
return;
|
|
75
|
+
const updateUrl = () => {
|
|
76
|
+
setCurrentUrl(window.location.href);
|
|
77
|
+
};
|
|
78
|
+
// Listen for popstate (back/forward buttons) and pushState/replaceState
|
|
79
|
+
window.addEventListener('popstate', updateUrl);
|
|
80
|
+
// Also update on any navigation
|
|
81
|
+
const originalPushState = window.history.pushState;
|
|
82
|
+
const originalReplaceState = window.history.replaceState;
|
|
83
|
+
window.history.pushState = function (...args) {
|
|
84
|
+
originalPushState.apply(window.history, args);
|
|
85
|
+
updateUrl();
|
|
86
|
+
};
|
|
87
|
+
window.history.replaceState = function (...args) {
|
|
88
|
+
originalReplaceState.apply(window.history, args);
|
|
89
|
+
updateUrl();
|
|
90
|
+
};
|
|
91
|
+
return () => {
|
|
92
|
+
window.removeEventListener('popstate', updateUrl);
|
|
93
|
+
window.history.pushState = originalPushState;
|
|
94
|
+
window.history.replaceState = originalReplaceState;
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
const urlFunnelId = useMemo(() => {
|
|
98
|
+
if (typeof window === 'undefined')
|
|
99
|
+
return undefined;
|
|
100
|
+
const params = new URLSearchParams(window.location.search);
|
|
101
|
+
return params.get('funnelId') || undefined;
|
|
102
|
+
}, [currentUrl]); // Re-compute when URL changes
|
|
36
103
|
const effectiveFunnelId = urlFunnelId || options.funnelId;
|
|
37
104
|
/**
|
|
38
105
|
* Fetch debug data for the funnel debugger
|
|
@@ -63,19 +130,11 @@ export function useFunnel(options) {
|
|
|
63
130
|
queryKey: funnelQueryKeys.session(context?.sessionId || ''),
|
|
64
131
|
queryFn: async () => {
|
|
65
132
|
if (!context?.sessionId) {
|
|
66
|
-
console.warn('🍪 Funnel: No session ID available for query');
|
|
67
133
|
return null;
|
|
68
134
|
}
|
|
69
|
-
|
|
70
|
-
const currentUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
71
|
-
console.log(`🍪 Funnel: Fetching session data for ID: ${context.sessionId}`);
|
|
72
|
-
if (currentUrl) {
|
|
73
|
-
console.log(`🍪 Funnel: Including current URL for sync: ${currentUrl}`);
|
|
74
|
-
}
|
|
75
|
-
const response = await funnelResource.getSession(context.sessionId, currentUrl);
|
|
76
|
-
console.log(`🍪 Funnel: Session fetch response:`, response);
|
|
135
|
+
const response = await funnelResource.getSession(context.sessionId);
|
|
77
136
|
if (response.success && response.context) {
|
|
78
|
-
return response.context;
|
|
137
|
+
return enrichContext(response.context);
|
|
79
138
|
}
|
|
80
139
|
throw new Error(response.error || 'Failed to fetch session');
|
|
81
140
|
},
|
|
@@ -87,11 +146,23 @@ export function useFunnel(options) {
|
|
|
87
146
|
// Initialize session mutation
|
|
88
147
|
const initializeMutation = useMutation({
|
|
89
148
|
mutationFn: async (entryStepId) => {
|
|
149
|
+
// ✅ Prevent duplicate initialization calls
|
|
150
|
+
if (isInitializingRef.current) {
|
|
151
|
+
throw new Error('Initialization already in progress');
|
|
152
|
+
}
|
|
153
|
+
isInitializingRef.current = true;
|
|
90
154
|
if (!auth.session?.customerId || !store?.id) {
|
|
155
|
+
isInitializingRef.current = false;
|
|
91
156
|
throw new Error('Authentication required for funnel session');
|
|
92
157
|
}
|
|
158
|
+
// Get CURRENT URL params (not cached) to ensure we have latest values
|
|
159
|
+
const currentUrlParams = typeof window !== 'undefined'
|
|
160
|
+
? new URLSearchParams(window.location.search)
|
|
161
|
+
: new URLSearchParams();
|
|
162
|
+
const currentUrlFunnelId = currentUrlParams.get('funnelId') || undefined;
|
|
163
|
+
const currentEffectiveFunnelId = currentUrlFunnelId || options.funnelId;
|
|
93
164
|
// Check for existing session ID in URL parameters
|
|
94
|
-
let existingSessionId =
|
|
165
|
+
let existingSessionId = currentUrlParams.get('funnelSessionId') || undefined;
|
|
95
166
|
if (existingSessionId) {
|
|
96
167
|
console.log(`🍪 Funnel: Found session ID in URL params: ${existingSessionId}`);
|
|
97
168
|
}
|
|
@@ -102,11 +173,23 @@ export function useFunnel(options) {
|
|
|
102
173
|
.find(row => row.startsWith('tgd-funnel-session-id='));
|
|
103
174
|
existingSessionId = funnelSessionCookie ? funnelSessionCookie.split('=')[1] : undefined;
|
|
104
175
|
if (existingSessionId) {
|
|
105
|
-
console.log(`🍪 Funnel: Found session in cookie: ${existingSessionId}`);
|
|
176
|
+
console.log(`🍪 Funnel: Found existing session in cookie: ${existingSessionId}`);
|
|
106
177
|
}
|
|
107
178
|
}
|
|
108
|
-
|
|
109
|
-
|
|
179
|
+
// Log initialization strategy
|
|
180
|
+
console.log(`🍪 Funnel: Initialize request:`);
|
|
181
|
+
console.log(` - Existing session ID: ${existingSessionId || 'none'}`);
|
|
182
|
+
console.log(` - URL funnelId: ${currentUrlFunnelId || 'none'}`);
|
|
183
|
+
console.log(` - Hook funnelId: ${options.funnelId || 'none'}`);
|
|
184
|
+
console.log(` - Effective funnelId to send: ${currentEffectiveFunnelId || 'none (will use existing or backend default)'}`);
|
|
185
|
+
if (existingSessionId && !currentEffectiveFunnelId) {
|
|
186
|
+
console.log(` → Strategy: REUSE existing session from cookie`);
|
|
187
|
+
}
|
|
188
|
+
else if (existingSessionId && currentEffectiveFunnelId) {
|
|
189
|
+
console.log(` → Strategy: VALIDATE existing session matches funnelId, or create new`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log(` → Strategy: CREATE new session`);
|
|
110
193
|
}
|
|
111
194
|
// Send minimal CMS session data
|
|
112
195
|
const cmsSessionData = {
|
|
@@ -115,40 +198,66 @@ export function useFunnel(options) {
|
|
|
115
198
|
sessionId: auth.session.sessionId,
|
|
116
199
|
accountId: auth.session.accountId
|
|
117
200
|
};
|
|
201
|
+
// Get current URL for session synchronization
|
|
202
|
+
const currentUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
118
203
|
// Call API to initialize session - backend will restore existing or create new
|
|
119
204
|
const requestBody = {
|
|
120
205
|
cmsSession: cmsSessionData,
|
|
121
206
|
entryStepId, // Optional override
|
|
122
|
-
existingSessionId // Pass existing session ID from URL or cookie
|
|
207
|
+
existingSessionId, // Pass existing session ID from URL or cookie
|
|
208
|
+
currentUrl // Include current URL for step tracking
|
|
123
209
|
};
|
|
124
210
|
// Only include funnelId if it's provided (for backend fallback)
|
|
125
|
-
if (
|
|
126
|
-
requestBody.funnelId =
|
|
211
|
+
if (currentEffectiveFunnelId) {
|
|
212
|
+
requestBody.funnelId = currentEffectiveFunnelId;
|
|
127
213
|
}
|
|
214
|
+
console.log(`📤 Funnel: Sending initialize request to backend:`);
|
|
215
|
+
console.log(` Request body:`, JSON.stringify({
|
|
216
|
+
...requestBody,
|
|
217
|
+
cmsSession: { ...cmsSessionData, sessionId: `${cmsSessionData.sessionId.substring(0, 20)}...` }
|
|
218
|
+
}, null, 2));
|
|
128
219
|
const response = await funnelResource.initialize(requestBody);
|
|
220
|
+
console.log(`📥 Funnel: Received response from backend:`);
|
|
221
|
+
console.log(` Success: ${response.success}`);
|
|
222
|
+
if (response.context) {
|
|
223
|
+
console.log(` Returned session ID: ${response.context.sessionId}`);
|
|
224
|
+
console.log(` Returned funnel ID: ${response.context.funnelId}`);
|
|
225
|
+
console.log(` Is same session? ${response.context.sessionId === existingSessionId ? '✅ YES (reused)' : '❌ NO (new session created)'}`);
|
|
226
|
+
}
|
|
227
|
+
if (response.error) {
|
|
228
|
+
console.log(` Error: ${response.error}`);
|
|
229
|
+
}
|
|
129
230
|
if (response.success && response.context) {
|
|
130
|
-
return response.context;
|
|
231
|
+
return enrichContext(response.context);
|
|
131
232
|
}
|
|
132
233
|
else {
|
|
133
234
|
throw new Error(response.error || 'Failed to initialize funnel session');
|
|
134
235
|
}
|
|
135
236
|
},
|
|
136
237
|
onSuccess: (newContext) => {
|
|
137
|
-
|
|
238
|
+
// ✅ Reset initialization flag
|
|
239
|
+
isInitializingRef.current = false;
|
|
240
|
+
// Context is already enriched by mutationFn return, but safe to ensure
|
|
241
|
+
const enrichedContext = enrichContext(newContext);
|
|
242
|
+
setContext(enrichedContext);
|
|
138
243
|
setInitializationError(null);
|
|
139
244
|
// Set session cookie for persistence across page reloads
|
|
140
|
-
setSessionCookie(
|
|
141
|
-
console.log(
|
|
245
|
+
setSessionCookie(enrichedContext.sessionId);
|
|
246
|
+
console.log(`✅ Funnel: Session initialized/loaded successfully`);
|
|
247
|
+
console.log(` - Session ID: ${enrichedContext.sessionId}`);
|
|
248
|
+
console.log(` - Funnel ID: ${enrichedContext.funnelId}`);
|
|
249
|
+
console.log(` - Current Step: ${enrichedContext.currentStepId}`);
|
|
142
250
|
// Fetch debug data if in debug mode
|
|
143
251
|
if (debugMode) {
|
|
144
|
-
void fetchFunnelDebugData(
|
|
252
|
+
void fetchFunnelDebugData(enrichedContext.sessionId);
|
|
145
253
|
}
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
});
|
|
254
|
+
// Set the session query data directly (no need to refetch since initialize already returns full context)
|
|
255
|
+
queryClient.setQueryData(funnelQueryKeys.session(enrichedContext.sessionId), enrichedContext);
|
|
256
|
+
console.log(`✅ Funnel: Session query data populated from initialize response (no additional request needed)`);
|
|
150
257
|
},
|
|
151
258
|
onError: (error) => {
|
|
259
|
+
// ✅ Reset initialization flag on error
|
|
260
|
+
isInitializingRef.current = false;
|
|
152
261
|
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
153
262
|
setInitializationError(errorObj);
|
|
154
263
|
console.error('Error initializing funnel session:', error);
|
|
@@ -169,9 +278,7 @@ export function useFunnel(options) {
|
|
|
169
278
|
// ✅ Automatically include currentUrl for URL-to-Step mapping
|
|
170
279
|
// User can override by providing event.currentUrl explicitly
|
|
171
280
|
const currentUrl = event.currentUrl || (typeof window !== 'undefined' ? window.location.href : undefined);
|
|
172
|
-
console.log(`🍪 Funnel: Navigating with session ID: ${context.sessionId}`);
|
|
173
281
|
if (currentUrl) {
|
|
174
|
-
console.log(`🍪 Funnel: Current URL for sync: ${currentUrl}`);
|
|
175
282
|
}
|
|
176
283
|
const requestBody = {
|
|
177
284
|
sessionId: context.sessionId,
|
|
@@ -218,7 +325,8 @@ export function useFunnel(options) {
|
|
|
218
325
|
...(recoveredSessionId ? { recovered: true, oldSessionId: context.sessionId } : {})
|
|
219
326
|
}
|
|
220
327
|
};
|
|
221
|
-
|
|
328
|
+
const enrichedContext = enrichContext(newContext);
|
|
329
|
+
setContext(enrichedContext);
|
|
222
330
|
// Update cookie with new session ID if recovered
|
|
223
331
|
if (recoveredSessionId) {
|
|
224
332
|
document.cookie = `funnelSessionId=${recoveredSessionId}; path=/; max-age=86400; SameSite=Lax`;
|
|
@@ -231,7 +339,7 @@ export function useFunnel(options) {
|
|
|
231
339
|
type: 'redirect', // Default action type
|
|
232
340
|
url: result.url
|
|
233
341
|
},
|
|
234
|
-
context:
|
|
342
|
+
context: enrichedContext,
|
|
235
343
|
tracking: result.tracking
|
|
236
344
|
};
|
|
237
345
|
// Handle navigation callback with override capability
|
|
@@ -245,19 +353,25 @@ export function useFunnel(options) {
|
|
|
245
353
|
// Perform default navigation if not overridden
|
|
246
354
|
if (shouldPerformDefaultNavigation && navigationResult.action.url) {
|
|
247
355
|
// Add URL parameters for cross-domain session continuity
|
|
248
|
-
const urlWithParams = addSessionParams(navigationResult.action.url,
|
|
356
|
+
const urlWithParams = addSessionParams(navigationResult.action.url, enrichedContext.sessionId, effectiveFunnelId || options.funnelId);
|
|
249
357
|
const updatedAction = { ...navigationResult.action, url: urlWithParams };
|
|
250
358
|
performNavigation(updatedAction);
|
|
251
359
|
}
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
360
|
+
// Skip background refreshes if we are navigating away (full page reload)
|
|
361
|
+
// This prevents "lingering" requests from the old page context
|
|
362
|
+
const isFullNavigation = shouldPerformDefaultNavigation &&
|
|
363
|
+
navigationResult.action.url &&
|
|
364
|
+
(navigationResult.action.type === 'redirect' || navigationResult.action.type === 'replace');
|
|
365
|
+
if (!isFullNavigation) {
|
|
366
|
+
// Fetch debug data if in debug mode
|
|
367
|
+
if (debugMode) {
|
|
368
|
+
void fetchFunnelDebugData(enrichedContext.sessionId);
|
|
369
|
+
}
|
|
370
|
+
// Invalidate and refetch session data
|
|
371
|
+
void queryClient.invalidateQueries({
|
|
372
|
+
queryKey: funnelQueryKeys.session(enrichedContext.sessionId)
|
|
373
|
+
});
|
|
256
374
|
}
|
|
257
|
-
// Invalidate and refetch session data
|
|
258
|
-
void queryClient.invalidateQueries({
|
|
259
|
-
queryKey: funnelQueryKeys.session(newContext.sessionId)
|
|
260
|
-
});
|
|
261
375
|
},
|
|
262
376
|
onError: (error) => {
|
|
263
377
|
console.error('Funnel navigation error:', error);
|
|
@@ -291,7 +405,8 @@ export function useFunnel(options) {
|
|
|
291
405
|
...updates,
|
|
292
406
|
lastActivityAt: Date.now()
|
|
293
407
|
};
|
|
294
|
-
|
|
408
|
+
const enrichedContext = enrichContext(updatedContext);
|
|
409
|
+
setContext(enrichedContext);
|
|
295
410
|
console.log(`🍪 Funnel: Updated context for step ${context.currentStepId}`);
|
|
296
411
|
// Invalidate session query
|
|
297
412
|
void queryClient.invalidateQueries({
|
|
@@ -317,6 +432,8 @@ export function useFunnel(options) {
|
|
|
317
432
|
return;
|
|
318
433
|
console.log(`🍪 Funnel: Ended session ${context.sessionId}`);
|
|
319
434
|
setContext(null);
|
|
435
|
+
// Reset the processed URL funnelId ref to allow re-initialization
|
|
436
|
+
lastProcessedUrlFunnelIdRef.current = undefined;
|
|
320
437
|
// Clear queries
|
|
321
438
|
queryClient.removeQueries({
|
|
322
439
|
queryKey: funnelQueryKeys.session(context.sessionId)
|
|
@@ -342,14 +459,15 @@ export function useFunnel(options) {
|
|
|
342
459
|
*/
|
|
343
460
|
const addSessionParams = useCallback((url, sessionId, funnelId) => {
|
|
344
461
|
try {
|
|
345
|
-
|
|
462
|
+
// Handle relative URLs by using current origin
|
|
463
|
+
const urlObj = url.startsWith('http')
|
|
464
|
+
? new URL(url)
|
|
465
|
+
: new URL(url, window.location.origin);
|
|
346
466
|
urlObj.searchParams.set('funnelSessionId', sessionId);
|
|
347
467
|
if (funnelId) {
|
|
348
468
|
urlObj.searchParams.set('funnelId', funnelId);
|
|
349
469
|
}
|
|
350
|
-
|
|
351
|
-
console.log(`🍪 Funnel: Added session params to URL: ${url} → ${urlWithParams}`);
|
|
352
|
-
return urlWithParams;
|
|
470
|
+
return urlObj.toString();
|
|
353
471
|
}
|
|
354
472
|
catch (error) {
|
|
355
473
|
console.warn('Failed to add session params to URL:', error);
|
|
@@ -370,33 +488,34 @@ export function useFunnel(options) {
|
|
|
370
488
|
}
|
|
371
489
|
switch (action.type) {
|
|
372
490
|
case 'redirect':
|
|
373
|
-
console.log(`🍪 Funnel: Redirecting to ${targetUrl}`);
|
|
374
491
|
window.location.href = targetUrl;
|
|
375
492
|
break;
|
|
376
493
|
case 'replace':
|
|
377
|
-
console.log(`🍪 Funnel: Replacing current page with ${targetUrl}`);
|
|
378
494
|
window.location.replace(targetUrl);
|
|
379
495
|
break;
|
|
380
496
|
case 'push':
|
|
381
|
-
console.log(`🍪 Funnel: Pushing to history: ${targetUrl}`);
|
|
382
497
|
window.history.pushState({}, '', targetUrl);
|
|
383
498
|
// Trigger a popstate event to update React Router
|
|
384
499
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
385
500
|
break;
|
|
386
501
|
case 'external':
|
|
387
|
-
console.log(`🍪 Funnel: Opening external URL: ${targetUrl}`);
|
|
388
502
|
window.open(targetUrl, '_blank');
|
|
389
503
|
break;
|
|
390
504
|
case 'none':
|
|
391
|
-
|
|
505
|
+
// No navigation needed
|
|
392
506
|
break;
|
|
393
507
|
default:
|
|
394
|
-
console.warn(
|
|
508
|
+
console.warn(`Unknown navigation action type: ${action.type}`);
|
|
395
509
|
break;
|
|
396
510
|
}
|
|
397
511
|
}, []);
|
|
398
512
|
// Public API methods
|
|
399
513
|
const initializeSession = useCallback(async (entryStepId) => {
|
|
514
|
+
// ✅ Check ref before even starting (prevents React StrictMode double-invocation)
|
|
515
|
+
if (isInitializingRef.current) {
|
|
516
|
+
console.log('⏭️ Funnel: initializeSession called but already initializing');
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
400
519
|
setInitializationAttempted(true);
|
|
401
520
|
await initializeMutation.mutateAsync(entryStepId);
|
|
402
521
|
}, [initializeMutation]);
|
|
@@ -421,29 +540,84 @@ export function useFunnel(options) {
|
|
|
421
540
|
setInitializationError(null);
|
|
422
541
|
await initializeSession();
|
|
423
542
|
}, [initializeSession]);
|
|
424
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Auto-initialization effect
|
|
545
|
+
*
|
|
546
|
+
* Handles funnel session initialization with the following logic:
|
|
547
|
+
* 1. If no session exists => Initialize with URL funnelId or hook funnelId or backend default
|
|
548
|
+
* 2. If session exists + explicit funnelId provided that differs => Reset and create new
|
|
549
|
+
* 3. If session exists + no funnelId provided => Keep existing session
|
|
550
|
+
*
|
|
551
|
+
* IMPORTANT: Only resets session if an EXPLICIT funnelId is provided that differs
|
|
552
|
+
* from the current session. If no funnelId is provided, keeps the existing session.
|
|
553
|
+
*
|
|
554
|
+
* Priority: URL funnelId > Hook funnelId > Existing session funnelId > Backend default
|
|
555
|
+
*/
|
|
425
556
|
useEffect(() => {
|
|
426
|
-
if
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
557
|
+
// Skip if auto-initialize is disabled
|
|
558
|
+
const autoInit = options.autoInitialize ?? true; // Default to true
|
|
559
|
+
if (!autoInit) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
// Skip if required dependencies are not available
|
|
563
|
+
if (!auth.session?.customerId || !store?.id) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// Skip if already initializing (check both mutation state and ref)
|
|
567
|
+
if (initializeMutation.isPending || isInitializingRef.current) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
// Determine if we have an explicit funnelId request (URL has priority)
|
|
571
|
+
const explicitFunnelId = urlFunnelId || options.funnelId;
|
|
572
|
+
// Case 1: No session exists yet - need to initialize
|
|
573
|
+
if (!context) {
|
|
574
|
+
// Check if we've already attempted initialization
|
|
575
|
+
if (!initializationAttempted) {
|
|
576
|
+
setInitializationAttempted(true);
|
|
577
|
+
initializeSession().catch(error => {
|
|
578
|
+
console.error('❌ Funnel: Auto-initialization failed:', error);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
// Case 2: Session exists - check if we need to reset it
|
|
584
|
+
// ONLY reset if an explicit funnelId is provided AND it differs from current session
|
|
585
|
+
if (explicitFunnelId && context.funnelId && explicitFunnelId !== context.funnelId) {
|
|
586
|
+
// Check if we've already processed this funnelId to prevent loops
|
|
587
|
+
if (lastProcessedUrlFunnelIdRef.current === explicitFunnelId) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
console.log(`🔄 Funnel: Switching from funnel ${context.funnelId} to ${explicitFunnelId}`);
|
|
591
|
+
// Mark this funnelId as processed
|
|
592
|
+
lastProcessedUrlFunnelIdRef.current = explicitFunnelId;
|
|
593
|
+
// Clear existing session
|
|
594
|
+
setContext(null);
|
|
595
|
+
setInitializationAttempted(false);
|
|
596
|
+
setInitializationError(null);
|
|
597
|
+
// Clear session cookie
|
|
598
|
+
document.cookie = 'tgd-funnel-session-id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
|
|
599
|
+
// Clear queries for old session
|
|
600
|
+
queryClient.removeQueries({
|
|
601
|
+
queryKey: funnelQueryKeys.session(context.sessionId)
|
|
602
|
+
});
|
|
603
|
+
// Initialize new session with correct funnelId
|
|
434
604
|
initializeSession().catch(error => {
|
|
435
|
-
console.error('
|
|
605
|
+
console.error('❌ Funnel: Failed to reset session:', error);
|
|
436
606
|
});
|
|
437
607
|
}
|
|
438
608
|
}, [
|
|
439
609
|
options.autoInitialize,
|
|
440
|
-
|
|
610
|
+
options.funnelId,
|
|
611
|
+
urlFunnelId,
|
|
612
|
+
context?.funnelId,
|
|
613
|
+
context?.sessionId,
|
|
441
614
|
initializationAttempted,
|
|
442
|
-
initializationError,
|
|
443
615
|
auth.session?.customerId,
|
|
444
616
|
store?.id,
|
|
445
617
|
initializeMutation.isPending,
|
|
446
|
-
initializeSession
|
|
618
|
+
initializeSession,
|
|
619
|
+
hasExistingSessionCookie,
|
|
620
|
+
queryClient
|
|
447
621
|
]);
|
|
448
622
|
// Sync session data from query to local state
|
|
449
623
|
useEffect(() => {
|
|
@@ -453,6 +627,7 @@ export function useFunnel(options) {
|
|
|
453
627
|
}, [sessionData, context]);
|
|
454
628
|
const isLoading = initializeMutation.isPending || navigateMutation.isPending || updateContextMutation.isPending;
|
|
455
629
|
const isInitialized = !!context;
|
|
630
|
+
const isNavigating = navigateMutation.isPending; // Explicit navigation state
|
|
456
631
|
return {
|
|
457
632
|
next,
|
|
458
633
|
goToStep,
|
|
@@ -463,6 +638,7 @@ export function useFunnel(options) {
|
|
|
463
638
|
context,
|
|
464
639
|
isLoading,
|
|
465
640
|
isInitialized,
|
|
641
|
+
isNavigating, // Expose isNavigating
|
|
466
642
|
initializeSession,
|
|
467
643
|
endSession,
|
|
468
644
|
retryInitialization,
|
|
@@ -15,6 +15,7 @@ export function useGoogleAutocomplete(options) {
|
|
|
15
15
|
const autocompleteServiceRef = useRef(null);
|
|
16
16
|
const placesServiceRef = useRef(null);
|
|
17
17
|
const scriptLoadedRef = useRef(false);
|
|
18
|
+
const debounceTimeoutRef = useRef(null);
|
|
18
19
|
// Inject Google Maps script
|
|
19
20
|
useEffect(() => {
|
|
20
21
|
if (!apiKey) {
|
|
@@ -97,25 +98,32 @@ export function useGoogleAutocomplete(options) {
|
|
|
97
98
|
setPredictions([]);
|
|
98
99
|
return;
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
// Clear existing timeout
|
|
102
|
+
if (debounceTimeoutRef.current) {
|
|
103
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
104
|
+
}
|
|
105
|
+
// Set new timeout
|
|
106
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
107
|
+
setIsLoading(true);
|
|
108
|
+
const request = {
|
|
109
|
+
input,
|
|
110
|
+
...(countryRestriction && {
|
|
111
|
+
componentRestrictions: { country: countryRestriction.toLowerCase() },
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
autocompleteServiceRef.current.getPlacePredictions(request, (results, status) => {
|
|
115
|
+
setIsLoading(false);
|
|
116
|
+
if (status === window.google.maps.places.PlacesServiceStatus.OK && results) {
|
|
117
|
+
setPredictions(results);
|
|
116
118
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
else {
|
|
120
|
+
setPredictions([]);
|
|
121
|
+
if (status !== window.google.maps.places.PlacesServiceStatus.ZERO_RESULTS) {
|
|
122
|
+
// Prediction failed but not zero results
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}, 300); // 300ms debounce
|
|
119
127
|
}, [initializeServices, isScriptLoaded]);
|
|
120
128
|
// Get detailed place information
|
|
121
129
|
const getPlaceDetails = useCallback((placeId) => {
|