react-fathom 0.1.11 → 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 (70) hide show
  1. package/README.md +886 -24
  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 +51 -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 +51 -5
  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 +27 -4
  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/types.ts +36 -9
  46. package/types/FathomContext.d.ts +1 -1
  47. package/types/FathomContext.d.ts.map +1 -1
  48. package/types/FathomProvider.d.ts.map +1 -1
  49. package/types/components/TrackClick.d.ts +1 -1
  50. package/types/components/TrackVisible.d.ts +1 -1
  51. package/types/hooks/useTrackOnClick.d.ts +1 -1
  52. package/types/hooks/useTrackOnVisible.d.ts +1 -1
  53. package/types/index.d.ts +1 -0
  54. package/types/index.d.ts.map +1 -1
  55. package/types/native/FathomWebView.d.ts +59 -0
  56. package/types/native/FathomWebView.d.ts.map +1 -0
  57. package/types/native/NativeFathomProvider.d.ts +36 -0
  58. package/types/native/NativeFathomProvider.d.ts.map +1 -0
  59. package/types/native/createWebViewClient.d.ts +51 -0
  60. package/types/native/createWebViewClient.d.ts.map +1 -0
  61. package/types/native/index.d.ts +10 -0
  62. package/types/native/index.d.ts.map +1 -0
  63. package/types/native/types.d.ts +125 -0
  64. package/types/native/types.d.ts.map +1 -0
  65. package/types/native/useAppStateTracking.d.ts +25 -0
  66. package/types/native/useAppStateTracking.d.ts.map +1 -0
  67. package/types/native/useNavigationTracking.d.ts +30 -0
  68. package/types/native/useNavigationTracking.d.ts.map +1 -0
  69. package/types/types.d.ts +34 -9
  70. package/types/types.d.ts.map +1 -1
@@ -0,0 +1,380 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest'
2
+
3
+ import { createWebViewClient } from './createWebViewClient'
4
+ import type { FathomWebViewRef } from './FathomWebView'
5
+
6
+ describe('createWebViewClient', () => {
7
+ const createMockWebViewRef = (isReady = true): FathomWebViewRef => ({
8
+ trackPageview: vi.fn(),
9
+ trackEvent: vi.fn(),
10
+ trackGoal: vi.fn(),
11
+ blockTrackingForMe: vi.fn(),
12
+ enableTrackingForMe: vi.fn(),
13
+ isReady: vi.fn(() => isReady),
14
+ })
15
+
16
+ beforeEach(() => {
17
+ vi.clearAllMocks()
18
+ })
19
+
20
+ describe('basic client creation', () => {
21
+ it('should create a client with all required methods', () => {
22
+ const client = createWebViewClient(() => null)
23
+
24
+ expect(client.load).toBeDefined()
25
+ expect(client.trackPageview).toBeDefined()
26
+ expect(client.trackEvent).toBeDefined()
27
+ expect(client.trackGoal).toBeDefined()
28
+ expect(client.setSite).toBeDefined()
29
+ expect(client.blockTrackingForMe).toBeDefined()
30
+ expect(client.enableTrackingForMe).toBeDefined()
31
+ expect(client.isTrackingEnabled).toBeDefined()
32
+ expect(client.processQueue).toBeDefined()
33
+ expect(client.getQueueLength).toBeDefined()
34
+ expect(client.setWebViewReady).toBeDefined()
35
+ })
36
+
37
+ it('should start with tracking enabled', () => {
38
+ const client = createWebViewClient(() => null)
39
+ expect(client.isTrackingEnabled()).toBe(true)
40
+ })
41
+
42
+ it('should start with empty queue', () => {
43
+ const client = createWebViewClient(() => null)
44
+ expect(client.getQueueLength()).toBe(0)
45
+ })
46
+ })
47
+
48
+ describe('when WebView is ready', () => {
49
+ it('should call trackPageview on WebView immediately', () => {
50
+ const mockRef = createMockWebViewRef(true)
51
+ const client = createWebViewClient(() => mockRef)
52
+
53
+ client.trackPageview({ url: '/test' })
54
+
55
+ expect(mockRef.trackPageview).toHaveBeenCalledWith({ url: '/test' })
56
+ })
57
+
58
+ it('should call trackEvent on WebView immediately', () => {
59
+ const mockRef = createMockWebViewRef(true)
60
+ const client = createWebViewClient(() => mockRef)
61
+
62
+ client.trackEvent('button-click', { _value: 100 })
63
+
64
+ expect(mockRef.trackEvent).toHaveBeenCalledWith('button-click', { _value: 100 })
65
+ })
66
+
67
+ it('should call trackGoal on WebView immediately', () => {
68
+ const mockRef = createMockWebViewRef(true)
69
+ const client = createWebViewClient(() => mockRef)
70
+
71
+ client.trackGoal('PURCHASE', 2999)
72
+
73
+ expect(mockRef.trackGoal).toHaveBeenCalledWith('PURCHASE', 2999)
74
+ })
75
+
76
+ it('should call blockTrackingForMe on WebView immediately', () => {
77
+ const mockRef = createMockWebViewRef(true)
78
+ const client = createWebViewClient(() => mockRef)
79
+
80
+ client.blockTrackingForMe()
81
+
82
+ expect(mockRef.blockTrackingForMe).toHaveBeenCalled()
83
+ })
84
+
85
+ it('should call enableTrackingForMe on WebView immediately', () => {
86
+ const mockRef = createMockWebViewRef(true)
87
+ const client = createWebViewClient(() => mockRef)
88
+
89
+ client.enableTrackingForMe()
90
+
91
+ expect(mockRef.enableTrackingForMe).toHaveBeenCalled()
92
+ })
93
+ })
94
+
95
+ describe('when WebView is not ready (queuing)', () => {
96
+ it('should queue trackPageview when WebView is not ready', () => {
97
+ const mockRef = createMockWebViewRef(false)
98
+ const client = createWebViewClient(() => mockRef)
99
+
100
+ client.trackPageview({ url: '/test' })
101
+
102
+ expect(mockRef.trackPageview).not.toHaveBeenCalled()
103
+ expect(client.getQueueLength()).toBe(1)
104
+ })
105
+
106
+ it('should queue trackEvent when WebView is not ready', () => {
107
+ const mockRef = createMockWebViewRef(false)
108
+ const client = createWebViewClient(() => mockRef)
109
+
110
+ client.trackEvent('button-click')
111
+
112
+ expect(mockRef.trackEvent).not.toHaveBeenCalled()
113
+ expect(client.getQueueLength()).toBe(1)
114
+ })
115
+
116
+ it('should queue trackGoal when WebView is not ready', () => {
117
+ const mockRef = createMockWebViewRef(false)
118
+ const client = createWebViewClient(() => mockRef)
119
+
120
+ client.trackGoal('PURCHASE', 2999)
121
+
122
+ expect(mockRef.trackGoal).not.toHaveBeenCalled()
123
+ expect(client.getQueueLength()).toBe(1)
124
+ })
125
+
126
+ it('should queue multiple commands', () => {
127
+ const mockRef = createMockWebViewRef(false)
128
+ const client = createWebViewClient(() => mockRef)
129
+
130
+ client.trackPageview({ url: '/page1' })
131
+ client.trackEvent('event1')
132
+ client.trackGoal('GOAL1', 100)
133
+
134
+ expect(client.getQueueLength()).toBe(3)
135
+ })
136
+
137
+ it('should process queue when setWebViewReady is called', () => {
138
+ const mockRef = createMockWebViewRef(false)
139
+ const client = createWebViewClient(() => mockRef)
140
+
141
+ client.trackPageview({ url: '/test' })
142
+ client.trackEvent('button-click', { _value: 100 })
143
+
144
+ expect(client.getQueueLength()).toBe(2)
145
+
146
+ // Now make WebView ready
147
+ mockRef.isReady = vi.fn(() => true)
148
+ client.setWebViewReady()
149
+
150
+ expect(mockRef.trackPageview).toHaveBeenCalledWith({ url: '/test' })
151
+ expect(mockRef.trackEvent).toHaveBeenCalledWith('button-click', { _value: 100 })
152
+ expect(client.getQueueLength()).toBe(0)
153
+ })
154
+
155
+ it('should process queue in order', () => {
156
+ const mockRef = createMockWebViewRef(false)
157
+ const client = createWebViewClient(() => mockRef)
158
+ const callOrder: string[] = []
159
+
160
+ mockRef.trackPageview = vi.fn(() => callOrder.push('pageview'))
161
+ mockRef.trackEvent = vi.fn(() => callOrder.push('event'))
162
+ mockRef.trackGoal = vi.fn(() => callOrder.push('goal'))
163
+
164
+ client.trackPageview({ url: '/first' })
165
+ client.trackEvent('second')
166
+ client.trackGoal('third', 100)
167
+
168
+ mockRef.isReady = vi.fn(() => true)
169
+ client.setWebViewReady()
170
+
171
+ expect(callOrder).toEqual(['pageview', 'event', 'goal'])
172
+ })
173
+ })
174
+
175
+ describe('queue size limits', () => {
176
+ it('should respect maxQueueSize option', () => {
177
+ const mockRef = createMockWebViewRef(false)
178
+ const client = createWebViewClient(() => mockRef, { maxQueueSize: 3 })
179
+
180
+ client.trackEvent('event1')
181
+ client.trackEvent('event2')
182
+ client.trackEvent('event3')
183
+ client.trackEvent('event4') // Should remove event1
184
+
185
+ expect(client.getQueueLength()).toBe(3)
186
+ })
187
+
188
+ it('should remove oldest command when queue is full', () => {
189
+ const mockRef = createMockWebViewRef(false)
190
+ const client = createWebViewClient(() => mockRef, { maxQueueSize: 2 })
191
+
192
+ client.trackEvent('event1')
193
+ client.trackEvent('event2')
194
+ client.trackEvent('event3') // Should remove event1
195
+
196
+ mockRef.isReady = vi.fn(() => true)
197
+ client.setWebViewReady()
198
+
199
+ // event1 should not have been called, only event2 and event3
200
+ expect(mockRef.trackEvent).toHaveBeenCalledTimes(2)
201
+ expect(mockRef.trackEvent).toHaveBeenCalledWith('event2', undefined)
202
+ expect(mockRef.trackEvent).toHaveBeenCalledWith('event3', undefined)
203
+ })
204
+ })
205
+
206
+ describe('queue disabled', () => {
207
+ it('should not queue commands when enableQueue is false', () => {
208
+ const mockRef = createMockWebViewRef(false)
209
+ const client = createWebViewClient(() => mockRef, { enableQueue: false })
210
+
211
+ client.trackPageview({ url: '/test' })
212
+ client.trackEvent('event1')
213
+
214
+ expect(client.getQueueLength()).toBe(0)
215
+ expect(mockRef.trackPageview).not.toHaveBeenCalled()
216
+ })
217
+ })
218
+
219
+ describe('tracking blocked', () => {
220
+ it('should not track pageview when blocked', () => {
221
+ const mockRef = createMockWebViewRef(true)
222
+ const client = createWebViewClient(() => mockRef)
223
+
224
+ client.blockTrackingForMe()
225
+ client.trackPageview({ url: '/test' })
226
+
227
+ // blockTrackingForMe itself is called, but not trackPageview after
228
+ expect(mockRef.trackPageview).not.toHaveBeenCalled()
229
+ })
230
+
231
+ it('should not track events when blocked', () => {
232
+ const mockRef = createMockWebViewRef(true)
233
+ const client = createWebViewClient(() => mockRef)
234
+
235
+ client.blockTrackingForMe()
236
+ client.trackEvent('button-click')
237
+
238
+ expect(mockRef.trackEvent).not.toHaveBeenCalled()
239
+ })
240
+
241
+ it('should not track goals when blocked', () => {
242
+ const mockRef = createMockWebViewRef(true)
243
+ const client = createWebViewClient(() => mockRef)
244
+
245
+ client.blockTrackingForMe()
246
+ client.trackGoal('PURCHASE', 2999)
247
+
248
+ expect(mockRef.trackGoal).not.toHaveBeenCalled()
249
+ })
250
+
251
+ it('should resume tracking after enableTrackingForMe', () => {
252
+ const mockRef = createMockWebViewRef(true)
253
+ const client = createWebViewClient(() => mockRef)
254
+
255
+ client.blockTrackingForMe()
256
+ expect(client.isTrackingEnabled()).toBe(false)
257
+
258
+ client.enableTrackingForMe()
259
+ expect(client.isTrackingEnabled()).toBe(true)
260
+
261
+ client.trackEvent('button-click')
262
+ expect(mockRef.trackEvent).toHaveBeenCalledWith('button-click', undefined)
263
+ })
264
+ })
265
+
266
+ describe('load method', () => {
267
+ it('should process queue when load is called', () => {
268
+ const mockRef = createMockWebViewRef(false)
269
+ const client = createWebViewClient(() => mockRef)
270
+
271
+ client.trackEvent('event1')
272
+
273
+ // Queue should not be empty
274
+ expect(client.getQueueLength()).toBe(1)
275
+
276
+ // Make WebView ready and call load
277
+ mockRef.isReady = vi.fn(() => true)
278
+ client.load('SITE_ID')
279
+
280
+ // Queue should be processed
281
+ expect(mockRef.trackEvent).toHaveBeenCalledWith('event1', undefined)
282
+ expect(client.getQueueLength()).toBe(0)
283
+ })
284
+ })
285
+
286
+ describe('setSite method', () => {
287
+ it('should update site ID', () => {
288
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
289
+ const mockRef = createMockWebViewRef(true)
290
+ const client = createWebViewClient(() => mockRef, { debug: true })
291
+
292
+ client.setSite('NEW_SITE_ID')
293
+
294
+ // Should log a warning about changing site ID
295
+ expect(consoleSpy).toHaveBeenCalled()
296
+ consoleSpy.mockRestore()
297
+ })
298
+ })
299
+
300
+ describe('null WebView ref', () => {
301
+ it('should queue commands when ref is null', () => {
302
+ const client = createWebViewClient(() => null)
303
+
304
+ client.trackPageview({ url: '/test' })
305
+
306
+ expect(client.getQueueLength()).toBe(1)
307
+ })
308
+
309
+ it('should not throw when ref is null', () => {
310
+ const client = createWebViewClient(() => null)
311
+
312
+ expect(() => client.trackPageview()).not.toThrow()
313
+ expect(() => client.trackEvent('test')).not.toThrow()
314
+ expect(() => client.trackGoal('TEST', 100)).not.toThrow()
315
+ expect(() => client.blockTrackingForMe()).not.toThrow()
316
+ expect(() => client.enableTrackingForMe()).not.toThrow()
317
+ })
318
+ })
319
+
320
+ describe('processQueue method', () => {
321
+ it('should return number of processed commands', () => {
322
+ const mockRef = createMockWebViewRef(false)
323
+ const client = createWebViewClient(() => mockRef)
324
+
325
+ client.trackEvent('event1')
326
+ client.trackEvent('event2')
327
+ client.trackEvent('event3')
328
+
329
+ mockRef.isReady = vi.fn(() => true)
330
+ const processed = client.processQueue()
331
+
332
+ expect(processed).toBe(3)
333
+ })
334
+
335
+ it('should return 0 when WebView is not ready', () => {
336
+ const mockRef = createMockWebViewRef(false)
337
+ const client = createWebViewClient(() => mockRef)
338
+
339
+ client.trackEvent('event1')
340
+
341
+ const processed = client.processQueue()
342
+
343
+ expect(processed).toBe(0)
344
+ expect(client.getQueueLength()).toBe(1) // Still in queue
345
+ })
346
+
347
+ it('should return 0 when queue is empty', () => {
348
+ const mockRef = createMockWebViewRef(true)
349
+ const client = createWebViewClient(() => mockRef)
350
+
351
+ const processed = client.processQueue()
352
+
353
+ expect(processed).toBe(0)
354
+ })
355
+ })
356
+
357
+ describe('debug logging', () => {
358
+ it('should log when debug is enabled', () => {
359
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
360
+ const mockRef = createMockWebViewRef(true)
361
+ const client = createWebViewClient(() => mockRef, { debug: true })
362
+
363
+ client.trackPageview({ url: '/test' })
364
+
365
+ expect(consoleSpy).toHaveBeenCalled()
366
+ consoleSpy.mockRestore()
367
+ })
368
+
369
+ it('should not log when debug is disabled', () => {
370
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
371
+ const mockRef = createMockWebViewRef(true)
372
+ const client = createWebViewClient(() => mockRef, { debug: false })
373
+
374
+ client.trackPageview({ url: '/test' })
375
+
376
+ expect(consoleSpy).not.toHaveBeenCalled()
377
+ consoleSpy.mockRestore()
378
+ })
379
+ })
380
+ })
@@ -0,0 +1,271 @@
1
+ import type { FathomClient, EventOptions, LoadOptions, PageViewOptions } from '../types'
2
+ import type { FathomWebViewRef } from './FathomWebView'
3
+
4
+ export interface WebViewClientOptions {
5
+ /**
6
+ * Enable debug logging (default: false)
7
+ */
8
+ debug?: boolean
9
+
10
+ /**
11
+ * Enable offline event queuing (default: true)
12
+ * When enabled, events are queued if the WebView isn't ready yet
13
+ */
14
+ enableQueue?: boolean
15
+
16
+ /**
17
+ * Maximum number of events to queue (default: 100)
18
+ */
19
+ maxQueueSize?: number
20
+ }
21
+
22
+ interface QueuedCommand {
23
+ type: 'pageview' | 'event' | 'goal' | 'block' | 'enable'
24
+ args: unknown[]
25
+ timestamp: number
26
+ }
27
+
28
+ /**
29
+ * Creates a Fathom client that communicates with a FathomWebView component.
30
+ *
31
+ * This client queues commands until the WebView is ready, then flushes them.
32
+ * It implements the standard FathomClient interface for compatibility with
33
+ * the FathomProvider component.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { createWebViewClient, FathomWebView } from 'react-fathom/native'
38
+ *
39
+ * function App() {
40
+ * const webViewRef = useRef<FathomWebViewRef>(null)
41
+ * const client = useMemo(
42
+ * () => createWebViewClient(() => webViewRef.current, { debug: __DEV__ }),
43
+ * []
44
+ * )
45
+ *
46
+ * return (
47
+ * <FathomProvider client={client} siteId="YOUR_SITE_ID">
48
+ * <FathomWebView ref={webViewRef} siteId="YOUR_SITE_ID" />
49
+ * <YourApp />
50
+ * </FathomProvider>
51
+ * )
52
+ * }
53
+ * ```
54
+ */
55
+ export interface WebViewFathomClient extends FathomClient {
56
+ processQueue: () => number
57
+ getQueueLength: () => number
58
+ setWebViewReady: () => void
59
+ }
60
+
61
+ export function createWebViewClient(
62
+ getWebViewRef: () => FathomWebViewRef | null | undefined,
63
+ options: WebViewClientOptions = {},
64
+ ): WebViewFathomClient {
65
+ const { debug = false, enableQueue = true, maxQueueSize = 100 } = options
66
+
67
+ let isTrackingBlocked = false
68
+ let currentSiteId: string | undefined
69
+ let isLoaded = false
70
+
71
+ // Queue for commands sent before WebView is ready
72
+ const commandQueue: QueuedCommand[] = []
73
+
74
+ const log = (...args: unknown[]) => {
75
+ if (debug) {
76
+ console.log('[react-fathom/webview-client]', ...args)
77
+ }
78
+ }
79
+
80
+ const warn = (...args: unknown[]) => {
81
+ if (debug) {
82
+ console.warn('[react-fathom/webview-client]', ...args)
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Queue a command for later execution
88
+ */
89
+ const queueCommand = (type: QueuedCommand['type'], args: unknown[]) => {
90
+ if (!enableQueue) {
91
+ warn('Queue disabled, dropping command:', type)
92
+ return
93
+ }
94
+
95
+ if (commandQueue.length >= maxQueueSize) {
96
+ commandQueue.shift()
97
+ log('Queue full, removed oldest command')
98
+ }
99
+
100
+ commandQueue.push({
101
+ type,
102
+ args,
103
+ timestamp: Date.now(),
104
+ })
105
+
106
+ log(`Command queued (${commandQueue.length}/${maxQueueSize}):`, type)
107
+ }
108
+
109
+ /**
110
+ * Process all queued commands
111
+ */
112
+ const processQueue = () => {
113
+ const ref = getWebViewRef()
114
+ if (!ref?.isReady()) {
115
+ return 0
116
+ }
117
+
118
+ log(`Processing ${commandQueue.length} queued commands`)
119
+ let processed = 0
120
+
121
+ while (commandQueue.length > 0) {
122
+ const command = commandQueue.shift()!
123
+
124
+ switch (command.type) {
125
+ case 'pageview':
126
+ ref.trackPageview(command.args[0] as PageViewOptions | undefined)
127
+ break
128
+ case 'event':
129
+ ref.trackEvent(
130
+ command.args[0] as string,
131
+ command.args[1] as EventOptions | undefined,
132
+ )
133
+ break
134
+ case 'goal':
135
+ ref.trackGoal(command.args[0] as string, command.args[1] as number)
136
+ break
137
+ case 'block':
138
+ ref.blockTrackingForMe()
139
+ break
140
+ case 'enable':
141
+ ref.enableTrackingForMe()
142
+ break
143
+ }
144
+
145
+ processed++
146
+ }
147
+
148
+ log(`Processed ${processed} queued commands`)
149
+ return processed
150
+ }
151
+
152
+ /**
153
+ * Execute a command immediately or queue it
154
+ */
155
+ const executeOrQueue = (
156
+ type: QueuedCommand['type'],
157
+ args: unknown[],
158
+ executor: () => void,
159
+ ) => {
160
+ const ref = getWebViewRef()
161
+
162
+ if (ref?.isReady()) {
163
+ executor()
164
+ } else {
165
+ queueCommand(type, args)
166
+ }
167
+ }
168
+
169
+ const client: WebViewFathomClient = {
170
+ load: (siteId: string, opts?: LoadOptions) => {
171
+ currentSiteId = siteId
172
+ isLoaded = true
173
+ log('Client loaded with site ID:', siteId)
174
+
175
+ // Process any queued commands now that we're "loaded"
176
+ // (actual WebView readiness is separate)
177
+ processQueue()
178
+ },
179
+
180
+ trackPageview: (opts?: PageViewOptions) => {
181
+ if (isTrackingBlocked) {
182
+ log('Tracking blocked, skipping pageview')
183
+ return
184
+ }
185
+
186
+ executeOrQueue('pageview', [opts], () => {
187
+ const ref = getWebViewRef()
188
+ ref?.trackPageview(opts)
189
+ log('Tracked pageview:', opts)
190
+ })
191
+ },
192
+
193
+ trackEvent: (eventName: string, opts?: EventOptions) => {
194
+ if (isTrackingBlocked) {
195
+ log('Tracking blocked, skipping event')
196
+ return
197
+ }
198
+
199
+ executeOrQueue('event', [eventName, opts], () => {
200
+ const ref = getWebViewRef()
201
+ ref?.trackEvent(eventName, opts)
202
+ log('Tracked event:', eventName, opts)
203
+ })
204
+ },
205
+
206
+ trackGoal: (code: string, cents: number) => {
207
+ if (isTrackingBlocked) {
208
+ log('Tracking blocked, skipping goal')
209
+ return
210
+ }
211
+
212
+ executeOrQueue('goal', [code, cents], () => {
213
+ const ref = getWebViewRef()
214
+ ref?.trackGoal(code, cents)
215
+ log('Tracked goal:', code, cents)
216
+ })
217
+ },
218
+
219
+ setSite: (id: string) => {
220
+ currentSiteId = id
221
+ log('Site ID changed to:', id)
222
+ // Note: The WebView loads with a specific site ID, so changing it
223
+ // at runtime would require reloading the WebView
224
+ warn(
225
+ 'setSite() called but WebView was initialized with a different site ID. ' +
226
+ 'Consider re-mounting the FathomWebView component.',
227
+ )
228
+ },
229
+
230
+ blockTrackingForMe: () => {
231
+ isTrackingBlocked = true
232
+ executeOrQueue('block', [], () => {
233
+ const ref = getWebViewRef()
234
+ ref?.blockTrackingForMe()
235
+ })
236
+ log('Tracking blocked')
237
+ },
238
+
239
+ enableTrackingForMe: () => {
240
+ isTrackingBlocked = false
241
+ executeOrQueue('enable', [], () => {
242
+ const ref = getWebViewRef()
243
+ ref?.enableTrackingForMe()
244
+ })
245
+ log('Tracking enabled')
246
+
247
+ // Process queue when tracking is re-enabled
248
+ processQueue()
249
+ },
250
+
251
+ isTrackingEnabled: () => {
252
+ return !isTrackingBlocked
253
+ },
254
+
255
+ // Additional methods for React Native
256
+ processQueue,
257
+
258
+ getQueueLength: () => commandQueue.length,
259
+
260
+ /**
261
+ * Call this when the WebView signals it's ready.
262
+ * This will flush any queued commands.
263
+ */
264
+ setWebViewReady: () => {
265
+ log('WebView ready, processing queue')
266
+ processQueue()
267
+ },
268
+ }
269
+
270
+ return client
271
+ }
@@ -0,0 +1,29 @@
1
+ // WebView-based client (recommended for Fathom Pro)
2
+ export { FathomWebView, type FathomWebViewRef, type FathomWebViewProps } from './FathomWebView'
3
+ export { createWebViewClient, type WebViewFathomClient, type WebViewClientOptions } from './createWebViewClient'
4
+
5
+ // Provider components
6
+ export { NativeFathomProvider } from './NativeFathomProvider'
7
+ export { FathomProvider } from '../FathomProvider'
8
+
9
+ // Hooks
10
+ export { useFathom } from '../hooks/useFathom'
11
+ export { useAppStateTracking } from './useAppStateTracking'
12
+ export { useNavigationTracking } from './useNavigationTracking'
13
+
14
+ // Types
15
+ export type {
16
+ NativeFathomProviderProps,
17
+ UseNavigationTrackingOptions,
18
+ UseAppStateTrackingOptions,
19
+ // Re-exported from core
20
+ FathomClient,
21
+ EventOptions,
22
+ LoadOptions,
23
+ PageViewOptions,
24
+ } from './types'
25
+
26
+ export type {
27
+ FathomContextInterface,
28
+ FathomProviderProps,
29
+ } from '../types'