• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ryanhefner / react-fathom / f4939d82-a827-4457-9073-35d8b9107130

07 Jan 2026 08:13PM UTC coverage: 93.197% (-3.1%) from 96.266%
f4939d82-a827-4457-9073-35d8b9107130

Pull #3

circleci

web-flow
Merge pull request #4 from ryanhefner/claude/review-version-bump-1ixsA

feat(native): implement WebView-based Fathom client for React Native
Pull Request #3: [feat] React Native Client

175 of 193 branches covered (90.67%)

Branch coverage included in aggregate %.

224 of 243 new or added lines in 7 files covered. (92.18%)

373 of 395 relevant lines covered (94.43%)

16.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.92
/src/native/createWebViewClient.ts
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 = {},
45✔
64
): WebViewFathomClient {
65
  const { debug = false, enableQueue = true, maxQueueSize = 100 } = options
45✔
66

67
  let isTrackingBlocked = false
45✔
68
  let currentSiteId: string | undefined
69
  let isLoaded = false
45✔
70

71
  // Queue for commands sent before WebView is ready
72
  const commandQueue: QueuedCommand[] = []
45✔
73

74
  const log = (...args: unknown[]) => {
45✔
75
    if (debug) {
89✔
76
      console.log('[react-fathom/webview-client]', ...args)
3✔
77
    }
78
  }
79

80
  const warn = (...args: unknown[]) => {
45✔
81
    if (debug) {
3✔
82
      console.warn('[react-fathom/webview-client]', ...args)
1✔
83
    }
84
  }
85

86
  /**
87
   * Queue a command for later execution
88
   */
89
  const queueCommand = (type: QueuedCommand['type'], args: unknown[]) => {
45✔
90
    if (!enableQueue) {
34✔
91
      warn('Queue disabled, dropping command:', type)
2✔
92
      return
2✔
93
    }
94

95
    if (commandQueue.length >= maxQueueSize) {
32✔
96
      commandQueue.shift()
2✔
97
      log('Queue full, removed oldest command')
2✔
98
    }
99

100
    commandQueue.push({
32✔
101
      type,
102
      args,
103
      timestamp: Date.now(),
104
    })
105

106
    log(`Command queued (${commandQueue.length}/${maxQueueSize}):`, type)
32✔
107
  }
108

109
  /**
110
   * Process all queued commands
111
   */
112
  const processQueue = () => {
45✔
113
    const ref = getWebViewRef()
26✔
114
    if (!ref?.isReady()) {
26✔
115
      return 0
18✔
116
    }
117

118
    log(`Processing ${commandQueue.length} queued commands`)
8✔
119
    let processed = 0
8✔
120

121
    while (commandQueue.length > 0) {
8✔
122
      const command = commandQueue.shift()!
11✔
123

124
      switch (command.type) {
11!
125
        case 'pageview':
126
          ref.trackPageview(command.args[0] as PageViewOptions | undefined)
2✔
127
          break
2✔
128
        case 'event':
129
          ref.trackEvent(
8✔
130
            command.args[0] as string,
131
            command.args[1] as EventOptions | undefined,
132
          )
133
          break
8✔
134
        case 'goal':
135
          ref.trackGoal(command.args[0] as string, command.args[1] as number)
1✔
136
          break
1✔
137
        case 'block':
NEW
138
          ref.blockTrackingForMe()
×
NEW
139
          break
×
140
        case 'enable':
NEW
141
          ref.enableTrackingForMe()
×
NEW
142
          break
×
143
      }
144

145
      processed++
11✔
146
    }
147

148
    log(`Processed ${processed} queued commands`)
8✔
149
    return processed
8✔
150
  }
151

152
  /**
153
   * Execute a command immediately or queue it
154
   */
155
  const executeOrQueue = (
45✔
156
    type: QueuedCommand['type'],
157
    args: unknown[],
158
    executor: () => void,
159
  ) => {
160
    const ref = getWebViewRef()
47✔
161

162
    if (ref?.isReady()) {
47✔
163
      executor()
13✔
164
    } else {
165
      queueCommand(type, args)
34✔
166
    }
167
  }
168

169
  const client: WebViewFathomClient = {
45✔
170
    load: (siteId: string, opts?: LoadOptions) => {
171
      currentSiteId = siteId
16✔
172
      isLoaded = true
16✔
173
      log('Client loaded with site ID:', siteId)
16✔
174

175
      // Process any queued commands now that we're "loaded"
176
      // (actual WebView readiness is separate)
177
      processQueue()
16✔
178
    },
179

180
    trackPageview: (opts?: PageViewOptions) => {
181
      if (isTrackingBlocked) {
12✔
182
        log('Tracking blocked, skipping pageview')
1✔
183
        return
1✔
184
      }
185

186
      executeOrQueue('pageview', [opts], () => {
11✔
187
        const ref = getWebViewRef()
3✔
188
        ref?.trackPageview(opts)
3✔
189
        log('Tracked pageview:', opts)
3✔
190
      })
191
    },
192

193
    trackEvent: (eventName: string, opts?: EventOptions) => {
194
      if (isTrackingBlocked) {
22✔
195
        log('Tracking blocked, skipping event')
1✔
196
        return
1✔
197
      }
198

199
      executeOrQueue('event', [eventName, opts], () => {
21✔
200
        const ref = getWebViewRef()
2✔
201
        ref?.trackEvent(eventName, opts)
2✔
202
        log('Tracked event:', eventName, opts)
2✔
203
      })
204
    },
205

206
    trackGoal: (code: string, cents: number) => {
207
      if (isTrackingBlocked) {
7✔
208
        log('Tracking blocked, skipping goal')
1✔
209
        return
1✔
210
      }
211

212
      executeOrQueue('goal', [code, cents], () => {
6✔
213
        const ref = getWebViewRef()
1✔
214
        ref?.trackGoal(code, cents)
1✔
215
        log('Tracked goal:', code, cents)
1✔
216
      })
217
    },
218

219
    setSite: (id: string) => {
220
      currentSiteId = id
1✔
221
      log('Site ID changed to:', id)
1✔
222
      // Note: The WebView loads with a specific site ID, so changing it
223
      // at runtime would require reloading the WebView
224
      warn(
1✔
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
6✔
232
      executeOrQueue('block', [], () => {
6✔
233
        const ref = getWebViewRef()
5✔
234
        ref?.blockTrackingForMe()
5✔
235
      })
236
      log('Tracking blocked')
6✔
237
    },
238

239
    enableTrackingForMe: () => {
240
      isTrackingBlocked = false
3✔
241
      executeOrQueue('enable', [], () => {
3✔
242
        const ref = getWebViewRef()
2✔
243
        ref?.enableTrackingForMe()
2✔
244
      })
245
      log('Tracking enabled')
3✔
246

247
      // Process queue when tracking is re-enabled
248
      processQueue()
3✔
249
    },
250

251
    isTrackingEnabled: () => {
252
      return !isTrackingBlocked
3✔
253
    },
254

255
    // Additional methods for React Native
256
    processQueue,
257

258
    getQueueLength: () => commandQueue.length,
13✔
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')
4✔
266
      processQueue()
4✔
267
    },
268
  }
269

270
  return client
45✔
271
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc