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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type declarations for react-native modules used by react-fathom/native.
|
|
3
|
+
* This allows the package to compile without requiring react-native as a dev dependency.
|
|
4
|
+
*/
|
|
5
|
+
declare module 'react-native' {
|
|
6
|
+
export type AppStateStatus = 'active' | 'background' | 'inactive'
|
|
7
|
+
|
|
8
|
+
export interface AppStateStatic {
|
|
9
|
+
currentState: AppStateStatus
|
|
10
|
+
addEventListener(
|
|
11
|
+
event: 'change',
|
|
12
|
+
handler: (state: AppStateStatus) => void,
|
|
13
|
+
): { remove: () => void }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const AppState: AppStateStatic
|
|
17
|
+
|
|
18
|
+
export interface ViewStyle {
|
|
19
|
+
position?: 'absolute' | 'relative'
|
|
20
|
+
width?: number | string
|
|
21
|
+
height?: number | string
|
|
22
|
+
overflow?: 'visible' | 'hidden' | 'scroll'
|
|
23
|
+
opacity?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StyleSheetStatic {
|
|
27
|
+
create<T extends Record<string, ViewStyle>>(styles: T): T
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const StyleSheet: StyleSheetStatic
|
|
31
|
+
|
|
32
|
+
export interface ViewProps {
|
|
33
|
+
style?: ViewStyle
|
|
34
|
+
children?: React.ReactNode
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const View: React.FC<ViewProps>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare module 'react-native-webview' {
|
|
41
|
+
import type { Ref, RefObject, Component } from 'react'
|
|
42
|
+
|
|
43
|
+
export interface WebViewMessageEvent {
|
|
44
|
+
nativeEvent: {
|
|
45
|
+
data: string
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WebViewErrorEvent {
|
|
50
|
+
nativeEvent: {
|
|
51
|
+
description?: string
|
|
52
|
+
code?: number
|
|
53
|
+
domain?: string
|
|
54
|
+
url?: string
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface WebViewProps {
|
|
59
|
+
source?: { html: string; uri?: never } | { uri: string; html?: never }
|
|
60
|
+
onMessage?: (event: WebViewMessageEvent) => void
|
|
61
|
+
onError?: (event: WebViewErrorEvent) => void
|
|
62
|
+
javaScriptEnabled?: boolean
|
|
63
|
+
domStorageEnabled?: boolean
|
|
64
|
+
style?: Record<string, unknown>
|
|
65
|
+
scrollEnabled?: boolean
|
|
66
|
+
bounces?: boolean
|
|
67
|
+
cacheEnabled?: boolean
|
|
68
|
+
incognito?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class WebView extends Component<WebViewProps> {
|
|
72
|
+
injectJavaScript(script: string): void
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react'
|
|
2
|
+
import type { FathomClient, EventOptions, LoadOptions, PageViewOptions } from '../types'
|
|
3
|
+
import type { WebViewFathomClient } from './createWebViewClient'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for the NativeFathomProvider component
|
|
7
|
+
*/
|
|
8
|
+
export interface NativeFathomProviderProps {
|
|
9
|
+
/**
|
|
10
|
+
* Your Fathom Analytics site ID
|
|
11
|
+
*/
|
|
12
|
+
siteId: string
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options passed to fathom.load() in the WebView
|
|
16
|
+
*/
|
|
17
|
+
loadOptions?: LoadOptions
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Custom domain for Fathom script (if using Fathom's custom domains feature)
|
|
21
|
+
* @default 'cdn.usefathom.com'
|
|
22
|
+
*/
|
|
23
|
+
scriptDomain?: string
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default options merged into all trackPageview calls
|
|
27
|
+
*/
|
|
28
|
+
defaultPageviewOptions?: PageViewOptions
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default options merged into all trackEvent calls
|
|
32
|
+
*/
|
|
33
|
+
defaultEventOptions?: EventOptions
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Enable automatic app state tracking (foreground/background)
|
|
37
|
+
* Tracks 'app-foreground' and 'app-background' events
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
trackAppState?: boolean
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Enable debug logging
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
46
|
+
debug?: boolean
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Called when the Fathom script has loaded and is ready
|
|
50
|
+
*/
|
|
51
|
+
onReady?: () => void
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Called when an error occurs loading the script
|
|
55
|
+
*/
|
|
56
|
+
onError?: (error: string) => void
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A ref that will be populated with the WebView-based Fathom client instance.
|
|
60
|
+
* This allows the parent component that composes the provider to access
|
|
61
|
+
* the client directly, since it cannot use useFathom() (context only flows
|
|
62
|
+
* downward to children).
|
|
63
|
+
*
|
|
64
|
+
* The client is a WebViewFathomClient which extends FathomClient with
|
|
65
|
+
* additional methods for queue management.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* import { NativeFathomProvider, WebViewFathomClient } from 'react-fathom/native'
|
|
70
|
+
*
|
|
71
|
+
* function App() {
|
|
72
|
+
* const clientRef = useRef<WebViewFathomClient>(null);
|
|
73
|
+
*
|
|
74
|
+
* const handleDeepLink = (url: string) => {
|
|
75
|
+
* clientRef.current?.trackEvent('deep_link', { _url: url });
|
|
76
|
+
* };
|
|
77
|
+
*
|
|
78
|
+
* return (
|
|
79
|
+
* <NativeFathomProvider siteId="..." clientRef={clientRef}>
|
|
80
|
+
* <YourApp />
|
|
81
|
+
* </NativeFathomProvider>
|
|
82
|
+
* );
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
clientRef?: MutableRefObject<WebViewFathomClient | null>
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Children to render
|
|
90
|
+
*/
|
|
91
|
+
children: React.ReactNode
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Options for the useNavigationTracking hook
|
|
96
|
+
*/
|
|
97
|
+
export interface UseNavigationTrackingOptions {
|
|
98
|
+
/**
|
|
99
|
+
* React Navigation navigation container ref
|
|
100
|
+
*/
|
|
101
|
+
navigationRef: React.RefObject<any>
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Transform the route name before tracking (e.g., add prefixes)
|
|
105
|
+
*/
|
|
106
|
+
transformRouteName?: (routeName: string) => string
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Filter which routes should be tracked (return false to skip)
|
|
110
|
+
*/
|
|
111
|
+
shouldTrackRoute?: (routeName: string, params?: Record<string, any>) => boolean
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Include route params in the tracked URL
|
|
115
|
+
*/
|
|
116
|
+
includeParams?: boolean
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Options for the useAppStateTracking hook
|
|
121
|
+
*/
|
|
122
|
+
export interface UseAppStateTrackingOptions {
|
|
123
|
+
/**
|
|
124
|
+
* Event name for when the app comes to foreground (default: 'app-foreground')
|
|
125
|
+
*/
|
|
126
|
+
foregroundEventName?: string
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Event name for when the app goes to background (default: 'app-background')
|
|
130
|
+
*/
|
|
131
|
+
backgroundEventName?: string
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Additional event options to include with app state events
|
|
135
|
+
*/
|
|
136
|
+
eventOptions?: EventOptions
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Callback when app state changes
|
|
140
|
+
*/
|
|
141
|
+
onStateChange?: (state: 'active' | 'background' | 'inactive') => void
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Re-export core types for convenience
|
|
145
|
+
export type { FathomClient, EventOptions, LoadOptions, PageViewOptions }
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { renderHook, act } from '@testing-library/react'
|
|
6
|
+
|
|
7
|
+
import { FathomProvider } from '../FathomProvider'
|
|
8
|
+
import { useAppStateTracking } from './useAppStateTracking'
|
|
9
|
+
|
|
10
|
+
// Store the listener callback for testing
|
|
11
|
+
let appStateChangeCallback: ((state: string) => void) | null = null
|
|
12
|
+
const mockRemove = vi.fn()
|
|
13
|
+
|
|
14
|
+
// Mock react-native AppState
|
|
15
|
+
vi.mock('react-native', () => ({
|
|
16
|
+
AppState: {
|
|
17
|
+
currentState: 'active',
|
|
18
|
+
addEventListener: vi.fn((event, callback) => {
|
|
19
|
+
if (event === 'change') {
|
|
20
|
+
appStateChangeCallback = callback
|
|
21
|
+
}
|
|
22
|
+
return { remove: mockRemove }
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
describe('useAppStateTracking', () => {
|
|
28
|
+
const mockTrackEvent = vi.fn()
|
|
29
|
+
const mockClient = {
|
|
30
|
+
trackEvent: mockTrackEvent,
|
|
31
|
+
trackPageview: vi.fn(),
|
|
32
|
+
trackGoal: vi.fn(),
|
|
33
|
+
load: vi.fn(),
|
|
34
|
+
setSite: vi.fn(),
|
|
35
|
+
blockTrackingForMe: vi.fn(),
|
|
36
|
+
enableTrackingForMe: vi.fn(),
|
|
37
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks()
|
|
42
|
+
appStateChangeCallback = null
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const createWrapper = () => {
|
|
46
|
+
return ({ children }: { children: React.ReactNode }) =>
|
|
47
|
+
React.createElement(FathomProvider, { client: mockClient }, children)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
it('should set up AppState listener on mount', async () => {
|
|
51
|
+
const reactNative = await import('react-native')
|
|
52
|
+
|
|
53
|
+
renderHook(() => useAppStateTracking(), { wrapper: createWrapper() })
|
|
54
|
+
|
|
55
|
+
expect(reactNative.AppState.addEventListener).toHaveBeenCalledWith(
|
|
56
|
+
'change',
|
|
57
|
+
expect.any(Function),
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should clean up listener on unmount', () => {
|
|
62
|
+
const { unmount } = renderHook(() => useAppStateTracking(), {
|
|
63
|
+
wrapper: createWrapper(),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
unmount()
|
|
67
|
+
|
|
68
|
+
expect(mockRemove).toHaveBeenCalled()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should track foreground event when app becomes active', () => {
|
|
72
|
+
renderHook(() => useAppStateTracking(), { wrapper: createWrapper() })
|
|
73
|
+
|
|
74
|
+
// Simulate app going to background then foreground
|
|
75
|
+
act(() => {
|
|
76
|
+
appStateChangeCallback?.('background')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
act(() => {
|
|
80
|
+
appStateChangeCallback?.('active')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('app-foreground', expect.anything())
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should track background event when app goes to background', () => {
|
|
87
|
+
renderHook(() => useAppStateTracking(), { wrapper: createWrapper() })
|
|
88
|
+
|
|
89
|
+
// Simulate app going to background (from active state)
|
|
90
|
+
act(() => {
|
|
91
|
+
appStateChangeCallback?.('background')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('app-background', expect.anything())
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should use custom foreground event name', () => {
|
|
98
|
+
renderHook(
|
|
99
|
+
() =>
|
|
100
|
+
useAppStateTracking({
|
|
101
|
+
foregroundEventName: 'app-resumed',
|
|
102
|
+
}),
|
|
103
|
+
{ wrapper: createWrapper() },
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// Simulate app going to background then foreground
|
|
107
|
+
act(() => {
|
|
108
|
+
appStateChangeCallback?.('background')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
act(() => {
|
|
112
|
+
appStateChangeCallback?.('active')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('app-resumed', expect.anything())
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should use custom background event name', () => {
|
|
119
|
+
renderHook(
|
|
120
|
+
() =>
|
|
121
|
+
useAppStateTracking({
|
|
122
|
+
backgroundEventName: 'app-paused',
|
|
123
|
+
}),
|
|
124
|
+
{ wrapper: createWrapper() },
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
act(() => {
|
|
128
|
+
appStateChangeCallback?.('background')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('app-paused', expect.anything())
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should include eventOptions in tracked events', () => {
|
|
135
|
+
const eventOptions = { _site_id: 'app-state-tracking' }
|
|
136
|
+
|
|
137
|
+
renderHook(
|
|
138
|
+
() =>
|
|
139
|
+
useAppStateTracking({
|
|
140
|
+
eventOptions,
|
|
141
|
+
}),
|
|
142
|
+
{ wrapper: createWrapper() },
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
act(() => {
|
|
146
|
+
appStateChangeCallback?.('background')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('app-background', eventOptions)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should call onStateChange callback when state changes', () => {
|
|
153
|
+
const onStateChange = vi.fn()
|
|
154
|
+
|
|
155
|
+
renderHook(
|
|
156
|
+
() =>
|
|
157
|
+
useAppStateTracking({
|
|
158
|
+
onStateChange,
|
|
159
|
+
}),
|
|
160
|
+
{ wrapper: createWrapper() },
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
act(() => {
|
|
164
|
+
appStateChangeCallback?.('background')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
expect(onStateChange).toHaveBeenCalledWith('background')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('should track inactive state as background', () => {
|
|
171
|
+
renderHook(() => useAppStateTracking(), { wrapper: createWrapper() })
|
|
172
|
+
|
|
173
|
+
act(() => {
|
|
174
|
+
appStateChangeCallback?.('inactive')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('app-background', expect.anything())
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should track foreground when transitioning from inactive to active', () => {
|
|
181
|
+
renderHook(() => useAppStateTracking(), { wrapper: createWrapper() })
|
|
182
|
+
|
|
183
|
+
// Go to inactive first
|
|
184
|
+
act(() => {
|
|
185
|
+
appStateChangeCallback?.('inactive')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
mockTrackEvent.mockClear()
|
|
189
|
+
|
|
190
|
+
// Then to active
|
|
191
|
+
act(() => {
|
|
192
|
+
appStateChangeCallback?.('active')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('app-foreground', expect.anything())
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should not track when transitioning between inactive and background', () => {
|
|
199
|
+
renderHook(() => useAppStateTracking(), { wrapper: createWrapper() })
|
|
200
|
+
|
|
201
|
+
// Go to inactive first
|
|
202
|
+
act(() => {
|
|
203
|
+
appStateChangeCallback?.('inactive')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
mockTrackEvent.mockClear()
|
|
207
|
+
|
|
208
|
+
// Then to background (no foreground event should be tracked)
|
|
209
|
+
act(() => {
|
|
210
|
+
appStateChangeCallback?.('background')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Should not track foreground event
|
|
214
|
+
expect(mockTrackEvent).not.toHaveBeenCalledWith(
|
|
215
|
+
'app-foreground',
|
|
216
|
+
expect.anything(),
|
|
217
|
+
)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should work with all options combined', () => {
|
|
221
|
+
const onStateChange = vi.fn()
|
|
222
|
+
const eventOptions = { _value: 1 }
|
|
223
|
+
|
|
224
|
+
renderHook(
|
|
225
|
+
() =>
|
|
226
|
+
useAppStateTracking({
|
|
227
|
+
foregroundEventName: 'resumed',
|
|
228
|
+
backgroundEventName: 'paused',
|
|
229
|
+
eventOptions,
|
|
230
|
+
onStateChange,
|
|
231
|
+
}),
|
|
232
|
+
{ wrapper: createWrapper() },
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
act(() => {
|
|
236
|
+
appStateChangeCallback?.('background')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('paused', eventOptions)
|
|
240
|
+
expect(onStateChange).toHaveBeenCalledWith('background')
|
|
241
|
+
|
|
242
|
+
act(() => {
|
|
243
|
+
appStateChangeCallback?.('active')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('resumed', eventOptions)
|
|
247
|
+
expect(onStateChange).toHaveBeenCalledWith('active')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { AppState, type AppStateStatus } from 'react-native'
|
|
3
|
+
|
|
4
|
+
import { useFathom } from '../hooks/useFathom'
|
|
5
|
+
import type { UseAppStateTrackingOptions } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook that tracks app state changes (foreground/background) as Fathom events.
|
|
9
|
+
*
|
|
10
|
+
* This is useful for understanding user engagement patterns and session behavior.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { useAppStateTracking } from 'react-fathom/native'
|
|
15
|
+
*
|
|
16
|
+
* function App() {
|
|
17
|
+
* useAppStateTracking({
|
|
18
|
+
* foregroundEventName: 'app-resumed',
|
|
19
|
+
* backgroundEventName: 'app-paused',
|
|
20
|
+
* onStateChange: (state) => {
|
|
21
|
+
* console.log('App state:', state)
|
|
22
|
+
* },
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* return <YourApp />
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useAppStateTracking(options: UseAppStateTrackingOptions = {}) {
|
|
30
|
+
const {
|
|
31
|
+
foregroundEventName = 'app-foreground',
|
|
32
|
+
backgroundEventName = 'app-background',
|
|
33
|
+
eventOptions,
|
|
34
|
+
onStateChange,
|
|
35
|
+
} = options
|
|
36
|
+
|
|
37
|
+
const { trackEvent } = useFathom()
|
|
38
|
+
const appStateRef = useRef<AppStateStatus>(AppState.currentState)
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
42
|
+
const previousState = appStateRef.current
|
|
43
|
+
|
|
44
|
+
// Track when app comes to foreground
|
|
45
|
+
if (previousState.match(/inactive|background/) && nextAppState === 'active') {
|
|
46
|
+
trackEvent?.(foregroundEventName, eventOptions)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Track when app goes to background
|
|
50
|
+
if (previousState === 'active' && nextAppState.match(/inactive|background/)) {
|
|
51
|
+
trackEvent?.(backgroundEventName, eventOptions)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Call the optional state change callback
|
|
55
|
+
onStateChange?.(nextAppState as 'active' | 'background' | 'inactive')
|
|
56
|
+
|
|
57
|
+
appStateRef.current = nextAppState
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const subscription = AppState.addEventListener('change', handleAppStateChange)
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
subscription.remove()
|
|
64
|
+
}
|
|
65
|
+
}, [trackEvent, foregroundEventName, backgroundEventName, eventOptions, onStateChange])
|
|
66
|
+
}
|