@tagadapay/plugin-sdk 2.8.9 → 3.0.1
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 +22 -0
- package/dist/v2/core/client.js +90 -1
- package/dist/v2/core/config/environment.d.ts +24 -2
- package/dist/v2/core/config/environment.js +58 -25
- 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.d.ts +5 -0
- package/dist/v2/core/resources/apiClient.js +47 -0
- package/dist/v2/core/resources/funnel.d.ts +8 -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.d.ts +1 -0
- package/dist/v2/core/utils/pluginConfig.js +84 -22
- 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 +3 -2
- package/dist/v2/index.js +1 -1
- package/dist/v2/react/components/ApplePayButton.js +1 -1
- 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 -38
- package/dist/v2/react/hooks/useFunnel.js +22 -660
- 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 +10 -58
- package/dist/v2/react/hooks/useOffersQuery.js +110 -8
- 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 +7 -4
- package/dist/v2/react/index.js +4 -2
- package/dist/v2/react/providers/TagadaProvider.d.ts +45 -2
- package/dist/v2/react/providers/TagadaProvider.js +116 -3
- 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,668 +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 } = useTagadaContext();
|
|
25
|
-
const queryClient = useQueryClient();
|
|
26
|
-
const apiClient = getGlobalApiClient();
|
|
27
|
-
const funnelResource = useMemo(() => new FunnelResource(apiClient), [apiClient]);
|
|
28
|
-
// Local state
|
|
29
|
-
const [context, setContext] = useState(null);
|
|
30
|
-
const [initializationAttempted, setInitializationAttempted] = useState(false);
|
|
31
|
-
const [initializationError, setInitializationError] = useState(null);
|
|
32
|
-
// Track the last processed URL funnelId to avoid re-processing on context changes
|
|
33
|
-
const lastProcessedUrlFunnelIdRef = useRef(undefined);
|
|
34
|
-
// Check if we have an existing session in cookies (to avoid unnecessary re-initialization)
|
|
35
|
-
const hasExistingSessionCookie = useMemo(() => {
|
|
36
|
-
if (typeof document === 'undefined')
|
|
37
|
-
return false;
|
|
38
|
-
const funnelSessionCookie = document.cookie
|
|
39
|
-
.split('; ')
|
|
40
|
-
.find(row => row.startsWith('tgd-funnel-session-id='));
|
|
41
|
-
return !!funnelSessionCookie;
|
|
42
|
-
}, []); // Only check once on mount
|
|
43
|
-
const currentStepId = options.currentStepId || context?.currentStepId;
|
|
44
|
-
// Check for URL parameter overrides - recalculate on every render to detect URL changes
|
|
45
|
-
// This is cheap and ensures we always have the latest URL params
|
|
46
|
-
const [currentUrl, setCurrentUrl] = useState(() => typeof window !== 'undefined' ? window.location.href : '');
|
|
47
|
-
// Listen for URL changes (navigation)
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (typeof window === 'undefined')
|
|
50
|
-
return;
|
|
51
|
-
const updateUrl = () => {
|
|
52
|
-
setCurrentUrl(window.location.href);
|
|
53
|
-
};
|
|
54
|
-
// Listen for popstate (back/forward buttons) and pushState/replaceState
|
|
55
|
-
window.addEventListener('popstate', updateUrl);
|
|
56
|
-
// Also update on any navigation
|
|
57
|
-
const originalPushState = window.history.pushState;
|
|
58
|
-
const originalReplaceState = window.history.replaceState;
|
|
59
|
-
window.history.pushState = function (...args) {
|
|
60
|
-
originalPushState.apply(window.history, args);
|
|
61
|
-
updateUrl();
|
|
62
|
-
};
|
|
63
|
-
window.history.replaceState = function (...args) {
|
|
64
|
-
originalReplaceState.apply(window.history, args);
|
|
65
|
-
updateUrl();
|
|
66
|
-
};
|
|
67
|
-
return () => {
|
|
68
|
-
window.removeEventListener('popstate', updateUrl);
|
|
69
|
-
window.history.pushState = originalPushState;
|
|
70
|
-
window.history.replaceState = originalReplaceState;
|
|
71
|
-
};
|
|
72
|
-
}, []);
|
|
73
|
-
const urlFunnelId = useMemo(() => {
|
|
74
|
-
if (typeof window === 'undefined')
|
|
75
|
-
return undefined;
|
|
76
|
-
const params = new URLSearchParams(window.location.search);
|
|
77
|
-
return params.get('funnelId') || undefined;
|
|
78
|
-
}, [currentUrl]); // Re-compute when URL changes
|
|
79
|
-
const effectiveFunnelId = urlFunnelId || options.funnelId;
|
|
80
|
-
/**
|
|
81
|
-
* Fetch debug data for the funnel debugger
|
|
82
|
-
*/
|
|
83
|
-
const fetchFunnelDebugData = useCallback(async (sessionId) => {
|
|
84
|
-
if (!debugMode) {
|
|
85
|
-
console.log('🐛 [useFunnel V2] Debug mode is OFF, skipping debug data fetch');
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
console.log('🐛 [useFunnel V2] Fetching debug data for session:', sessionId);
|
|
89
|
-
try {
|
|
90
|
-
const response = await funnelResource.getSession(sessionId, undefined, true);
|
|
91
|
-
console.log('🐛 [useFunnel V2] Debug data response:', response);
|
|
92
|
-
if (response.success && response.debugData) {
|
|
93
|
-
updateFunnelDebugData(response.debugData, null, false);
|
|
94
|
-
console.log('🐛 [useFunnel V2] Debug data updated successfully:', response.debugData);
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
console.warn('🐛 [useFunnel V2] No debug data in response');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
console.error('🐛 [useFunnel V2] Failed to fetch funnel debug data:', error);
|
|
102
|
-
}
|
|
103
|
-
}, [funnelResource, debugMode, updateFunnelDebugData]);
|
|
104
|
-
// Session query - only enabled when we have a session ID
|
|
105
|
-
const { data: sessionData, isLoading: isSessionLoading, error: sessionError, refetch: refetchSession } = useQuery({
|
|
106
|
-
queryKey: funnelQueryKeys.session(context?.sessionId || ''),
|
|
107
|
-
queryFn: async () => {
|
|
108
|
-
if (!context?.sessionId) {
|
|
109
|
-
console.warn('🍪 Funnel: No session ID available for query');
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
console.log(`🍪 Funnel: Fetching session data for ID: ${context.sessionId}`);
|
|
113
|
-
const response = await funnelResource.getSession(context.sessionId);
|
|
114
|
-
console.log(`🍪 Funnel: Session fetch response:`, response);
|
|
115
|
-
if (response.success && response.context) {
|
|
116
|
-
return response.context;
|
|
117
|
-
}
|
|
118
|
-
throw new Error(response.error || 'Failed to fetch session');
|
|
119
|
-
},
|
|
120
|
-
enabled: !!context?.sessionId && (options.enabled !== false),
|
|
121
|
-
staleTime: 30000, // 30 seconds
|
|
122
|
-
gcTime: 5 * 60 * 1000, // 5 minutes
|
|
123
|
-
refetchOnWindowFocus: false,
|
|
124
|
-
});
|
|
125
|
-
// Initialize session mutation
|
|
126
|
-
const initializeMutation = useMutation({
|
|
127
|
-
mutationFn: async (entryStepId) => {
|
|
128
|
-
if (!auth.session?.customerId || !store?.id) {
|
|
129
|
-
throw new Error('Authentication required for funnel session');
|
|
130
|
-
}
|
|
131
|
-
// Get CURRENT URL params (not cached) to ensure we have latest values
|
|
132
|
-
const currentUrlParams = typeof window !== 'undefined'
|
|
133
|
-
? new URLSearchParams(window.location.search)
|
|
134
|
-
: new URLSearchParams();
|
|
135
|
-
const currentUrlFunnelId = currentUrlParams.get('funnelId') || undefined;
|
|
136
|
-
const currentEffectiveFunnelId = currentUrlFunnelId || options.funnelId;
|
|
137
|
-
// Check for existing session ID in URL parameters
|
|
138
|
-
let existingSessionId = currentUrlParams.get('funnelSessionId') || undefined;
|
|
139
|
-
if (existingSessionId) {
|
|
140
|
-
console.log(`🍪 Funnel: Found session ID in URL params: ${existingSessionId}`);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
// Fallback to cookie for same-domain scenarios
|
|
144
|
-
const funnelSessionCookie = document.cookie
|
|
145
|
-
.split('; ')
|
|
146
|
-
.find(row => row.startsWith('tgd-funnel-session-id='));
|
|
147
|
-
existingSessionId = funnelSessionCookie ? funnelSessionCookie.split('=')[1] : undefined;
|
|
148
|
-
if (existingSessionId) {
|
|
149
|
-
console.log(`🍪 Funnel: Found existing session in cookie: ${existingSessionId}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// Log initialization strategy
|
|
153
|
-
console.log(`🍪 Funnel: Initialize request:`);
|
|
154
|
-
console.log(` - Existing session ID: ${existingSessionId || 'none'}`);
|
|
155
|
-
console.log(` - URL funnelId: ${currentUrlFunnelId || 'none'}`);
|
|
156
|
-
console.log(` - Hook funnelId: ${options.funnelId || 'none'}`);
|
|
157
|
-
console.log(` - Effective funnelId to send: ${currentEffectiveFunnelId || 'none (will use existing or backend default)'}`);
|
|
158
|
-
if (existingSessionId && !currentEffectiveFunnelId) {
|
|
159
|
-
console.log(` → Strategy: REUSE existing session from cookie`);
|
|
160
|
-
}
|
|
161
|
-
else if (existingSessionId && currentEffectiveFunnelId) {
|
|
162
|
-
console.log(` → Strategy: VALIDATE existing session matches funnelId, or create new`);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
console.log(` → Strategy: CREATE new session`);
|
|
166
|
-
}
|
|
167
|
-
// Send minimal CMS session data
|
|
168
|
-
const cmsSessionData = {
|
|
169
|
-
customerId: auth.session.customerId,
|
|
170
|
-
storeId: store.id,
|
|
171
|
-
sessionId: auth.session.sessionId,
|
|
172
|
-
accountId: auth.session.accountId
|
|
173
|
-
};
|
|
174
|
-
// Get current URL for session synchronization
|
|
175
|
-
const currentUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
176
|
-
// Call API to initialize session - backend will restore existing or create new
|
|
177
|
-
const requestBody = {
|
|
178
|
-
cmsSession: cmsSessionData,
|
|
179
|
-
entryStepId, // Optional override
|
|
180
|
-
existingSessionId, // Pass existing session ID from URL or cookie
|
|
181
|
-
currentUrl // Include current URL for step tracking
|
|
182
|
-
};
|
|
183
|
-
// Only include funnelId if it's provided (for backend fallback)
|
|
184
|
-
if (currentEffectiveFunnelId) {
|
|
185
|
-
requestBody.funnelId = currentEffectiveFunnelId;
|
|
186
|
-
}
|
|
187
|
-
console.log(`📤 Funnel: Sending initialize request to backend:`);
|
|
188
|
-
console.log(` Request body:`, JSON.stringify({
|
|
189
|
-
...requestBody,
|
|
190
|
-
cmsSession: { ...cmsSessionData, sessionId: `${cmsSessionData.sessionId.substring(0, 20)}...` }
|
|
191
|
-
}, null, 2));
|
|
192
|
-
const response = await funnelResource.initialize(requestBody);
|
|
193
|
-
console.log(`📥 Funnel: Received response from backend:`);
|
|
194
|
-
console.log(` Success: ${response.success}`);
|
|
195
|
-
if (response.context) {
|
|
196
|
-
console.log(` Returned session ID: ${response.context.sessionId}`);
|
|
197
|
-
console.log(` Returned funnel ID: ${response.context.funnelId}`);
|
|
198
|
-
console.log(` Is same session? ${response.context.sessionId === existingSessionId ? '✅ YES (reused)' : '❌ NO (new session created)'}`);
|
|
199
|
-
}
|
|
200
|
-
if (response.error) {
|
|
201
|
-
console.log(` Error: ${response.error}`);
|
|
202
|
-
}
|
|
203
|
-
if (response.success && response.context) {
|
|
204
|
-
return response.context;
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
throw new Error(response.error || 'Failed to initialize funnel session');
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
onSuccess: (newContext) => {
|
|
211
|
-
setContext(newContext);
|
|
212
|
-
setInitializationError(null);
|
|
213
|
-
// Set session cookie for persistence across page reloads
|
|
214
|
-
setSessionCookie(newContext.sessionId);
|
|
215
|
-
console.log(`✅ Funnel: Session initialized/loaded successfully`);
|
|
216
|
-
console.log(` - Session ID: ${newContext.sessionId}`);
|
|
217
|
-
console.log(` - Funnel ID: ${newContext.funnelId}`);
|
|
218
|
-
console.log(` - Current Step: ${newContext.currentStepId}`);
|
|
219
|
-
// Fetch debug data if in debug mode
|
|
220
|
-
if (debugMode) {
|
|
221
|
-
void fetchFunnelDebugData(newContext.sessionId);
|
|
222
|
-
}
|
|
223
|
-
// Invalidate session query to refetch with new session ID
|
|
224
|
-
void queryClient.invalidateQueries({
|
|
225
|
-
queryKey: funnelQueryKeys.session(newContext.sessionId)
|
|
226
|
-
});
|
|
227
|
-
},
|
|
228
|
-
onError: (error) => {
|
|
229
|
-
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
230
|
-
setInitializationError(errorObj);
|
|
231
|
-
console.error('Error initializing funnel session:', error);
|
|
232
|
-
if (options.onError) {
|
|
233
|
-
options.onError(errorObj);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
// Navigate mutation
|
|
238
|
-
const navigateMutation = useMutation({
|
|
239
|
-
mutationFn: async (event) => {
|
|
240
|
-
if (!context) {
|
|
241
|
-
throw new Error('Funnel session not initialized');
|
|
242
|
-
}
|
|
243
|
-
if (!context.sessionId) {
|
|
244
|
-
throw new Error('Funnel session ID missing - session may be corrupted');
|
|
245
|
-
}
|
|
246
|
-
// ✅ Automatically include currentUrl for URL-to-Step mapping
|
|
247
|
-
// User can override by providing event.currentUrl explicitly
|
|
248
|
-
const currentUrl = event.currentUrl || (typeof window !== 'undefined' ? window.location.href : undefined);
|
|
249
|
-
console.log(`🍪 Funnel: Navigating with session ID: ${context.sessionId}`);
|
|
250
|
-
if (currentUrl) {
|
|
251
|
-
console.log(`🍪 Funnel: Current URL for sync: ${currentUrl}`);
|
|
252
|
-
}
|
|
253
|
-
const requestBody = {
|
|
254
|
-
sessionId: context.sessionId,
|
|
255
|
-
event: {
|
|
256
|
-
type: event.type,
|
|
257
|
-
data: event.data,
|
|
258
|
-
timestamp: event.timestamp || new Date().toISOString(),
|
|
259
|
-
currentUrl: event.currentUrl // Preserve user override if provided
|
|
260
|
-
},
|
|
261
|
-
contextUpdates: {
|
|
262
|
-
lastActivityAt: Date.now()
|
|
263
|
-
},
|
|
264
|
-
currentUrl, // ✅ Send to backend for URL→Step mapping
|
|
265
|
-
funnelId: effectiveFunnelId || options.funnelId // ✅ Send for session recovery
|
|
266
|
-
};
|
|
267
|
-
const response = await funnelResource.navigate(requestBody);
|
|
268
|
-
if (response.success && response.result) {
|
|
269
|
-
return response.result;
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
throw new Error(response.error || 'Navigation failed');
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
onSuccess: (result) => {
|
|
276
|
-
if (!context)
|
|
277
|
-
return;
|
|
278
|
-
// 🔄 Handle session recovery (if backend created a new session)
|
|
279
|
-
let recoveredSessionId;
|
|
280
|
-
if (result.sessionId && result.sessionId !== context.sessionId) {
|
|
281
|
-
console.warn(`🔄 Funnel: Session recovered! Old: ${context.sessionId}, New: ${result.sessionId}`);
|
|
282
|
-
recoveredSessionId = result.sessionId;
|
|
283
|
-
}
|
|
284
|
-
// Update local context
|
|
285
|
-
const newContext = {
|
|
286
|
-
...context,
|
|
287
|
-
sessionId: recoveredSessionId || context.sessionId, // ✅ Use recovered session ID if provided
|
|
288
|
-
currentStepId: result.stepId,
|
|
289
|
-
previousStepId: context.currentStepId,
|
|
290
|
-
lastActivityAt: Date.now(),
|
|
291
|
-
metadata: {
|
|
292
|
-
...context.metadata,
|
|
293
|
-
lastEvent: 'navigation',
|
|
294
|
-
lastTransition: new Date().toISOString(),
|
|
295
|
-
...(recoveredSessionId ? { recovered: true, oldSessionId: context.sessionId } : {})
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
setContext(newContext);
|
|
299
|
-
// Update cookie with new session ID if recovered
|
|
300
|
-
if (recoveredSessionId) {
|
|
301
|
-
document.cookie = `funnelSessionId=${recoveredSessionId}; path=/; max-age=86400; SameSite=Lax`;
|
|
302
|
-
console.log(`🍪 Funnel: Updated cookie with recovered session ID: ${recoveredSessionId}`);
|
|
303
|
-
}
|
|
304
|
-
// Create typed navigation result
|
|
305
|
-
const navigationResult = {
|
|
306
|
-
stepId: result.stepId,
|
|
307
|
-
action: {
|
|
308
|
-
type: 'redirect', // Default action type
|
|
309
|
-
url: result.url
|
|
310
|
-
},
|
|
311
|
-
context: newContext,
|
|
312
|
-
tracking: result.tracking
|
|
313
|
-
};
|
|
314
|
-
// Handle navigation callback with override capability
|
|
315
|
-
let shouldPerformDefaultNavigation = true;
|
|
316
|
-
if (options.onNavigate) {
|
|
317
|
-
const callbackResult = options.onNavigate(navigationResult);
|
|
318
|
-
if (callbackResult === false) {
|
|
319
|
-
shouldPerformDefaultNavigation = false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
// Perform default navigation if not overridden
|
|
323
|
-
if (shouldPerformDefaultNavigation && navigationResult.action.url) {
|
|
324
|
-
// Add URL parameters for cross-domain session continuity
|
|
325
|
-
const urlWithParams = addSessionParams(navigationResult.action.url, newContext.sessionId, effectiveFunnelId || options.funnelId);
|
|
326
|
-
const updatedAction = { ...navigationResult.action, url: urlWithParams };
|
|
327
|
-
performNavigation(updatedAction);
|
|
328
|
-
}
|
|
329
|
-
console.log(`🍪 Funnel: Navigated from ${context.currentStepId} to ${result.stepId}`);
|
|
330
|
-
// Fetch debug data if in debug mode
|
|
331
|
-
if (debugMode) {
|
|
332
|
-
void fetchFunnelDebugData(newContext.sessionId);
|
|
333
|
-
}
|
|
334
|
-
// Invalidate and refetch session data
|
|
335
|
-
void queryClient.invalidateQueries({
|
|
336
|
-
queryKey: funnelQueryKeys.session(newContext.sessionId)
|
|
337
|
-
});
|
|
338
|
-
},
|
|
339
|
-
onError: (error) => {
|
|
340
|
-
console.error('Funnel navigation error:', error);
|
|
341
|
-
if (options.onError) {
|
|
342
|
-
options.onError(error instanceof Error ? error : new Error(String(error)));
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
// Update context mutation
|
|
347
|
-
const updateContextMutation = useMutation({
|
|
348
|
-
mutationFn: async (updates) => {
|
|
349
|
-
if (!context) {
|
|
350
|
-
throw new Error('Funnel session not initialized');
|
|
351
|
-
}
|
|
352
|
-
const requestBody = {
|
|
353
|
-
contextUpdates: updates
|
|
354
|
-
};
|
|
355
|
-
const response = await funnelResource.updateContext(context.sessionId, requestBody);
|
|
356
|
-
if (response.success) {
|
|
357
|
-
return updates;
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
throw new Error(response.error || 'Context update failed');
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
|
-
onSuccess: (updates) => {
|
|
364
|
-
if (!context)
|
|
365
|
-
return;
|
|
366
|
-
const updatedContext = {
|
|
367
|
-
...context,
|
|
368
|
-
...updates,
|
|
369
|
-
lastActivityAt: Date.now()
|
|
370
|
-
};
|
|
371
|
-
setContext(updatedContext);
|
|
372
|
-
console.log(`🍪 Funnel: Updated context for step ${context.currentStepId}`);
|
|
373
|
-
// Invalidate session query
|
|
374
|
-
void queryClient.invalidateQueries({
|
|
375
|
-
queryKey: funnelQueryKeys.session(context.sessionId)
|
|
376
|
-
});
|
|
377
|
-
},
|
|
378
|
-
onError: (error) => {
|
|
379
|
-
console.error('Error updating funnel context:', error);
|
|
380
|
-
if (options.onError) {
|
|
381
|
-
options.onError(error instanceof Error ? error : new Error(String(error)));
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
// End session mutation
|
|
386
|
-
const endSessionMutation = useMutation({
|
|
387
|
-
mutationFn: async () => {
|
|
388
|
-
if (!context)
|
|
389
|
-
return;
|
|
390
|
-
await funnelResource.endSession(context.sessionId);
|
|
391
|
-
},
|
|
392
|
-
onSuccess: () => {
|
|
393
|
-
if (!context)
|
|
394
|
-
return;
|
|
395
|
-
console.log(`🍪 Funnel: Ended session ${context.sessionId}`);
|
|
396
|
-
setContext(null);
|
|
397
|
-
// Reset the processed URL funnelId ref to allow re-initialization
|
|
398
|
-
lastProcessedUrlFunnelIdRef.current = undefined;
|
|
399
|
-
// Clear queries
|
|
400
|
-
queryClient.removeQueries({
|
|
401
|
-
queryKey: funnelQueryKeys.session(context.sessionId)
|
|
402
|
-
});
|
|
403
|
-
},
|
|
404
|
-
onError: (error) => {
|
|
405
|
-
console.error('Error ending funnel session:', error);
|
|
406
|
-
// Don't throw here - session ending is best effort
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
/**
|
|
410
|
-
* Set funnel session cookie
|
|
411
|
-
*/
|
|
412
|
-
const setSessionCookie = useCallback((sessionId) => {
|
|
413
|
-
const maxAge = 24 * 60 * 60; // 24 hours
|
|
414
|
-
const expires = new Date(Date.now() + maxAge * 1000).toUTCString();
|
|
415
|
-
// Set cookie for same-domain scenarios
|
|
416
|
-
document.cookie = `tgd-funnel-session-id=${sessionId}; path=/; expires=${expires}; SameSite=Lax`;
|
|
417
|
-
console.log(`🍪 Funnel: Set session cookie: ${sessionId}`);
|
|
418
|
-
}, []);
|
|
419
|
-
/**
|
|
420
|
-
* Add session parameters to URL for cross-domain continuity
|
|
421
|
-
*/
|
|
422
|
-
const addSessionParams = useCallback((url, sessionId, funnelId) => {
|
|
423
|
-
try {
|
|
424
|
-
const urlObj = new URL(url);
|
|
425
|
-
urlObj.searchParams.set('funnelSessionId', sessionId);
|
|
426
|
-
if (funnelId) {
|
|
427
|
-
urlObj.searchParams.set('funnelId', funnelId);
|
|
428
|
-
}
|
|
429
|
-
const urlWithParams = urlObj.toString();
|
|
430
|
-
console.log(`🍪 Funnel: Added session params to URL: ${url} → ${urlWithParams}`);
|
|
431
|
-
return urlWithParams;
|
|
432
|
-
}
|
|
433
|
-
catch (error) {
|
|
434
|
-
console.warn('Failed to add session params to URL:', error);
|
|
435
|
-
return url; // Return original URL if parsing fails
|
|
436
|
-
}
|
|
437
|
-
}, []);
|
|
438
|
-
/**
|
|
439
|
-
* Perform navigation based on action type
|
|
440
|
-
*/
|
|
441
|
-
const performNavigation = useCallback((action) => {
|
|
442
|
-
if (!action.url)
|
|
443
|
-
return;
|
|
444
|
-
// Handle relative URLs by making them absolute
|
|
445
|
-
let targetUrl = action.url;
|
|
446
|
-
if (targetUrl.startsWith('/') && !targetUrl.startsWith('//')) {
|
|
447
|
-
// Relative URL - use current origin
|
|
448
|
-
targetUrl = window.location.origin + targetUrl;
|
|
449
|
-
}
|
|
450
|
-
switch (action.type) {
|
|
451
|
-
case 'redirect':
|
|
452
|
-
console.log(`🍪 Funnel: Redirecting to ${targetUrl}`);
|
|
453
|
-
window.location.href = targetUrl;
|
|
454
|
-
break;
|
|
455
|
-
case 'replace':
|
|
456
|
-
console.log(`🍪 Funnel: Replacing current page with ${targetUrl}`);
|
|
457
|
-
window.location.replace(targetUrl);
|
|
458
|
-
break;
|
|
459
|
-
case 'push':
|
|
460
|
-
console.log(`🍪 Funnel: Pushing to history: ${targetUrl}`);
|
|
461
|
-
window.history.pushState({}, '', targetUrl);
|
|
462
|
-
// Trigger a popstate event to update React Router
|
|
463
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
464
|
-
break;
|
|
465
|
-
case 'external':
|
|
466
|
-
console.log(`🍪 Funnel: Opening external URL: ${targetUrl}`);
|
|
467
|
-
window.open(targetUrl, '_blank');
|
|
468
|
-
break;
|
|
469
|
-
case 'none':
|
|
470
|
-
console.log(`🍪 Funnel: No navigation action required`);
|
|
471
|
-
break;
|
|
472
|
-
default:
|
|
473
|
-
console.warn(`🍪 Funnel: Unknown navigation action type: ${action.type}`);
|
|
474
|
-
break;
|
|
475
|
-
}
|
|
476
|
-
}, []);
|
|
477
|
-
// Public API methods
|
|
478
|
-
const initializeSession = useCallback(async (entryStepId) => {
|
|
479
|
-
setInitializationAttempted(true);
|
|
480
|
-
await initializeMutation.mutateAsync(entryStepId);
|
|
481
|
-
}, [initializeMutation]);
|
|
482
|
-
const next = useCallback(async (event) => {
|
|
483
|
-
return navigateMutation.mutateAsync(event);
|
|
484
|
-
}, [navigateMutation]);
|
|
485
|
-
const goToStep = useCallback(async (stepId) => {
|
|
486
|
-
return next({
|
|
487
|
-
type: FunnelActionType.DIRECT_NAVIGATION,
|
|
488
|
-
data: { targetStepId: stepId },
|
|
489
|
-
timestamp: new Date().toISOString()
|
|
490
|
-
});
|
|
491
|
-
}, [next]);
|
|
492
|
-
const updateContext = useCallback(async (updates) => {
|
|
493
|
-
await updateContextMutation.mutateAsync(updates);
|
|
494
|
-
}, [updateContextMutation]);
|
|
495
|
-
const endSession = useCallback(async () => {
|
|
496
|
-
await endSessionMutation.mutateAsync();
|
|
497
|
-
}, [endSessionMutation]);
|
|
498
|
-
const retryInitialization = useCallback(async () => {
|
|
499
|
-
setInitializationAttempted(false);
|
|
500
|
-
setInitializationError(null);
|
|
501
|
-
await initializeSession();
|
|
502
|
-
}, [initializeSession]);
|
|
503
|
-
/**
|
|
504
|
-
* Auto-initialization effect
|
|
505
|
-
*
|
|
506
|
-
* Handles funnel session initialization with the following logic:
|
|
507
|
-
* 1. If no session exists => Initialize with URL funnelId or hook funnelId or backend default
|
|
508
|
-
* 2. If session exists + explicit funnelId provided that differs => Reset and create new
|
|
509
|
-
* 3. If session exists + no funnelId provided => Keep existing session
|
|
510
|
-
*
|
|
511
|
-
* IMPORTANT: Only resets session if an EXPLICIT funnelId is provided that differs
|
|
512
|
-
* from the current session. If no funnelId is provided, keeps the existing session.
|
|
513
|
-
*
|
|
514
|
-
* Priority: URL funnelId > Hook funnelId > Existing session funnelId > Backend default
|
|
515
|
-
*/
|
|
516
|
-
useEffect(() => {
|
|
517
|
-
console.log('🔍 Funnel: Auto-init effect triggered');
|
|
518
|
-
console.log(` - autoInitialize: ${options.autoInitialize ?? true}`);
|
|
519
|
-
console.log(` - hasAuth: ${!!auth.session?.customerId}`);
|
|
520
|
-
console.log(` - hasStore: ${!!store?.id}`);
|
|
521
|
-
console.log(` - isPending: ${initializeMutation.isPending}`);
|
|
522
|
-
console.log(` - hasContext: ${!!context}`);
|
|
523
|
-
console.log(` - context.sessionId: ${context?.sessionId || 'none'}`);
|
|
524
|
-
console.log(` - context.funnelId: ${context?.funnelId || 'none'}`);
|
|
525
|
-
console.log(` - urlFunnelId: ${urlFunnelId || 'none'}`);
|
|
526
|
-
console.log(` - options.funnelId: ${options.funnelId || 'none'}`);
|
|
527
|
-
console.log(` - initializationAttempted: ${initializationAttempted}`);
|
|
528
|
-
console.log(` - hasExistingSessionCookie: ${hasExistingSessionCookie}`);
|
|
529
|
-
// Skip if auto-initialize is disabled
|
|
530
|
-
const autoInit = options.autoInitialize ?? true; // Default to true
|
|
531
|
-
if (!autoInit) {
|
|
532
|
-
console.log('⏭️ Funnel: Skipping - auto-initialize disabled');
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
// Skip if required dependencies are not available
|
|
536
|
-
if (!auth.session?.customerId || !store?.id) {
|
|
537
|
-
console.log('⏭️ Funnel: Skipping - missing auth or store');
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
// Skip if already initializing
|
|
541
|
-
if (initializeMutation.isPending) {
|
|
542
|
-
console.log('⏭️ Funnel: Skipping - already initializing');
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
// Determine if we have an explicit funnelId request (URL has priority)
|
|
546
|
-
const explicitFunnelId = urlFunnelId || options.funnelId;
|
|
547
|
-
console.log(` - explicitFunnelId: ${explicitFunnelId || 'none'}`);
|
|
548
|
-
// Case 1: No session exists yet - need to initialize
|
|
549
|
-
if (!context) {
|
|
550
|
-
console.log('📍 Funnel: Case 1 - No context exists');
|
|
551
|
-
// Check if we've already attempted initialization
|
|
552
|
-
if (!initializationAttempted) {
|
|
553
|
-
if (hasExistingSessionCookie) {
|
|
554
|
-
console.log('🍪 Funnel: Loading existing session from cookie...');
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
console.log('🍪 Funnel: No session found - creating new session...');
|
|
558
|
-
}
|
|
559
|
-
if (explicitFunnelId) {
|
|
560
|
-
console.log(` with funnelId: ${explicitFunnelId}`);
|
|
561
|
-
}
|
|
562
|
-
setInitializationAttempted(true);
|
|
563
|
-
initializeSession().catch(error => {
|
|
564
|
-
console.error('❌ Funnel: Auto-initialization failed:', error);
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
console.log('⏭️ Funnel: Skipping - already attempted initialization');
|
|
569
|
-
}
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
console.log('📍 Funnel: Case 2 - Context exists, checking for reset needs');
|
|
573
|
-
// Case 2: Session exists - check if we need to reset it
|
|
574
|
-
// ONLY reset if an explicit funnelId is provided AND it differs from current session
|
|
575
|
-
if (explicitFunnelId && context.funnelId && explicitFunnelId !== context.funnelId) {
|
|
576
|
-
console.log(`🔍 Funnel: Mismatch check - explicitFunnelId: ${explicitFunnelId}, context.funnelId: ${context.funnelId}`);
|
|
577
|
-
// Check if we've already processed this funnelId to prevent loops
|
|
578
|
-
if (lastProcessedUrlFunnelIdRef.current === explicitFunnelId) {
|
|
579
|
-
console.log('⏭️ Funnel: Skipping - already processed this funnelId');
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
console.log(`🔄 Funnel: Explicit funnelId mismatch detected!`);
|
|
583
|
-
console.log(` Current session funnelId: ${context.funnelId}`);
|
|
584
|
-
console.log(` Requested funnelId: ${explicitFunnelId}`);
|
|
585
|
-
console.log(` Resetting session...`);
|
|
586
|
-
// Mark this funnelId as processed
|
|
587
|
-
lastProcessedUrlFunnelIdRef.current = explicitFunnelId;
|
|
588
|
-
// Clear existing session
|
|
589
|
-
setContext(null);
|
|
590
|
-
setInitializationAttempted(false);
|
|
591
|
-
setInitializationError(null);
|
|
592
|
-
// Clear session cookie
|
|
593
|
-
document.cookie = 'tgd-funnel-session-id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
|
|
594
|
-
// Clear queries for old session
|
|
595
|
-
queryClient.removeQueries({
|
|
596
|
-
queryKey: funnelQueryKeys.session(context.sessionId)
|
|
597
|
-
});
|
|
598
|
-
// Initialize new session with correct funnelId
|
|
599
|
-
console.log(`🍪 Funnel: Creating new session with funnelId: ${explicitFunnelId}`);
|
|
600
|
-
initializeSession().catch(error => {
|
|
601
|
-
console.error('❌ Funnel: Failed to reset session:', error);
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
// Case 3: Session exists and no conflicting funnelId - keep using it
|
|
606
|
-
console.log('✅ Funnel: Case 3 - Keeping existing session (no reset needed)');
|
|
607
|
-
console.log(` - explicitFunnelId: ${explicitFunnelId || 'none (will keep existing)'}`);
|
|
608
|
-
console.log(` - context.funnelId: ${context.funnelId}`);
|
|
609
|
-
console.log(` - Match or no explicit request: keeping session ${context.sessionId}`);
|
|
610
|
-
}
|
|
611
|
-
}, [
|
|
612
|
-
options.autoInitialize,
|
|
613
|
-
options.funnelId,
|
|
614
|
-
urlFunnelId,
|
|
615
|
-
context?.funnelId,
|
|
616
|
-
context?.sessionId,
|
|
617
|
-
initializationAttempted,
|
|
618
|
-
auth.session?.customerId,
|
|
619
|
-
store?.id,
|
|
620
|
-
initializeMutation.isPending,
|
|
621
|
-
initializeSession,
|
|
622
|
-
hasExistingSessionCookie,
|
|
623
|
-
queryClient
|
|
624
|
-
]);
|
|
625
|
-
// Sync session data from query to local state
|
|
626
|
-
useEffect(() => {
|
|
627
|
-
if (sessionData && sessionData !== context) {
|
|
628
|
-
setContext(sessionData);
|
|
629
|
-
}
|
|
630
|
-
}, [sessionData, context]);
|
|
631
|
-
const isLoading = initializeMutation.isPending || navigateMutation.isPending || updateContextMutation.isPending;
|
|
632
|
-
const isInitialized = !!context;
|
|
633
|
-
return {
|
|
634
|
-
next,
|
|
635
|
-
goToStep,
|
|
636
|
-
updateContext,
|
|
637
|
-
currentStep: {
|
|
638
|
-
id: currentStepId || 'unknown'
|
|
639
|
-
},
|
|
640
|
-
context,
|
|
641
|
-
isLoading,
|
|
642
|
-
isInitialized,
|
|
643
|
-
initializeSession,
|
|
644
|
-
endSession,
|
|
645
|
-
retryInitialization,
|
|
646
|
-
initializationError,
|
|
647
|
-
isSessionLoading,
|
|
648
|
-
sessionError,
|
|
649
|
-
refetch: () => void refetchSession()
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* 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
|
|
654
26
|
*/
|
|
655
|
-
export function
|
|
656
|
-
const funnel =
|
|
657
|
-
|
|
658
|
-
currentStepId: initialStepId,
|
|
659
|
-
autoInitialize: true
|
|
660
|
-
});
|
|
661
|
-
return {
|
|
662
|
-
currentStepId: funnel.currentStep.id,
|
|
663
|
-
next: funnel.next,
|
|
664
|
-
goToStep: funnel.goToStep,
|
|
665
|
-
isLoading: funnel.isLoading,
|
|
666
|
-
context: funnel.context
|
|
667
|
-
};
|
|
27
|
+
export function useFunnel() {
|
|
28
|
+
const { funnel } = useTagadaContext();
|
|
29
|
+
return funnel;
|
|
668
30
|
}
|