@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.
Files changed (51) 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 +18 -0
  7. package/dist/v2/core/client.js +45 -0
  8. package/dist/v2/core/config/environment.d.ts +8 -0
  9. package/dist/v2/core/config/environment.js +18 -0
  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.js +1 -1
  15. package/dist/v2/core/resources/funnel.d.ts +1 -0
  16. package/dist/v2/core/resources/offers.d.ts +182 -8
  17. package/dist/v2/core/resources/offers.js +25 -0
  18. package/dist/v2/core/resources/products.d.ts +5 -0
  19. package/dist/v2/core/resources/products.js +15 -1
  20. package/dist/v2/core/types.d.ts +1 -0
  21. package/dist/v2/core/utils/funnelQueryKeys.d.ts +23 -0
  22. package/dist/v2/core/utils/funnelQueryKeys.js +23 -0
  23. package/dist/v2/core/utils/index.d.ts +2 -0
  24. package/dist/v2/core/utils/index.js +2 -0
  25. package/dist/v2/core/utils/pluginConfig.js +44 -32
  26. package/dist/v2/core/utils/sessionStorage.d.ts +20 -0
  27. package/dist/v2/core/utils/sessionStorage.js +39 -0
  28. package/dist/v2/index.d.ts +4 -2
  29. package/dist/v2/index.js +1 -1
  30. package/dist/v2/react/components/DebugDrawer.js +99 -2
  31. package/dist/v2/react/hooks/__examples__/FunnelContextExample.d.ts +3 -0
  32. package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +4 -3
  33. package/dist/v2/react/hooks/useClubOffers.d.ts +2 -2
  34. package/dist/v2/react/hooks/useFunnel.d.ts +27 -39
  35. package/dist/v2/react/hooks/useFunnel.js +22 -659
  36. package/dist/v2/react/hooks/useFunnelLegacy.d.ts +52 -0
  37. package/dist/v2/react/hooks/useFunnelLegacy.js +733 -0
  38. package/dist/v2/react/hooks/useOfferQuery.d.ts +109 -0
  39. package/dist/v2/react/hooks/useOfferQuery.js +483 -0
  40. package/dist/v2/react/hooks/useOffersQuery.d.ts +9 -75
  41. package/dist/v2/react/hooks/useProductsQuery.d.ts +1 -0
  42. package/dist/v2/react/hooks/useProductsQuery.js +10 -6
  43. package/dist/v2/react/index.d.ts +8 -4
  44. package/dist/v2/react/index.js +4 -2
  45. package/dist/v2/react/providers/TagadaProvider.d.ts +66 -5
  46. package/dist/v2/react/providers/TagadaProvider.js +120 -6
  47. package/dist/v2/standalone/index.d.ts +20 -0
  48. package/dist/v2/standalone/index.js +22 -0
  49. package/dist/v2/vue/index.d.ts +6 -0
  50. package/dist/v2/vue/index.js +10 -0
  51. package/package.json +6 -1
@@ -1,667 +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, 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 useSimpleFunnel(funnelId, initialStepId) {
655
- const funnel = useFunnel({
656
- funnelId,
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
  }