@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.
Files changed (54) hide show
  1. package/README.md +14 -14
  2. package/dist/index.js +1 -1
  3. package/dist/react/hooks/usePluginConfig.d.ts +1 -0
  4. package/dist/react/hooks/usePluginConfig.js +69 -18
  5. package/dist/react/providers/TagadaProvider.js +1 -4
  6. package/dist/v2/core/client.d.ts +22 -0
  7. package/dist/v2/core/client.js +90 -1
  8. package/dist/v2/core/config/environment.d.ts +24 -2
  9. package/dist/v2/core/config/environment.js +58 -25
  10. package/dist/v2/core/funnelClient.d.ts +84 -0
  11. package/dist/v2/core/funnelClient.js +252 -0
  12. package/dist/v2/core/index.d.ts +2 -0
  13. package/dist/v2/core/index.js +3 -0
  14. package/dist/v2/core/resources/apiClient.d.ts +5 -0
  15. package/dist/v2/core/resources/apiClient.js +47 -0
  16. package/dist/v2/core/resources/funnel.d.ts +8 -0
  17. package/dist/v2/core/resources/offers.d.ts +182 -8
  18. package/dist/v2/core/resources/offers.js +25 -0
  19. package/dist/v2/core/resources/products.d.ts +5 -0
  20. package/dist/v2/core/resources/products.js +15 -1
  21. package/dist/v2/core/types.d.ts +1 -0
  22. package/dist/v2/core/utils/funnelQueryKeys.d.ts +23 -0
  23. package/dist/v2/core/utils/funnelQueryKeys.js +23 -0
  24. package/dist/v2/core/utils/index.d.ts +2 -0
  25. package/dist/v2/core/utils/index.js +2 -0
  26. package/dist/v2/core/utils/pluginConfig.d.ts +1 -0
  27. package/dist/v2/core/utils/pluginConfig.js +84 -22
  28. package/dist/v2/core/utils/sessionStorage.d.ts +20 -0
  29. package/dist/v2/core/utils/sessionStorage.js +39 -0
  30. package/dist/v2/index.d.ts +3 -2
  31. package/dist/v2/index.js +1 -1
  32. package/dist/v2/react/components/ApplePayButton.js +1 -1
  33. package/dist/v2/react/hooks/__examples__/FunnelContextExample.d.ts +3 -0
  34. package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +4 -3
  35. package/dist/v2/react/hooks/useClubOffers.d.ts +2 -2
  36. package/dist/v2/react/hooks/useFunnel.d.ts +27 -38
  37. package/dist/v2/react/hooks/useFunnel.js +22 -660
  38. package/dist/v2/react/hooks/useFunnelLegacy.d.ts +52 -0
  39. package/dist/v2/react/hooks/useFunnelLegacy.js +733 -0
  40. package/dist/v2/react/hooks/useOfferQuery.d.ts +109 -0
  41. package/dist/v2/react/hooks/useOfferQuery.js +483 -0
  42. package/dist/v2/react/hooks/useOffersQuery.d.ts +10 -58
  43. package/dist/v2/react/hooks/useOffersQuery.js +110 -8
  44. package/dist/v2/react/hooks/useProductsQuery.d.ts +1 -0
  45. package/dist/v2/react/hooks/useProductsQuery.js +10 -6
  46. package/dist/v2/react/index.d.ts +7 -4
  47. package/dist/v2/react/index.js +4 -2
  48. package/dist/v2/react/providers/TagadaProvider.d.ts +45 -2
  49. package/dist/v2/react/providers/TagadaProvider.js +116 -3
  50. package/dist/v2/standalone/index.d.ts +20 -0
  51. package/dist/v2/standalone/index.js +22 -0
  52. package/dist/v2/vue/index.d.ts +6 -0
  53. package/dist/v2/vue/index.js +10 -0
  54. package/package.json +6 -1
@@ -1,668 +1,30 @@
1
1
  /**
2
- * useFunnel Hook (v2) - TanStack Query-based funnel navigation
2
+ * useFunnel Hook - Access funnel state and navigation methods
3
3
  *
4
- * Modern implementation using TanStack Query for state management
5
- * and the v2 ApiClient for API calls.
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
- * React Hook for Funnel Navigation (Plugin SDK v2)
20
+ * Hook to access funnel state and methods
19
21
  *
20
- * Modern funnel navigation using TanStack Query for state management
21
- * and the v2 ApiClient architecture.
22
- */
23
- export function useFunnel(options = {}) {
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 useSimpleFunnel(funnelId, initialStepId) {
656
- const funnel = useFunnel({
657
- funnelId,
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
  }