@tagadapay/plugin-sdk 2.8.10 → 3.0.2
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/README.md +14 -14
- package/dist/index.js +1 -1
- package/dist/react/hooks/usePluginConfig.d.ts +1 -0
- package/dist/react/hooks/usePluginConfig.js +69 -18
- package/dist/react/providers/TagadaProvider.js +1 -4
- package/dist/v2/core/client.d.ts +18 -0
- package/dist/v2/core/client.js +45 -0
- package/dist/v2/core/config/environment.d.ts +8 -0
- package/dist/v2/core/config/environment.js +18 -0
- package/dist/v2/core/funnelClient.d.ts +84 -0
- package/dist/v2/core/funnelClient.js +252 -0
- package/dist/v2/core/index.d.ts +2 -0
- package/dist/v2/core/index.js +3 -0
- package/dist/v2/core/resources/apiClient.js +1 -1
- package/dist/v2/core/resources/funnel.d.ts +1 -0
- package/dist/v2/core/resources/offers.d.ts +182 -8
- package/dist/v2/core/resources/offers.js +25 -0
- package/dist/v2/core/resources/products.d.ts +5 -0
- package/dist/v2/core/resources/products.js +15 -1
- package/dist/v2/core/types.d.ts +1 -0
- package/dist/v2/core/utils/funnelQueryKeys.d.ts +23 -0
- package/dist/v2/core/utils/funnelQueryKeys.js +23 -0
- package/dist/v2/core/utils/index.d.ts +2 -0
- package/dist/v2/core/utils/index.js +2 -0
- package/dist/v2/core/utils/pluginConfig.js +44 -32
- package/dist/v2/core/utils/sessionStorage.d.ts +20 -0
- package/dist/v2/core/utils/sessionStorage.js +39 -0
- package/dist/v2/index.d.ts +4 -2
- package/dist/v2/index.js +1 -1
- package/dist/v2/react/components/DebugDrawer.js +99 -2
- package/dist/v2/react/hooks/__examples__/FunnelContextExample.d.ts +3 -0
- package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +4 -3
- package/dist/v2/react/hooks/useClubOffers.d.ts +2 -2
- package/dist/v2/react/hooks/useFunnel.d.ts +27 -39
- package/dist/v2/react/hooks/useFunnel.js +22 -659
- package/dist/v2/react/hooks/useFunnelLegacy.d.ts +52 -0
- package/dist/v2/react/hooks/useFunnelLegacy.js +733 -0
- package/dist/v2/react/hooks/useOfferQuery.d.ts +109 -0
- package/dist/v2/react/hooks/useOfferQuery.js +483 -0
- package/dist/v2/react/hooks/useOffersQuery.d.ts +9 -75
- package/dist/v2/react/hooks/useProductsQuery.d.ts +1 -0
- package/dist/v2/react/hooks/useProductsQuery.js +10 -6
- package/dist/v2/react/index.d.ts +8 -4
- package/dist/v2/react/index.js +4 -2
- package/dist/v2/react/providers/TagadaProvider.d.ts +66 -5
- package/dist/v2/react/providers/TagadaProvider.js +120 -6
- package/dist/v2/standalone/index.d.ts +20 -0
- package/dist/v2/standalone/index.js +22 -0
- package/dist/v2/vue/index.d.ts +6 -0
- package/dist/v2/vue/index.js +10 -0
- package/package.json +6 -1
|
@@ -1,667 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useFunnel Hook
|
|
2
|
+
* useFunnel Hook - Access funnel state and navigation methods
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* and
|
|
4
|
+
* This hook consumes the funnel state from TagadaProvider. All complex
|
|
5
|
+
* initialization and session management is handled at the provider level.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // In your root component or layout:
|
|
10
|
+
* <TagadaProvider funnelId="funnelv2_xxx" autoInitializeFunnel={true}>
|
|
11
|
+
* <YourApp />
|
|
12
|
+
* </TagadaProvider>
|
|
13
|
+
*
|
|
14
|
+
* // In any child component:
|
|
15
|
+
* const { context, next, isLoading } = useFunnel();
|
|
16
|
+
* ```
|
|
6
17
|
*/
|
|
7
|
-
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
8
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
|
-
import { FunnelActionType, FunnelResource } from '../../core/resources/funnel';
|
|
10
18
|
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
11
|
-
import { getGlobalApiClient } from './useApiQuery';
|
|
12
|
-
// Query keys for funnel operations
|
|
13
|
-
const funnelQueryKeys = {
|
|
14
|
-
session: (sessionId) => ['funnel', 'session', sessionId],
|
|
15
|
-
context: (sessionId) => ['funnel', 'context', sessionId],
|
|
16
|
-
};
|
|
17
19
|
/**
|
|
18
|
-
*
|
|
20
|
+
* Hook to access funnel state and methods
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const { auth, store, debugMode, updateFunnelDebugData, pluginConfig } = useTagadaContext();
|
|
25
|
-
const queryClient = useQueryClient();
|
|
26
|
-
const apiClient = getGlobalApiClient();
|
|
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]);
|
|
50
|
-
// Local state
|
|
51
|
-
const [context, setContext] = useState(null);
|
|
52
|
-
const [initializationAttempted, setInitializationAttempted] = useState(false);
|
|
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
|
|
67
|
-
const currentStepId = options.currentStepId || context?.currentStepId;
|
|
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
|
|
103
|
-
const effectiveFunnelId = urlFunnelId || options.funnelId;
|
|
104
|
-
/**
|
|
105
|
-
* Fetch debug data for the funnel debugger
|
|
106
|
-
*/
|
|
107
|
-
const fetchFunnelDebugData = useCallback(async (sessionId) => {
|
|
108
|
-
if (!debugMode) {
|
|
109
|
-
console.log('🐛 [useFunnel V2] Debug mode is OFF, skipping debug data fetch');
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
console.log('🐛 [useFunnel V2] Fetching debug data for session:', sessionId);
|
|
113
|
-
try {
|
|
114
|
-
const response = await funnelResource.getSession(sessionId, undefined, true);
|
|
115
|
-
console.log('🐛 [useFunnel V2] Debug data response:', response);
|
|
116
|
-
if (response.success && response.debugData) {
|
|
117
|
-
updateFunnelDebugData(response.debugData, null, false);
|
|
118
|
-
console.log('🐛 [useFunnel V2] Debug data updated successfully:', response.debugData);
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
console.warn('🐛 [useFunnel V2] No debug data in response');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
console.error('🐛 [useFunnel V2] Failed to fetch funnel debug data:', error);
|
|
126
|
-
}
|
|
127
|
-
}, [funnelResource, debugMode, updateFunnelDebugData]);
|
|
128
|
-
// Session query - only enabled when we have a session ID
|
|
129
|
-
const { data: sessionData, isLoading: isSessionLoading, error: sessionError, refetch: refetchSession } = useQuery({
|
|
130
|
-
queryKey: funnelQueryKeys.session(context?.sessionId || ''),
|
|
131
|
-
queryFn: async () => {
|
|
132
|
-
if (!context?.sessionId) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
const response = await funnelResource.getSession(context.sessionId);
|
|
136
|
-
if (response.success && response.context) {
|
|
137
|
-
return enrichContext(response.context);
|
|
138
|
-
}
|
|
139
|
-
throw new Error(response.error || 'Failed to fetch session');
|
|
140
|
-
},
|
|
141
|
-
enabled: !!context?.sessionId && (options.enabled !== false),
|
|
142
|
-
staleTime: 30000, // 30 seconds
|
|
143
|
-
gcTime: 5 * 60 * 1000, // 5 minutes
|
|
144
|
-
refetchOnWindowFocus: false,
|
|
145
|
-
});
|
|
146
|
-
// Initialize session mutation
|
|
147
|
-
const initializeMutation = useMutation({
|
|
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;
|
|
154
|
-
if (!auth.session?.customerId || !store?.id) {
|
|
155
|
-
isInitializingRef.current = false;
|
|
156
|
-
throw new Error('Authentication required for funnel session');
|
|
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;
|
|
164
|
-
// Check for existing session ID in URL parameters
|
|
165
|
-
let existingSessionId = currentUrlParams.get('funnelSessionId') || undefined;
|
|
166
|
-
if (existingSessionId) {
|
|
167
|
-
console.log(`🍪 Funnel: Found session ID in URL params: ${existingSessionId}`);
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
// Fallback to cookie for same-domain scenarios
|
|
171
|
-
const funnelSessionCookie = document.cookie
|
|
172
|
-
.split('; ')
|
|
173
|
-
.find(row => row.startsWith('tgd-funnel-session-id='));
|
|
174
|
-
existingSessionId = funnelSessionCookie ? funnelSessionCookie.split('=')[1] : undefined;
|
|
175
|
-
if (existingSessionId) {
|
|
176
|
-
console.log(`🍪 Funnel: Found existing session in cookie: ${existingSessionId}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
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`);
|
|
193
|
-
}
|
|
194
|
-
// Send minimal CMS session data
|
|
195
|
-
const cmsSessionData = {
|
|
196
|
-
customerId: auth.session.customerId,
|
|
197
|
-
storeId: store.id,
|
|
198
|
-
sessionId: auth.session.sessionId,
|
|
199
|
-
accountId: auth.session.accountId
|
|
200
|
-
};
|
|
201
|
-
// Get current URL for session synchronization
|
|
202
|
-
const currentUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
203
|
-
// Call API to initialize session - backend will restore existing or create new
|
|
204
|
-
const requestBody = {
|
|
205
|
-
cmsSession: cmsSessionData,
|
|
206
|
-
entryStepId, // Optional override
|
|
207
|
-
existingSessionId, // Pass existing session ID from URL or cookie
|
|
208
|
-
currentUrl // Include current URL for step tracking
|
|
209
|
-
};
|
|
210
|
-
// Only include funnelId if it's provided (for backend fallback)
|
|
211
|
-
if (currentEffectiveFunnelId) {
|
|
212
|
-
requestBody.funnelId = currentEffectiveFunnelId;
|
|
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));
|
|
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
|
-
}
|
|
230
|
-
if (response.success && response.context) {
|
|
231
|
-
return enrichContext(response.context);
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
throw new Error(response.error || 'Failed to initialize funnel session');
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
onSuccess: (newContext) => {
|
|
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);
|
|
243
|
-
setInitializationError(null);
|
|
244
|
-
// Set session cookie for persistence across page reloads
|
|
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}`);
|
|
250
|
-
// Fetch debug data if in debug mode
|
|
251
|
-
if (debugMode) {
|
|
252
|
-
void fetchFunnelDebugData(enrichedContext.sessionId);
|
|
253
|
-
}
|
|
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)`);
|
|
257
|
-
},
|
|
258
|
-
onError: (error) => {
|
|
259
|
-
// ✅ Reset initialization flag on error
|
|
260
|
-
isInitializingRef.current = false;
|
|
261
|
-
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
262
|
-
setInitializationError(errorObj);
|
|
263
|
-
console.error('Error initializing funnel session:', error);
|
|
264
|
-
if (options.onError) {
|
|
265
|
-
options.onError(errorObj);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
// Navigate mutation
|
|
270
|
-
const navigateMutation = useMutation({
|
|
271
|
-
mutationFn: async (event) => {
|
|
272
|
-
if (!context) {
|
|
273
|
-
throw new Error('Funnel session not initialized');
|
|
274
|
-
}
|
|
275
|
-
if (!context.sessionId) {
|
|
276
|
-
throw new Error('Funnel session ID missing - session may be corrupted');
|
|
277
|
-
}
|
|
278
|
-
// ✅ Automatically include currentUrl for URL-to-Step mapping
|
|
279
|
-
// User can override by providing event.currentUrl explicitly
|
|
280
|
-
const currentUrl = event.currentUrl || (typeof window !== 'undefined' ? window.location.href : undefined);
|
|
281
|
-
if (currentUrl) {
|
|
282
|
-
}
|
|
283
|
-
const requestBody = {
|
|
284
|
-
sessionId: context.sessionId,
|
|
285
|
-
event: {
|
|
286
|
-
type: event.type,
|
|
287
|
-
data: event.data,
|
|
288
|
-
timestamp: event.timestamp || new Date().toISOString(),
|
|
289
|
-
currentUrl: event.currentUrl // Preserve user override if provided
|
|
290
|
-
},
|
|
291
|
-
contextUpdates: {
|
|
292
|
-
lastActivityAt: Date.now()
|
|
293
|
-
},
|
|
294
|
-
currentUrl, // ✅ Send to backend for URL→Step mapping
|
|
295
|
-
funnelId: effectiveFunnelId || options.funnelId // ✅ Send for session recovery
|
|
296
|
-
};
|
|
297
|
-
const response = await funnelResource.navigate(requestBody);
|
|
298
|
-
if (response.success && response.result) {
|
|
299
|
-
return response.result;
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
throw new Error(response.error || 'Navigation failed');
|
|
303
|
-
}
|
|
304
|
-
},
|
|
305
|
-
onSuccess: (result) => {
|
|
306
|
-
if (!context)
|
|
307
|
-
return;
|
|
308
|
-
// 🔄 Handle session recovery (if backend created a new session)
|
|
309
|
-
let recoveredSessionId;
|
|
310
|
-
if (result.sessionId && result.sessionId !== context.sessionId) {
|
|
311
|
-
console.warn(`🔄 Funnel: Session recovered! Old: ${context.sessionId}, New: ${result.sessionId}`);
|
|
312
|
-
recoveredSessionId = result.sessionId;
|
|
313
|
-
}
|
|
314
|
-
// Update local context
|
|
315
|
-
const newContext = {
|
|
316
|
-
...context,
|
|
317
|
-
sessionId: recoveredSessionId || context.sessionId, // ✅ Use recovered session ID if provided
|
|
318
|
-
currentStepId: result.stepId,
|
|
319
|
-
previousStepId: context.currentStepId,
|
|
320
|
-
lastActivityAt: Date.now(),
|
|
321
|
-
metadata: {
|
|
322
|
-
...context.metadata,
|
|
323
|
-
lastEvent: 'navigation',
|
|
324
|
-
lastTransition: new Date().toISOString(),
|
|
325
|
-
...(recoveredSessionId ? { recovered: true, oldSessionId: context.sessionId } : {})
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
const enrichedContext = enrichContext(newContext);
|
|
329
|
-
setContext(enrichedContext);
|
|
330
|
-
// Update cookie with new session ID if recovered
|
|
331
|
-
if (recoveredSessionId) {
|
|
332
|
-
document.cookie = `funnelSessionId=${recoveredSessionId}; path=/; max-age=86400; SameSite=Lax`;
|
|
333
|
-
console.log(`🍪 Funnel: Updated cookie with recovered session ID: ${recoveredSessionId}`);
|
|
334
|
-
}
|
|
335
|
-
// Create typed navigation result
|
|
336
|
-
const navigationResult = {
|
|
337
|
-
stepId: result.stepId,
|
|
338
|
-
action: {
|
|
339
|
-
type: 'redirect', // Default action type
|
|
340
|
-
url: result.url
|
|
341
|
-
},
|
|
342
|
-
context: enrichedContext,
|
|
343
|
-
tracking: result.tracking
|
|
344
|
-
};
|
|
345
|
-
// Handle navigation callback with override capability
|
|
346
|
-
let shouldPerformDefaultNavigation = true;
|
|
347
|
-
if (options.onNavigate) {
|
|
348
|
-
const callbackResult = options.onNavigate(navigationResult);
|
|
349
|
-
if (callbackResult === false) {
|
|
350
|
-
shouldPerformDefaultNavigation = false;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
// Perform default navigation if not overridden
|
|
354
|
-
if (shouldPerformDefaultNavigation && navigationResult.action.url) {
|
|
355
|
-
// Add URL parameters for cross-domain session continuity
|
|
356
|
-
const urlWithParams = addSessionParams(navigationResult.action.url, enrichedContext.sessionId, effectiveFunnelId || options.funnelId);
|
|
357
|
-
const updatedAction = { ...navigationResult.action, url: urlWithParams };
|
|
358
|
-
performNavigation(updatedAction);
|
|
359
|
-
}
|
|
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
|
-
});
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
onError: (error) => {
|
|
377
|
-
console.error('Funnel navigation error:', error);
|
|
378
|
-
if (options.onError) {
|
|
379
|
-
options.onError(error instanceof Error ? error : new Error(String(error)));
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
// Update context mutation
|
|
384
|
-
const updateContextMutation = useMutation({
|
|
385
|
-
mutationFn: async (updates) => {
|
|
386
|
-
if (!context) {
|
|
387
|
-
throw new Error('Funnel session not initialized');
|
|
388
|
-
}
|
|
389
|
-
const requestBody = {
|
|
390
|
-
contextUpdates: updates
|
|
391
|
-
};
|
|
392
|
-
const response = await funnelResource.updateContext(context.sessionId, requestBody);
|
|
393
|
-
if (response.success) {
|
|
394
|
-
return updates;
|
|
395
|
-
}
|
|
396
|
-
else {
|
|
397
|
-
throw new Error(response.error || 'Context update failed');
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
onSuccess: (updates) => {
|
|
401
|
-
if (!context)
|
|
402
|
-
return;
|
|
403
|
-
const updatedContext = {
|
|
404
|
-
...context,
|
|
405
|
-
...updates,
|
|
406
|
-
lastActivityAt: Date.now()
|
|
407
|
-
};
|
|
408
|
-
const enrichedContext = enrichContext(updatedContext);
|
|
409
|
-
setContext(enrichedContext);
|
|
410
|
-
console.log(`🍪 Funnel: Updated context for step ${context.currentStepId}`);
|
|
411
|
-
// Invalidate session query
|
|
412
|
-
void queryClient.invalidateQueries({
|
|
413
|
-
queryKey: funnelQueryKeys.session(context.sessionId)
|
|
414
|
-
});
|
|
415
|
-
},
|
|
416
|
-
onError: (error) => {
|
|
417
|
-
console.error('Error updating funnel context:', error);
|
|
418
|
-
if (options.onError) {
|
|
419
|
-
options.onError(error instanceof Error ? error : new Error(String(error)));
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
// End session mutation
|
|
424
|
-
const endSessionMutation = useMutation({
|
|
425
|
-
mutationFn: async () => {
|
|
426
|
-
if (!context)
|
|
427
|
-
return;
|
|
428
|
-
await funnelResource.endSession(context.sessionId);
|
|
429
|
-
},
|
|
430
|
-
onSuccess: () => {
|
|
431
|
-
if (!context)
|
|
432
|
-
return;
|
|
433
|
-
console.log(`🍪 Funnel: Ended session ${context.sessionId}`);
|
|
434
|
-
setContext(null);
|
|
435
|
-
// Reset the processed URL funnelId ref to allow re-initialization
|
|
436
|
-
lastProcessedUrlFunnelIdRef.current = undefined;
|
|
437
|
-
// Clear queries
|
|
438
|
-
queryClient.removeQueries({
|
|
439
|
-
queryKey: funnelQueryKeys.session(context.sessionId)
|
|
440
|
-
});
|
|
441
|
-
},
|
|
442
|
-
onError: (error) => {
|
|
443
|
-
console.error('Error ending funnel session:', error);
|
|
444
|
-
// Don't throw here - session ending is best effort
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
/**
|
|
448
|
-
* Set funnel session cookie
|
|
449
|
-
*/
|
|
450
|
-
const setSessionCookie = useCallback((sessionId) => {
|
|
451
|
-
const maxAge = 24 * 60 * 60; // 24 hours
|
|
452
|
-
const expires = new Date(Date.now() + maxAge * 1000).toUTCString();
|
|
453
|
-
// Set cookie for same-domain scenarios
|
|
454
|
-
document.cookie = `tgd-funnel-session-id=${sessionId}; path=/; expires=${expires}; SameSite=Lax`;
|
|
455
|
-
console.log(`🍪 Funnel: Set session cookie: ${sessionId}`);
|
|
456
|
-
}, []);
|
|
457
|
-
/**
|
|
458
|
-
* Add session parameters to URL for cross-domain continuity
|
|
459
|
-
*/
|
|
460
|
-
const addSessionParams = useCallback((url, sessionId, funnelId) => {
|
|
461
|
-
try {
|
|
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);
|
|
466
|
-
urlObj.searchParams.set('funnelSessionId', sessionId);
|
|
467
|
-
if (funnelId) {
|
|
468
|
-
urlObj.searchParams.set('funnelId', funnelId);
|
|
469
|
-
}
|
|
470
|
-
return urlObj.toString();
|
|
471
|
-
}
|
|
472
|
-
catch (error) {
|
|
473
|
-
console.warn('Failed to add session params to URL:', error);
|
|
474
|
-
return url; // Return original URL if parsing fails
|
|
475
|
-
}
|
|
476
|
-
}, []);
|
|
477
|
-
/**
|
|
478
|
-
* Perform navigation based on action type
|
|
479
|
-
*/
|
|
480
|
-
const performNavigation = useCallback((action) => {
|
|
481
|
-
if (!action.url)
|
|
482
|
-
return;
|
|
483
|
-
// Handle relative URLs by making them absolute
|
|
484
|
-
let targetUrl = action.url;
|
|
485
|
-
if (targetUrl.startsWith('/') && !targetUrl.startsWith('//')) {
|
|
486
|
-
// Relative URL - use current origin
|
|
487
|
-
targetUrl = window.location.origin + targetUrl;
|
|
488
|
-
}
|
|
489
|
-
switch (action.type) {
|
|
490
|
-
case 'redirect':
|
|
491
|
-
window.location.href = targetUrl;
|
|
492
|
-
break;
|
|
493
|
-
case 'replace':
|
|
494
|
-
window.location.replace(targetUrl);
|
|
495
|
-
break;
|
|
496
|
-
case 'push':
|
|
497
|
-
window.history.pushState({}, '', targetUrl);
|
|
498
|
-
// Trigger a popstate event to update React Router
|
|
499
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
500
|
-
break;
|
|
501
|
-
case 'external':
|
|
502
|
-
window.open(targetUrl, '_blank');
|
|
503
|
-
break;
|
|
504
|
-
case 'none':
|
|
505
|
-
// No navigation needed
|
|
506
|
-
break;
|
|
507
|
-
default:
|
|
508
|
-
console.warn(`Unknown navigation action type: ${action.type}`);
|
|
509
|
-
break;
|
|
510
|
-
}
|
|
511
|
-
}, []);
|
|
512
|
-
// Public API methods
|
|
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
|
-
}
|
|
519
|
-
setInitializationAttempted(true);
|
|
520
|
-
await initializeMutation.mutateAsync(entryStepId);
|
|
521
|
-
}, [initializeMutation]);
|
|
522
|
-
const next = useCallback(async (event) => {
|
|
523
|
-
return navigateMutation.mutateAsync(event);
|
|
524
|
-
}, [navigateMutation]);
|
|
525
|
-
const goToStep = useCallback(async (stepId) => {
|
|
526
|
-
return next({
|
|
527
|
-
type: FunnelActionType.DIRECT_NAVIGATION,
|
|
528
|
-
data: { targetStepId: stepId },
|
|
529
|
-
timestamp: new Date().toISOString()
|
|
530
|
-
});
|
|
531
|
-
}, [next]);
|
|
532
|
-
const updateContext = useCallback(async (updates) => {
|
|
533
|
-
await updateContextMutation.mutateAsync(updates);
|
|
534
|
-
}, [updateContextMutation]);
|
|
535
|
-
const endSession = useCallback(async () => {
|
|
536
|
-
await endSessionMutation.mutateAsync();
|
|
537
|
-
}, [endSessionMutation]);
|
|
538
|
-
const retryInitialization = useCallback(async () => {
|
|
539
|
-
setInitializationAttempted(false);
|
|
540
|
-
setInitializationError(null);
|
|
541
|
-
await initializeSession();
|
|
542
|
-
}, [initializeSession]);
|
|
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
|
-
*/
|
|
556
|
-
useEffect(() => {
|
|
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
|
|
604
|
-
initializeSession().catch(error => {
|
|
605
|
-
console.error('❌ Funnel: Failed to reset session:', error);
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}, [
|
|
609
|
-
options.autoInitialize,
|
|
610
|
-
options.funnelId,
|
|
611
|
-
urlFunnelId,
|
|
612
|
-
context?.funnelId,
|
|
613
|
-
context?.sessionId,
|
|
614
|
-
initializationAttempted,
|
|
615
|
-
auth.session?.customerId,
|
|
616
|
-
store?.id,
|
|
617
|
-
initializeMutation.isPending,
|
|
618
|
-
initializeSession,
|
|
619
|
-
hasExistingSessionCookie,
|
|
620
|
-
queryClient
|
|
621
|
-
]);
|
|
622
|
-
// Sync session data from query to local state
|
|
623
|
-
useEffect(() => {
|
|
624
|
-
if (sessionData && sessionData !== context) {
|
|
625
|
-
setContext(sessionData);
|
|
626
|
-
}
|
|
627
|
-
}, [sessionData, context]);
|
|
628
|
-
const isLoading = initializeMutation.isPending || navigateMutation.isPending || updateContextMutation.isPending;
|
|
629
|
-
const isInitialized = !!context;
|
|
630
|
-
const isNavigating = navigateMutation.isPending; // Explicit navigation state
|
|
631
|
-
return {
|
|
632
|
-
next,
|
|
633
|
-
goToStep,
|
|
634
|
-
updateContext,
|
|
635
|
-
currentStep: {
|
|
636
|
-
id: currentStepId || 'unknown'
|
|
637
|
-
},
|
|
638
|
-
context,
|
|
639
|
-
isLoading,
|
|
640
|
-
isInitialized,
|
|
641
|
-
isNavigating, // Expose isNavigating
|
|
642
|
-
initializeSession,
|
|
643
|
-
endSession,
|
|
644
|
-
retryInitialization,
|
|
645
|
-
initializationError,
|
|
646
|
-
isSessionLoading,
|
|
647
|
-
sessionError,
|
|
648
|
-
refetch: () => void refetchSession()
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* Simplified funnel hook for basic step tracking (v2)
|
|
22
|
+
* This hook simply returns the funnel state from TagadaProvider.
|
|
23
|
+
* All complex logic is handled at the provider level.
|
|
24
|
+
*
|
|
25
|
+
* @returns FunnelContextValue with state and methods
|
|
653
26
|
*/
|
|
654
|
-
export function
|
|
655
|
-
const funnel =
|
|
656
|
-
|
|
657
|
-
currentStepId: initialStepId,
|
|
658
|
-
autoInitialize: true
|
|
659
|
-
});
|
|
660
|
-
return {
|
|
661
|
-
currentStepId: funnel.currentStep.id,
|
|
662
|
-
next: funnel.next,
|
|
663
|
-
goToStep: funnel.goToStep,
|
|
664
|
-
isLoading: funnel.isLoading,
|
|
665
|
-
context: funnel.context
|
|
666
|
-
};
|
|
27
|
+
export function useFunnel() {
|
|
28
|
+
const { funnel } = useTagadaContext();
|
|
29
|
+
return funnel;
|
|
667
30
|
}
|