react-fathom 0.1.10 → 0.2.0

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 (80) hide show
  1. package/README.md +941 -25
  2. package/dist/cjs/index.cjs +55 -9
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/cjs/native/index.cjs +1079 -0
  5. package/dist/cjs/native/index.cjs.map +1 -0
  6. package/dist/cjs/next/index.cjs +89 -5
  7. package/dist/cjs/next/index.cjs.map +1 -1
  8. package/dist/es/index.js +55 -9
  9. package/dist/es/index.js.map +1 -1
  10. package/dist/es/native/index.js +1071 -0
  11. package/dist/es/native/index.js.map +1 -0
  12. package/dist/es/next/index.js +90 -6
  13. package/dist/es/next/index.js.map +1 -1
  14. package/dist/react-fathom.js +55 -9
  15. package/dist/react-fathom.js.map +1 -1
  16. package/dist/react-fathom.min.js +2 -2
  17. package/dist/react-fathom.min.js.map +1 -1
  18. package/package.json +28 -5
  19. package/src/FathomContext.tsx +30 -1
  20. package/src/FathomProvider.test.tsx +115 -15
  21. package/src/FathomProvider.tsx +10 -2
  22. package/src/components/TrackClick.test.tsx +7 -7
  23. package/src/components/TrackClick.tsx +1 -1
  24. package/src/components/TrackVisible.test.tsx +7 -7
  25. package/src/components/TrackVisible.tsx +1 -1
  26. package/src/hooks/useFathom.test.tsx +14 -3
  27. package/src/hooks/useTrackOnClick.test.tsx +4 -4
  28. package/src/hooks/useTrackOnClick.ts +1 -1
  29. package/src/hooks/useTrackOnVisible.test.tsx +4 -4
  30. package/src/hooks/useTrackOnVisible.ts +1 -1
  31. package/src/index.ts +1 -0
  32. package/src/native/FathomWebView.test.tsx +410 -0
  33. package/src/native/FathomWebView.tsx +297 -0
  34. package/src/native/NativeFathomProvider.test.tsx +372 -0
  35. package/src/native/NativeFathomProvider.tsx +113 -0
  36. package/src/native/createWebViewClient.test.ts +380 -0
  37. package/src/native/createWebViewClient.ts +271 -0
  38. package/src/native/index.ts +29 -0
  39. package/src/native/react-native.d.ts +74 -0
  40. package/src/native/types.ts +145 -0
  41. package/src/native/useAppStateTracking.test.ts +249 -0
  42. package/src/native/useAppStateTracking.ts +66 -0
  43. package/src/native/useNavigationTracking.test.ts +446 -0
  44. package/src/native/useNavigationTracking.ts +177 -0
  45. package/src/next/NextFathomProviderApp.client.tsx +5 -0
  46. package/src/next/NextFathomProviderApp.test.tsx +154 -0
  47. package/src/next/NextFathomProviderApp.tsx +62 -0
  48. package/src/next/index.ts +3 -0
  49. package/src/types.ts +36 -9
  50. package/types/FathomContext.d.ts +1 -1
  51. package/types/FathomContext.d.ts.map +1 -1
  52. package/types/FathomProvider.d.ts.map +1 -1
  53. package/types/components/TrackClick.d.ts +1 -1
  54. package/types/components/TrackVisible.d.ts +1 -1
  55. package/types/hooks/useTrackOnClick.d.ts +1 -1
  56. package/types/hooks/useTrackOnVisible.d.ts +1 -1
  57. package/types/index.d.ts +1 -0
  58. package/types/index.d.ts.map +1 -1
  59. package/types/native/FathomWebView.d.ts +59 -0
  60. package/types/native/FathomWebView.d.ts.map +1 -0
  61. package/types/native/NativeFathomProvider.d.ts +36 -0
  62. package/types/native/NativeFathomProvider.d.ts.map +1 -0
  63. package/types/native/createWebViewClient.d.ts +51 -0
  64. package/types/native/createWebViewClient.d.ts.map +1 -0
  65. package/types/native/index.d.ts +10 -0
  66. package/types/native/index.d.ts.map +1 -0
  67. package/types/native/types.d.ts +125 -0
  68. package/types/native/types.d.ts.map +1 -0
  69. package/types/native/useAppStateTracking.d.ts +25 -0
  70. package/types/native/useAppStateTracking.d.ts.map +1 -0
  71. package/types/native/useNavigationTracking.d.ts +30 -0
  72. package/types/native/useNavigationTracking.d.ts.map +1 -0
  73. package/types/next/NextFathomProviderApp.client.d.ts +3 -0
  74. package/types/next/NextFathomProviderApp.client.d.ts.map +1 -0
  75. package/types/next/NextFathomProviderApp.d.ts +38 -4
  76. package/types/next/NextFathomProviderApp.d.ts.map +1 -1
  77. package/types/next/index.d.ts +1 -0
  78. package/types/next/index.d.ts.map +1 -1
  79. package/types/types.d.ts +34 -9
  80. package/types/types.d.ts.map +1 -1
@@ -0,0 +1,446 @@
1
+ import React from 'react'
2
+
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { renderHook, act, waitFor } from '@testing-library/react'
6
+
7
+ import { FathomProvider } from '../FathomProvider'
8
+ import { useNavigationTracking } from './useNavigationTracking'
9
+
10
+ describe('useNavigationTracking', () => {
11
+ const mockTrackPageview = vi.fn()
12
+ const mockClient = {
13
+ trackEvent: vi.fn(),
14
+ trackPageview: mockTrackPageview,
15
+ trackGoal: vi.fn(),
16
+ load: vi.fn(),
17
+ setSite: vi.fn(),
18
+ blockTrackingForMe: vi.fn(),
19
+ enableTrackingForMe: vi.fn(),
20
+ isTrackingEnabled: vi.fn(() => true),
21
+ }
22
+
23
+ // Store listener callbacks for testing
24
+ let stateChangeCallback: (() => void) | null = null
25
+
26
+ const createMockNavigationRef = (initialRoute = 'Home', params?: object) => {
27
+ let currentRoute = initialRoute
28
+ let currentParams = params
29
+
30
+ return {
31
+ current: {
32
+ getRootState: () => ({
33
+ index: 0,
34
+ routes: [{ name: currentRoute, params: currentParams }],
35
+ }),
36
+ addListener: vi.fn((event, callback) => {
37
+ if (event === 'state') {
38
+ stateChangeCallback = callback
39
+ }
40
+ return vi.fn() // Return unsubscribe function
41
+ }),
42
+ // Helper method for testing to change route
43
+ __setRoute: (name: string, newParams?: object) => {
44
+ currentRoute = name
45
+ currentParams = newParams
46
+ },
47
+ },
48
+ }
49
+ }
50
+
51
+ beforeEach(() => {
52
+ vi.clearAllMocks()
53
+ stateChangeCallback = null
54
+ })
55
+
56
+ const createWrapper = () => {
57
+ return ({ children }: { children: React.ReactNode }) =>
58
+ React.createElement(FathomProvider, { client: mockClient }, children)
59
+ }
60
+
61
+ it('should set up navigation listener on mount', async () => {
62
+ const navigationRef = createMockNavigationRef()
63
+
64
+ renderHook(
65
+ () =>
66
+ useNavigationTracking({
67
+ navigationRef: navigationRef as any,
68
+ }),
69
+ { wrapper: createWrapper() },
70
+ )
71
+
72
+ await waitFor(() => {
73
+ expect(navigationRef.current.addListener).toHaveBeenCalledWith(
74
+ 'state',
75
+ expect.any(Function),
76
+ )
77
+ })
78
+ })
79
+
80
+ it('should track initial route on mount', async () => {
81
+ const navigationRef = createMockNavigationRef('Home')
82
+
83
+ renderHook(
84
+ () =>
85
+ useNavigationTracking({
86
+ navigationRef: navigationRef as any,
87
+ }),
88
+ { wrapper: createWrapper() },
89
+ )
90
+
91
+ await waitFor(() => {
92
+ expect(mockTrackPageview).toHaveBeenCalledWith({
93
+ url: '/Home',
94
+ })
95
+ })
96
+ })
97
+
98
+ it('should track route changes', async () => {
99
+ const navigationRef = createMockNavigationRef('Home')
100
+
101
+ renderHook(
102
+ () =>
103
+ useNavigationTracking({
104
+ navigationRef: navigationRef as any,
105
+ }),
106
+ { wrapper: createWrapper() },
107
+ )
108
+
109
+ // Wait for initial tracking
110
+ await waitFor(() => {
111
+ expect(mockTrackPageview).toHaveBeenCalled()
112
+ })
113
+
114
+ mockTrackPageview.mockClear()
115
+
116
+ // Simulate navigation to Settings
117
+ navigationRef.current.__setRoute('Settings')
118
+
119
+ act(() => {
120
+ stateChangeCallback?.()
121
+ })
122
+
123
+ await waitFor(() => {
124
+ expect(mockTrackPageview).toHaveBeenCalledWith({
125
+ url: '/Settings',
126
+ referrer: '/Home',
127
+ })
128
+ })
129
+ })
130
+
131
+ it('should not track same route twice', async () => {
132
+ const navigationRef = createMockNavigationRef('Home')
133
+
134
+ renderHook(
135
+ () =>
136
+ useNavigationTracking({
137
+ navigationRef: navigationRef as any,
138
+ }),
139
+ { wrapper: createWrapper() },
140
+ )
141
+
142
+ await waitFor(() => {
143
+ expect(mockTrackPageview).toHaveBeenCalledTimes(1)
144
+ })
145
+
146
+ // Trigger state change without route change
147
+ act(() => {
148
+ stateChangeCallback?.()
149
+ })
150
+
151
+ // Should still only have been called once
152
+ expect(mockTrackPageview).toHaveBeenCalledTimes(1)
153
+ })
154
+
155
+ it('should transform route names when transformRouteName is provided', async () => {
156
+ const navigationRef = createMockNavigationRef('Home')
157
+
158
+ renderHook(
159
+ () =>
160
+ useNavigationTracking({
161
+ navigationRef: navigationRef as any,
162
+ transformRouteName: (name) => `/screens/${name.toLowerCase()}`,
163
+ }),
164
+ { wrapper: createWrapper() },
165
+ )
166
+
167
+ await waitFor(() => {
168
+ expect(mockTrackPageview).toHaveBeenCalledWith({
169
+ url: '/screens/home',
170
+ })
171
+ })
172
+ })
173
+
174
+ it('should filter routes when shouldTrackRoute returns false', async () => {
175
+ const navigationRef = createMockNavigationRef('ModalScreen')
176
+
177
+ renderHook(
178
+ () =>
179
+ useNavigationTracking({
180
+ navigationRef: navigationRef as any,
181
+ shouldTrackRoute: (name) => !name.includes('Modal'),
182
+ }),
183
+ { wrapper: createWrapper() },
184
+ )
185
+
186
+ // Wait a bit to ensure tracking doesn't happen
187
+ await new Promise((resolve) => setTimeout(resolve, 50))
188
+
189
+ expect(mockTrackPageview).not.toHaveBeenCalled()
190
+ })
191
+
192
+ it('should track route when shouldTrackRoute returns true', async () => {
193
+ const navigationRef = createMockNavigationRef('Home')
194
+
195
+ renderHook(
196
+ () =>
197
+ useNavigationTracking({
198
+ navigationRef: navigationRef as any,
199
+ shouldTrackRoute: (name) => !name.includes('Modal'),
200
+ }),
201
+ { wrapper: createWrapper() },
202
+ )
203
+
204
+ await waitFor(() => {
205
+ expect(mockTrackPageview).toHaveBeenCalledWith({
206
+ url: '/Home',
207
+ })
208
+ })
209
+ })
210
+
211
+ it('should include params when includeParams is true', async () => {
212
+ const navigationRef = createMockNavigationRef('Profile', { userId: '123' })
213
+
214
+ renderHook(
215
+ () =>
216
+ useNavigationTracking({
217
+ navigationRef: navigationRef as any,
218
+ includeParams: true,
219
+ }),
220
+ { wrapper: createWrapper() },
221
+ )
222
+
223
+ await waitFor(() => {
224
+ expect(mockTrackPageview).toHaveBeenCalledWith({
225
+ url: '/Profile?userId=123',
226
+ })
227
+ })
228
+ })
229
+
230
+ it('should not include params when includeParams is false', async () => {
231
+ const navigationRef = createMockNavigationRef('Profile', { userId: '123' })
232
+
233
+ renderHook(
234
+ () =>
235
+ useNavigationTracking({
236
+ navigationRef: navigationRef as any,
237
+ includeParams: false,
238
+ }),
239
+ { wrapper: createWrapper() },
240
+ )
241
+
242
+ await waitFor(() => {
243
+ expect(mockTrackPageview).toHaveBeenCalledWith({
244
+ url: '/Profile',
245
+ })
246
+ })
247
+ })
248
+
249
+ it('should handle multiple params', async () => {
250
+ const navigationRef = createMockNavigationRef('Search', {
251
+ query: 'test',
252
+ page: 1,
253
+ })
254
+
255
+ renderHook(
256
+ () =>
257
+ useNavigationTracking({
258
+ navigationRef: navigationRef as any,
259
+ includeParams: true,
260
+ }),
261
+ { wrapper: createWrapper() },
262
+ )
263
+
264
+ await waitFor(() => {
265
+ const call = mockTrackPageview.mock.calls[0][0]
266
+ expect(call.url).toContain('/Search?')
267
+ expect(call.url).toContain('query=test')
268
+ expect(call.url).toContain('page=1')
269
+ })
270
+ })
271
+
272
+ it('should encode special characters in params', async () => {
273
+ const navigationRef = createMockNavigationRef('Search', {
274
+ query: 'hello world',
275
+ })
276
+
277
+ renderHook(
278
+ () =>
279
+ useNavigationTracking({
280
+ navigationRef: navigationRef as any,
281
+ includeParams: true,
282
+ }),
283
+ { wrapper: createWrapper() },
284
+ )
285
+
286
+ await waitFor(() => {
287
+ expect(mockTrackPageview).toHaveBeenCalledWith({
288
+ url: '/Search?query=hello%20world',
289
+ })
290
+ })
291
+ })
292
+
293
+ it('should skip null and undefined params', async () => {
294
+ const navigationRef = createMockNavigationRef('Profile', {
295
+ userId: '123',
296
+ extra: null,
297
+ other: undefined,
298
+ })
299
+
300
+ renderHook(
301
+ () =>
302
+ useNavigationTracking({
303
+ navigationRef: navigationRef as any,
304
+ includeParams: true,
305
+ }),
306
+ { wrapper: createWrapper() },
307
+ )
308
+
309
+ await waitFor(() => {
310
+ expect(mockTrackPageview).toHaveBeenCalledWith({
311
+ url: '/Profile?userId=123',
312
+ })
313
+ })
314
+ })
315
+
316
+ it('should pass params to shouldTrackRoute', async () => {
317
+ const shouldTrackRoute = vi.fn(() => true)
318
+ const params = { userId: '123' }
319
+ const navigationRef = createMockNavigationRef('Profile', params)
320
+
321
+ renderHook(
322
+ () =>
323
+ useNavigationTracking({
324
+ navigationRef: navigationRef as any,
325
+ shouldTrackRoute,
326
+ includeParams: true, // Need this for params to be passed
327
+ }),
328
+ { wrapper: createWrapper() },
329
+ )
330
+
331
+ await waitFor(() => {
332
+ expect(shouldTrackRoute).toHaveBeenCalledWith('Profile', params)
333
+ })
334
+ })
335
+
336
+ it('should handle navigation ref without getRootState', async () => {
337
+ const navigationRef = {
338
+ current: {
339
+ getRootState: undefined,
340
+ addListener: vi.fn(() => vi.fn()),
341
+ },
342
+ }
343
+
344
+ // Should not throw
345
+ renderHook(
346
+ () =>
347
+ useNavigationTracking({
348
+ navigationRef: navigationRef as any,
349
+ }),
350
+ { wrapper: createWrapper() },
351
+ )
352
+
353
+ await new Promise((resolve) => setTimeout(resolve, 50))
354
+
355
+ expect(mockTrackPageview).not.toHaveBeenCalled()
356
+ })
357
+
358
+ it('should handle null navigation ref current', async () => {
359
+ const navigationRef = {
360
+ current: null,
361
+ }
362
+
363
+ // Should not throw
364
+ renderHook(
365
+ () =>
366
+ useNavigationTracking({
367
+ navigationRef: navigationRef as any,
368
+ }),
369
+ { wrapper: createWrapper() },
370
+ )
371
+
372
+ await new Promise((resolve) => setTimeout(resolve, 50))
373
+
374
+ expect(mockTrackPageview).not.toHaveBeenCalled()
375
+ })
376
+
377
+ describe('nested navigation', () => {
378
+ it('should track deepest route in nested navigators', async () => {
379
+ const navigationRef = {
380
+ current: {
381
+ getRootState: () => ({
382
+ index: 0,
383
+ routes: [
384
+ {
385
+ name: 'MainStack',
386
+ state: {
387
+ index: 1,
388
+ routes: [
389
+ { name: 'Home' },
390
+ {
391
+ name: 'TabNavigator',
392
+ state: {
393
+ index: 0,
394
+ routes: [{ name: 'Feed' }],
395
+ },
396
+ },
397
+ ],
398
+ },
399
+ },
400
+ ],
401
+ }),
402
+ addListener: vi.fn(() => vi.fn()),
403
+ },
404
+ }
405
+
406
+ renderHook(
407
+ () =>
408
+ useNavigationTracking({
409
+ navigationRef: navigationRef as any,
410
+ }),
411
+ { wrapper: createWrapper() },
412
+ )
413
+
414
+ await waitFor(() => {
415
+ expect(mockTrackPageview).toHaveBeenCalledWith({
416
+ url: '/Feed',
417
+ })
418
+ })
419
+ })
420
+ })
421
+
422
+ describe('combined options', () => {
423
+ it('should work with all options combined', async () => {
424
+ const navigationRef = createMockNavigationRef('UserProfile', {
425
+ id: '456',
426
+ })
427
+
428
+ renderHook(
429
+ () =>
430
+ useNavigationTracking({
431
+ navigationRef: navigationRef as any,
432
+ transformRouteName: (name) => `/app/${name}`,
433
+ shouldTrackRoute: () => true,
434
+ includeParams: true,
435
+ }),
436
+ { wrapper: createWrapper() },
437
+ )
438
+
439
+ await waitFor(() => {
440
+ expect(mockTrackPageview).toHaveBeenCalledWith({
441
+ url: '/app/UserProfile?id=456',
442
+ })
443
+ })
444
+ })
445
+ })
446
+ })
@@ -0,0 +1,177 @@
1
+ import { useEffect, useRef, useCallback } from 'react'
2
+
3
+ import { useFathom } from '../hooks/useFathom'
4
+ import type { UseNavigationTrackingOptions } from './types'
5
+
6
+ /**
7
+ * Hook that tracks screen navigation as pageviews using React Navigation.
8
+ *
9
+ * This integrates with React Navigation's navigation container to automatically
10
+ * track screen changes as Fathom pageviews.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * import { NavigationContainer } from '@react-navigation/native'
15
+ * import { useNavigationTracking } from 'react-fathom/native'
16
+ *
17
+ * function App() {
18
+ * const navigationRef = useNavigationContainerRef()
19
+ *
20
+ * useNavigationTracking({
21
+ * navigationRef,
22
+ * transformRouteName: (name) => `/screens/${name}`,
23
+ * })
24
+ *
25
+ * return (
26
+ * <NavigationContainer ref={navigationRef}>
27
+ * <Navigator />
28
+ * </NavigationContainer>
29
+ * )
30
+ * }
31
+ * ```
32
+ */
33
+ export function useNavigationTracking(options: UseNavigationTrackingOptions) {
34
+ const {
35
+ navigationRef,
36
+ transformRouteName,
37
+ shouldTrackRoute,
38
+ includeParams = false,
39
+ } = options
40
+
41
+ const { trackPageview } = useFathom()
42
+ const routeNameRef = useRef<string | undefined>(undefined)
43
+
44
+ /**
45
+ * Get the current route name from the navigation state
46
+ */
47
+ const getCurrentRouteName = useCallback((): string | undefined => {
48
+ if (!navigationRef.current) {
49
+ return undefined
50
+ }
51
+
52
+ const state = navigationRef.current.getRootState?.()
53
+ if (!state) {
54
+ return undefined
55
+ }
56
+
57
+ // Navigate through nested navigators to get the deepest route
58
+ let currentState = state
59
+ while (currentState.routes[currentState.index]?.state) {
60
+ currentState = currentState.routes[currentState.index].state as any
61
+ }
62
+
63
+ return currentState.routes[currentState.index]?.name
64
+ }, [navigationRef])
65
+
66
+ /**
67
+ * Get the current route params from the navigation state
68
+ */
69
+ const getCurrentRouteParams = useCallback((): Record<string, any> | undefined => {
70
+ if (!navigationRef.current) {
71
+ return undefined
72
+ }
73
+
74
+ const state = navigationRef.current.getRootState?.()
75
+ if (!state) {
76
+ return undefined
77
+ }
78
+
79
+ // Navigate through nested navigators to get the deepest route
80
+ let currentState = state
81
+ while (currentState.routes[currentState.index]?.state) {
82
+ currentState = currentState.routes[currentState.index].state as any
83
+ }
84
+
85
+ return currentState.routes[currentState.index]?.params as Record<string, any> | undefined
86
+ }, [navigationRef])
87
+
88
+ /**
89
+ * Build the URL to track
90
+ */
91
+ const buildTrackingUrl = useCallback(
92
+ (routeName: string, params?: Record<string, any>): string => {
93
+ let url = transformRouteName ? transformRouteName(routeName) : `/${routeName}`
94
+
95
+ if (includeParams && params && Object.keys(params).length > 0) {
96
+ const queryString = Object.entries(params)
97
+ .filter(([_, value]) => value !== undefined && value !== null)
98
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
99
+ .join('&')
100
+
101
+ if (queryString) {
102
+ url += `?${queryString}`
103
+ }
104
+ }
105
+
106
+ return url
107
+ },
108
+ [transformRouteName, includeParams],
109
+ )
110
+
111
+ /**
112
+ * Handle navigation state change
113
+ */
114
+ const handleStateChange = useCallback(() => {
115
+ const currentRouteName = getCurrentRouteName()
116
+ const previousRouteName = routeNameRef.current
117
+
118
+ if (currentRouteName && currentRouteName !== previousRouteName) {
119
+ const params = includeParams ? getCurrentRouteParams() : undefined
120
+
121
+ // Check if this route should be tracked
122
+ if (shouldTrackRoute && !shouldTrackRoute(currentRouteName, params)) {
123
+ routeNameRef.current = currentRouteName
124
+ return
125
+ }
126
+
127
+ const url = buildTrackingUrl(currentRouteName, params)
128
+
129
+ trackPageview?.({
130
+ url,
131
+ referrer: previousRouteName ? buildTrackingUrl(previousRouteName) : undefined,
132
+ })
133
+
134
+ routeNameRef.current = currentRouteName
135
+ }
136
+ }, [
137
+ getCurrentRouteName,
138
+ getCurrentRouteParams,
139
+ shouldTrackRoute,
140
+ buildTrackingUrl,
141
+ trackPageview,
142
+ includeParams,
143
+ ])
144
+
145
+ // Track initial route on mount
146
+ useEffect(() => {
147
+ // Small delay to ensure navigation is ready
148
+ const timeout = setTimeout(() => {
149
+ const initialRoute = getCurrentRouteName()
150
+ if (initialRoute) {
151
+ const params = includeParams ? getCurrentRouteParams() : undefined
152
+
153
+ if (!shouldTrackRoute || shouldTrackRoute(initialRoute, params)) {
154
+ const url = buildTrackingUrl(initialRoute, params)
155
+ trackPageview?.({ url })
156
+ routeNameRef.current = initialRoute
157
+ }
158
+ }
159
+ }, 0)
160
+
161
+ return () => clearTimeout(timeout)
162
+ }, []) // Only on mount
163
+
164
+ // Set up navigation state change listener
165
+ useEffect(() => {
166
+ if (!navigationRef.current) {
167
+ return
168
+ }
169
+
170
+ // Listen for navigation state changes
171
+ const unsubscribe = navigationRef.current.addListener?.('state', handleStateChange)
172
+
173
+ return () => {
174
+ unsubscribe?.()
175
+ }
176
+ }, [navigationRef, handleStateChange])
177
+ }
@@ -0,0 +1,5 @@
1
+ 'use client'
2
+
3
+ // Re-export the component to ensure 'use client' directive is at the top
4
+ export { NextFathomProviderApp } from './NextFathomProviderApp'
5
+ export type { NextFathomProviderAppProps } from './NextFathomProviderApp'