@repliqo/sdk-react-native 0.3.8 → 0.4.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 (42) hide show
  1. package/INTEGRATION_GUIDE.md +1384 -1312
  2. package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +242 -242
  3. package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -94
  4. package/dist/components/RepliqoFlatList.d.ts +5 -0
  5. package/dist/components/RepliqoFlatList.js +39 -0
  6. package/dist/components/RepliqoScrollView.d.ts +3 -0
  7. package/dist/components/RepliqoScrollView.js +16 -265
  8. package/dist/components/useRepliqoScrollTracking.d.ts +33 -0
  9. package/dist/components/useRepliqoScrollTracking.js +304 -0
  10. package/dist/core/client.d.ts +52 -0
  11. package/dist/core/client.js +304 -16
  12. package/dist/core/config.d.ts +1 -1
  13. package/dist/core/config.js +5 -1
  14. package/dist/core/storage.d.ts +29 -0
  15. package/dist/core/storage.js +33 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +3 -1
  18. package/dist/snapshot/capture.d.ts +7 -0
  19. package/dist/snapshot/capture.js +17 -5
  20. package/dist/trackers/error.tracker.d.ts +25 -0
  21. package/dist/trackers/error.tracker.js +114 -37
  22. package/dist/trackers/navigation.tracker.js +11 -0
  23. package/dist/transport/api.client.d.ts +34 -1
  24. package/dist/transport/api.client.js +102 -19
  25. package/dist/transport/batch-queue.d.ts +53 -1
  26. package/dist/transport/batch-queue.js +126 -19
  27. package/dist/types/events.d.ts +12 -0
  28. package/package.json +68 -64
  29. package/src/components/RepliqoFlatList.tsx +64 -0
  30. package/src/components/RepliqoScrollView.tsx +53 -302
  31. package/src/components/useRepliqoScrollTracking.ts +366 -0
  32. package/src/core/client.ts +953 -628
  33. package/src/core/config.ts +38 -30
  34. package/src/core/storage.ts +51 -0
  35. package/src/index.ts +54 -50
  36. package/src/snapshot/NativeScreenCapture.ts +62 -62
  37. package/src/snapshot/capture.ts +154 -142
  38. package/src/trackers/error.tracker.ts +211 -136
  39. package/src/trackers/navigation.tracker.ts +107 -98
  40. package/src/transport/api.client.ts +430 -327
  41. package/src/transport/batch-queue.ts +212 -95
  42. package/src/types/events.ts +72 -60
@@ -1,302 +1,53 @@
1
- import React, { useCallback, useRef, useEffect } from 'react';
2
- import {
3
- ScrollView,
4
- Keyboard,
5
- Dimensions,
6
- type ScrollViewProps,
7
- type NativeSyntheticEvent,
8
- type NativeScrollEvent,
9
- type LayoutChangeEvent,
10
- } from 'react-native';
11
- import { AppAnalytics } from '../core/client';
12
-
13
- /** Minimum scroll delta (px) to trigger a new tile capture. */
14
- const MIN_DELTA_PX = 20;
15
- /** Debounce: wait this long after scroll stops before capturing. */
16
- const DEBOUNCE_MS = 250;
17
- /** Throttle: minimum time between tile captures. */
18
- const THROTTLE_MS = 400;
19
- /**
20
- * During continuous scroll, force a capture every this many ms.
21
- * Ensures tiles are captured even during fast, uninterrupted scrolls
22
- * where the user never pauses long enough for the debounce to fire.
23
- */
24
- const SCROLL_INTERVAL_MS = 1200;
25
- /**
26
- * During scroll, throttle window-bounds re-measurement to at most once per
27
- * this many ms. Bounds rarely change while scrolling, but we still want to
28
- * catch orientation changes / keyboard / overlay animations.
29
- */
30
- const REMEASURE_THROTTLE_MS = 500;
31
-
32
- let scrollViewIdCounter = 0;
33
- const nextScrollViewId = () => `repliqo-scroll-${++scrollViewIdCounter}`;
34
-
35
- /**
36
- * Drop-in replacement for React Native's ScrollView that:
37
- *
38
- * 1. Reports scroll offset for accurate heatmap touch coords
39
- * 2. Captures viewport tiles on scroll stops AND during continuous
40
- * scroll for collaborative screen building
41
- * 3. Registers its window-relative bounds with the SDK so touches can
42
- * be auto-classified as "content" (inside ScrollView) or "fixed"
43
- * (outside, e.g. nav bars/headers)
44
- */
45
- export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
46
- ({ onScroll, onMomentumScrollEnd, onScrollEndDrag, onLayout, onContentSizeChange, ...props }, ref) => {
47
- const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
48
- const scrollIntervalTimer = useRef<ReturnType<typeof setInterval> | null>(null);
49
- const initialLayoutTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
50
- const lastCaptureTime = useRef(0);
51
- const lastCaptureY = useRef(-999);
52
- const lastMeasureTime = useRef(0);
53
- const initialCaptured = useRef(false);
54
- const realContentHeight = useRef(0);
55
- const scrollViewId = useRef<string>(nextScrollViewId());
56
- const innerRef = useRef<ScrollView | null>(null);
57
- const isMounted = useRef(true);
58
- // Last measured window-relative top Y of the ScrollView — passed to
59
- // captureScrollTile so the backend can crop each tile to just the
60
- // scroll viewport area (no fixed header/footer duplication).
61
- const lastViewportTop = useRef<number | null>(null);
62
- const scrollState = useRef<{
63
- offsetY: number;
64
- viewportHeight: number;
65
- contentHeight: number;
66
- } | null>(null);
67
-
68
- // Forward the inner ref to the parent's ref (if provided).
69
- // IMPORTANT: do NOT clear innerRef on cleanup — RN sometimes calls the
70
- // ref callback with null mid-update, and clearing would break async
71
- // measurement callbacks that fire after the ref reattaches.
72
- const setRef = useCallback((node: ScrollView | null) => {
73
- if (node !== null) {
74
- innerRef.current = node;
75
- }
76
- if (typeof ref === 'function') {
77
- ref(node);
78
- } else if (ref) {
79
- (ref as React.MutableRefObject<ScrollView | null>).current = node;
80
- }
81
- }, [ref]);
82
-
83
- /**
84
- * Measure window-relative bounds and register with SDK.
85
- * Idempotent + safe to call after unmount (guards via isMounted).
86
- */
87
- const measureAndRegister = useCallback(() => {
88
- if (!isMounted.current) return;
89
- const node = innerRef.current as any;
90
- if (!node || typeof node.measureInWindow !== 'function') return;
91
- node.measureInWindow((x: number, y: number, width: number, height: number) => {
92
- if (!isMounted.current) return;
93
- // RN sometimes returns 0/0 dims before the view is positioned —
94
- // ignore those, the next onLayout/onScroll will re-measure.
95
- if (width <= 0 || height <= 0) return;
96
- lastViewportTop.current = y;
97
- try {
98
- AppAnalytics.getInstance().registerScrollView(scrollViewId.current, {
99
- x, y, width, height,
100
- });
101
- } catch {}
102
- });
103
- }, []);
104
-
105
- // Cleanup all timers + unregister + listeners on unmount
106
- useEffect(() => {
107
- const id = scrollViewId.current;
108
- isMounted.current = true;
109
-
110
- // Re-measure when keyboard appears/disappears (causes layout shift)
111
- const showSub = Keyboard.addListener('keyboardDidShow', measureAndRegister);
112
- const hideSub = Keyboard.addListener('keyboardDidHide', measureAndRegister);
113
-
114
- return () => {
115
- isMounted.current = false;
116
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
117
- if (scrollIntervalTimer.current) clearInterval(scrollIntervalTimer.current);
118
- if (initialLayoutTimer.current) clearTimeout(initialLayoutTimer.current);
119
- showSub.remove();
120
- hideSub.remove();
121
- try {
122
- AppAnalytics.getInstance().unregisterScrollView(id);
123
- } catch {}
124
- };
125
- }, [measureAndRegister]);
126
-
127
- /**
128
- * Core tile capture logic. Reads from refs (stable across renders).
129
- * Checks throttle + delta before capturing.
130
- *
131
- * @param force Skip throttle check (used for interval captures)
132
- */
133
- const doCapture = (force = false) => {
134
- if (!isMounted.current) return;
135
- const state = scrollState.current;
136
- if (!state || state.viewportHeight <= 0) return;
137
-
138
- const now = Date.now();
139
- if (!force && now - lastCaptureTime.current < THROTTLE_MS) return;
140
-
141
- // Always check delta — no point re-capturing the same position
142
- if (Math.abs(state.offsetY - lastCaptureY.current) < MIN_DELTA_PX &&
143
- initialCaptured.current) return;
144
-
145
- lastCaptureTime.current = now;
146
- lastCaptureY.current = state.offsetY;
147
- initialCaptured.current = true;
148
-
149
- try {
150
- const windowHeight = Dimensions.get('window').height;
151
- AppAnalytics.getInstance().captureScrollTile(
152
- state.offsetY,
153
- state.viewportHeight,
154
- state.contentHeight,
155
- lastViewportTop.current ?? undefined,
156
- windowHeight,
157
- );
158
- } catch {}
159
- };
160
-
161
- /** Start the interval timer for continuous scroll captures. */
162
- const startScrollInterval = () => {
163
- if (scrollIntervalTimer.current !== null) return;
164
- scrollIntervalTimer.current = setInterval(() => {
165
- doCapture(true); // force=true skips throttle
166
- }, SCROLL_INTERVAL_MS);
167
- };
168
-
169
- /** Stop the interval timer. */
170
- const stopScrollInterval = () => {
171
- if (scrollIntervalTimer.current !== null) {
172
- clearInterval(scrollIntervalTimer.current);
173
- scrollIntervalTimer.current = null;
174
- }
175
- };
176
-
177
- const handleScroll = useCallback(
178
- (event: NativeSyntheticEvent<NativeScrollEvent>) => {
179
- const { contentOffset, layoutMeasurement, contentSize } =
180
- event.nativeEvent;
181
-
182
- // Update SDK with current scroll offset (both legacy + new registry)
183
- try {
184
- const sdk = AppAnalytics.getInstance();
185
- sdk.setScrollOffset(contentOffset.y);
186
- sdk.updateScrollViewOffset(scrollViewId.current, contentOffset.y);
187
- } catch {}
188
-
189
- // Re-measure bounds occasionally during scroll — catches layout
190
- // shifts from animations, keyboard, orientation changes, etc.
191
- const now = Date.now();
192
- if (now - lastMeasureTime.current > REMEASURE_THROTTLE_MS) {
193
- lastMeasureTime.current = now;
194
- measureAndRegister();
195
- }
196
-
197
- // Update scroll state — use realContentHeight if available
198
- const bestContentHeight = realContentHeight.current > 0
199
- ? realContentHeight.current
200
- : contentSize.height;
201
- scrollState.current = {
202
- offsetY: contentOffset.y,
203
- viewportHeight: layoutMeasurement.height,
204
- contentHeight: bestContentHeight,
205
- };
206
-
207
- // Debounce: capture after scroll stops
208
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
209
- debounceTimer.current = setTimeout(() => doCapture(), DEBOUNCE_MS);
210
-
211
- // Start interval for continuous scroll captures
212
- startScrollInterval();
213
-
214
- // Capture initial tile (Y~0) on first scroll event with real dims
215
- if (!initialCaptured.current && contentOffset.y < 10) {
216
- if (initialLayoutTimer.current) clearTimeout(initialLayoutTimer.current);
217
- initialLayoutTimer.current = setTimeout(() => doCapture(), 200);
218
- }
219
-
220
- onScroll?.(event);
221
- },
222
- [onScroll, measureAndRegister],
223
- );
224
-
225
- const handleScrollEndDrag = useCallback(
226
- (event: NativeSyntheticEvent<NativeScrollEvent>) => {
227
- // User lifted finger — capture immediately
228
- stopScrollInterval();
229
- doCapture(true);
230
- onScrollEndDrag?.(event);
231
- },
232
- [onScrollEndDrag],
233
- );
234
-
235
- const handleMomentumEnd = useCallback(
236
- (event: NativeSyntheticEvent<NativeScrollEvent>) => {
237
- if (debounceTimer.current) {
238
- clearTimeout(debounceTimer.current);
239
- debounceTimer.current = null;
240
- }
241
- stopScrollInterval();
242
- doCapture(true);
243
- onMomentumScrollEnd?.(event);
244
- },
245
- [onMomentumScrollEnd],
246
- );
247
-
248
- const handleLayout = useCallback(
249
- (event: LayoutChangeEvent) => {
250
- const { height } = event.nativeEvent.layout;
251
- if (!initialCaptured.current && height > 0) {
252
- scrollState.current = {
253
- offsetY: 0,
254
- viewportHeight: height,
255
- contentHeight: realContentHeight.current || height,
256
- };
257
- // Clear any prior pending initial-capture timer before scheduling
258
- if (initialLayoutTimer.current) clearTimeout(initialLayoutTimer.current);
259
- initialLayoutTimer.current = setTimeout(() => doCapture(), 1000);
260
- }
261
- // Register window-relative bounds immediately (no setTimeout).
262
- // measureInWindow itself is async; if it returns 0/0 dims, the
263
- // next onLayout / onScroll will retry — no race window.
264
- measureAndRegister();
265
- onLayout?.(event);
266
- },
267
- [onLayout, measureAndRegister],
268
- );
269
-
270
- /**
271
- * Track the real content height from onContentSizeChange.
272
- * This is more reliable than the first scroll event and ensures
273
- * the initial tile at Y=0 has correct contentHeight metadata.
274
- */
275
- const handleContentSizeChange = useCallback(
276
- (w: number, h: number) => {
277
- realContentHeight.current = h;
278
- // Update scroll state if it exists
279
- if (scrollState.current) {
280
- scrollState.current.contentHeight = h;
281
- }
282
- onContentSizeChange?.(w, h);
283
- },
284
- [onContentSizeChange],
285
- );
286
-
287
- return (
288
- <ScrollView
289
- ref={setRef}
290
- {...props}
291
- onScroll={handleScroll}
292
- onScrollEndDrag={handleScrollEndDrag}
293
- onMomentumScrollEnd={handleMomentumEnd}
294
- onLayout={handleLayout}
295
- onContentSizeChange={handleContentSizeChange}
296
- scrollEventThrottle={props.scrollEventThrottle ?? 16}
297
- />
298
- );
299
- },
300
- );
301
-
302
- RepliqoScrollView.displayName = 'RepliqoScrollView';
1
+ import React from 'react';
2
+ import { ScrollView, type ScrollViewProps } from 'react-native';
3
+ import { useRepliqoScrollTracking } from './useRepliqoScrollTracking';
4
+
5
+ /**
6
+ * Drop-in replacement for React Native's ScrollView that:
7
+ *
8
+ * 1. Reports scroll offset for accurate heatmap touch coords
9
+ * 2. Captures viewport tiles on scroll stops AND during continuous
10
+ * scroll for collaborative screen building
11
+ * 3. Registers its window-relative bounds with the SDK so touches can
12
+ * be auto-classified as "content" (inside ScrollView) or "fixed"
13
+ * (outside, e.g. nav bars/headers)
14
+ *
15
+ * All analytics behavior lives in useRepliqoScrollTracking (shared with
16
+ * RepliqoFlatList).
17
+ */
18
+ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
19
+ (
20
+ {
21
+ onScroll,
22
+ onMomentumScrollEnd,
23
+ onScrollEndDrag,
24
+ onLayout,
25
+ onContentSizeChange,
26
+ ...props
27
+ },
28
+ ref,
29
+ ) => {
30
+ const tracking = useRepliqoScrollTracking(ref, {
31
+ onScroll,
32
+ onMomentumScrollEnd,
33
+ onScrollEndDrag,
34
+ onLayout,
35
+ onContentSizeChange,
36
+ });
37
+
38
+ return (
39
+ <ScrollView
40
+ ref={tracking.setRef}
41
+ {...props}
42
+ onScroll={tracking.onScroll}
43
+ onScrollEndDrag={tracking.onScrollEndDrag}
44
+ onMomentumScrollEnd={tracking.onMomentumScrollEnd}
45
+ onLayout={tracking.onLayout}
46
+ onContentSizeChange={tracking.onContentSizeChange}
47
+ scrollEventThrottle={props.scrollEventThrottle ?? 16}
48
+ />
49
+ );
50
+ },
51
+ );
52
+
53
+ RepliqoScrollView.displayName = 'RepliqoScrollView';