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.
- package/README.md +886 -24
- package/dist/cjs/index.cjs +55 -9
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/native/index.cjs +1079 -0
- package/dist/cjs/native/index.cjs.map +1 -0
- package/dist/cjs/next/index.cjs +51 -5
- package/dist/cjs/next/index.cjs.map +1 -1
- package/dist/es/index.js +55 -9
- package/dist/es/index.js.map +1 -1
- package/dist/es/native/index.js +1071 -0
- package/dist/es/native/index.js.map +1 -0
- package/dist/es/next/index.js +51 -5
- package/dist/es/next/index.js.map +1 -1
- package/dist/react-fathom.js +55 -9
- package/dist/react-fathom.js.map +1 -1
- package/dist/react-fathom.min.js +2 -2
- package/dist/react-fathom.min.js.map +1 -1
- package/package.json +27 -4
- package/src/FathomContext.tsx +30 -1
- package/src/FathomProvider.test.tsx +115 -15
- package/src/FathomProvider.tsx +10 -2
- package/src/components/TrackClick.test.tsx +7 -7
- package/src/components/TrackClick.tsx +1 -1
- package/src/components/TrackVisible.test.tsx +7 -7
- package/src/components/TrackVisible.tsx +1 -1
- package/src/hooks/useFathom.test.tsx +14 -3
- package/src/hooks/useTrackOnClick.test.tsx +4 -4
- package/src/hooks/useTrackOnClick.ts +1 -1
- package/src/hooks/useTrackOnVisible.test.tsx +4 -4
- package/src/hooks/useTrackOnVisible.ts +1 -1
- package/src/index.ts +1 -0
- package/src/native/FathomWebView.test.tsx +410 -0
- package/src/native/FathomWebView.tsx +297 -0
- package/src/native/NativeFathomProvider.test.tsx +372 -0
- package/src/native/NativeFathomProvider.tsx +113 -0
- package/src/native/createWebViewClient.test.ts +380 -0
- package/src/native/createWebViewClient.ts +271 -0
- package/src/native/index.ts +29 -0
- package/src/native/react-native.d.ts +74 -0
- package/src/native/types.ts +145 -0
- package/src/native/useAppStateTracking.test.ts +249 -0
- package/src/native/useAppStateTracking.ts +66 -0
- package/src/native/useNavigationTracking.test.ts +446 -0
- package/src/native/useNavigationTracking.ts +177 -0
- package/src/types.ts +36 -9
- package/types/FathomContext.d.ts +1 -1
- package/types/FathomContext.d.ts.map +1 -1
- package/types/FathomProvider.d.ts.map +1 -1
- package/types/components/TrackClick.d.ts +1 -1
- package/types/components/TrackVisible.d.ts +1 -1
- package/types/hooks/useTrackOnClick.d.ts +1 -1
- package/types/hooks/useTrackOnVisible.d.ts +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/native/FathomWebView.d.ts +59 -0
- package/types/native/FathomWebView.d.ts.map +1 -0
- package/types/native/NativeFathomProvider.d.ts +36 -0
- package/types/native/NativeFathomProvider.d.ts.map +1 -0
- package/types/native/createWebViewClient.d.ts +51 -0
- package/types/native/createWebViewClient.d.ts.map +1 -0
- package/types/native/index.d.ts +10 -0
- package/types/native/index.d.ts.map +1 -0
- package/types/native/types.d.ts +125 -0
- package/types/native/types.d.ts.map +1 -0
- package/types/native/useAppStateTracking.d.ts +25 -0
- package/types/native/useAppStateTracking.d.ts.map +1 -0
- package/types/native/useNavigationTracking.d.ts +30 -0
- package/types/native/useNavigationTracking.d.ts.map +1 -0
- package/types/types.d.ts +34 -9
- 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'
|