@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,94 +1,94 @@
1
- package com.repliqo.screencapture;
2
-
3
- import android.app.Activity;
4
- import android.os.Handler;
5
- import android.os.Looper;
6
- import android.util.Log;
7
-
8
- import androidx.annotation.NonNull;
9
-
10
- import com.facebook.react.bridge.Arguments;
11
- import com.facebook.react.bridge.Promise;
12
- import com.facebook.react.bridge.ReactApplicationContext;
13
- import com.facebook.react.bridge.ReactContextBaseJavaModule;
14
- import com.facebook.react.bridge.ReactMethod;
15
- import com.facebook.react.bridge.WritableMap;
16
-
17
- /**
18
- * React Native native module for full-screen capture INCLUDING modals,
19
- * alerts, and system dialogs.
20
- *
21
- * Uses MultiWindowCapture which draws all app windows via View.draw()
22
- * and composites them. NO permissions required. NO MediaProjection.
23
- * NO foreground service.
24
- *
25
- * JS API:
26
- * NativeModules.ScreenCaptureModule.captureFrame() → Promise<{image,width,height,format}>
27
- *
28
- * The TS wrapper (NativeScreenCapture.ts) detects availability via
29
- * NativeModules.ScreenCaptureModule != null, so isAvailable() is kept
30
- * for explicit checks but not required.
31
- */
32
- public class ScreenCaptureModule extends ReactContextBaseJavaModule {
33
- private static final String TAG = "RepliqoCapture";
34
- private static final String MODULE_NAME = "ScreenCaptureModule";
35
-
36
- private final Handler mainHandler = new Handler(Looper.getMainLooper());
37
-
38
- public ScreenCaptureModule(ReactApplicationContext reactContext) {
39
- super(reactContext);
40
- }
41
-
42
- @NonNull
43
- @Override
44
- public String getName() {
45
- return MODULE_NAME;
46
- }
47
-
48
- /**
49
- * Check if the native capture module is available.
50
- * Always true on Android (no permissions needed).
51
- */
52
- @ReactMethod
53
- public void isAvailable(Promise promise) {
54
- promise.resolve(true);
55
- }
56
-
57
- /**
58
- * Capture a single frame including all visible windows.
59
- * Must run View.draw() on the UI thread, so we post to the main handler.
60
- *
61
- * Returns: { image: base64, width: int, height: int, format: "jpeg" }
62
- */
63
- @ReactMethod
64
- public void captureFrame(Promise promise) {
65
- Activity activity = getCurrentActivity();
66
- if (activity == null) {
67
- promise.resolve(null);
68
- return;
69
- }
70
-
71
- // View.draw() must run on the UI thread
72
- mainHandler.post(() -> {
73
- try {
74
- MultiWindowCapture.CaptureResult result =
75
- MultiWindowCapture.capture(activity);
76
-
77
- if (result == null) {
78
- promise.resolve(null);
79
- return;
80
- }
81
-
82
- WritableMap map = Arguments.createMap();
83
- map.putString("image", result.image);
84
- map.putInt("width", result.width);
85
- map.putInt("height", result.height);
86
- map.putString("format", "jpeg");
87
- promise.resolve(map);
88
- } catch (Exception e) {
89
- Log.w(TAG, "captureFrame failed: " + e.getMessage());
90
- promise.resolve(null);
91
- }
92
- });
93
- }
94
- }
1
+ package com.repliqo.screencapture;
2
+
3
+ import android.app.Activity;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
6
+ import android.util.Log;
7
+
8
+ import androidx.annotation.NonNull;
9
+
10
+ import com.facebook.react.bridge.Arguments;
11
+ import com.facebook.react.bridge.Promise;
12
+ import com.facebook.react.bridge.ReactApplicationContext;
13
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
14
+ import com.facebook.react.bridge.ReactMethod;
15
+ import com.facebook.react.bridge.WritableMap;
16
+
17
+ /**
18
+ * React Native native module for full-screen capture INCLUDING modals,
19
+ * alerts, and system dialogs.
20
+ *
21
+ * Uses MultiWindowCapture which draws all app windows via View.draw()
22
+ * and composites them. NO permissions required. NO MediaProjection.
23
+ * NO foreground service.
24
+ *
25
+ * JS API:
26
+ * NativeModules.ScreenCaptureModule.captureFrame() → Promise<{image,width,height,format}>
27
+ *
28
+ * The TS wrapper (NativeScreenCapture.ts) detects availability via
29
+ * NativeModules.ScreenCaptureModule != null, so isAvailable() is kept
30
+ * for explicit checks but not required.
31
+ */
32
+ public class ScreenCaptureModule extends ReactContextBaseJavaModule {
33
+ private static final String TAG = "RepliqoCapture";
34
+ private static final String MODULE_NAME = "ScreenCaptureModule";
35
+
36
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
37
+
38
+ public ScreenCaptureModule(ReactApplicationContext reactContext) {
39
+ super(reactContext);
40
+ }
41
+
42
+ @NonNull
43
+ @Override
44
+ public String getName() {
45
+ return MODULE_NAME;
46
+ }
47
+
48
+ /**
49
+ * Check if the native capture module is available.
50
+ * Always true on Android (no permissions needed).
51
+ */
52
+ @ReactMethod
53
+ public void isAvailable(Promise promise) {
54
+ promise.resolve(true);
55
+ }
56
+
57
+ /**
58
+ * Capture a single frame including all visible windows.
59
+ * Must run View.draw() on the UI thread, so we post to the main handler.
60
+ *
61
+ * Returns: { image: base64, width: int, height: int, format: "jpeg" }
62
+ */
63
+ @ReactMethod
64
+ public void captureFrame(Promise promise) {
65
+ Activity activity = getCurrentActivity();
66
+ if (activity == null) {
67
+ promise.resolve(null);
68
+ return;
69
+ }
70
+
71
+ // View.draw() must run on the UI thread
72
+ mainHandler.post(() -> {
73
+ try {
74
+ MultiWindowCapture.CaptureResult result =
75
+ MultiWindowCapture.capture(activity);
76
+
77
+ if (result == null) {
78
+ promise.resolve(null);
79
+ return;
80
+ }
81
+
82
+ WritableMap map = Arguments.createMap();
83
+ map.putString("image", result.image);
84
+ map.putInt("width", result.width);
85
+ map.putInt("height", result.height);
86
+ map.putString("format", "jpeg");
87
+ promise.resolve(map);
88
+ } catch (Exception e) {
89
+ Log.w(TAG, "captureFrame failed: " + e.getMessage());
90
+ promise.resolve(null);
91
+ }
92
+ });
93
+ }
94
+ }
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ import { FlatList, type FlatListProps } from 'react-native';
3
+ export declare const RepliqoFlatList: <ItemT>(props: FlatListProps<ItemT> & {
4
+ ref?: React.Ref<FlatList<ItemT>>;
5
+ }) => React.ReactElement;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RepliqoFlatList = void 0;
7
+ const react_1 = __importDefault(require("react"));
8
+ const react_native_1 = require("react-native");
9
+ const useRepliqoScrollTracking_1 = require("./useRepliqoScrollTracking");
10
+ /**
11
+ * Drop-in replacement for React Native's FlatList with the same analytics
12
+ * behavior as RepliqoScrollView:
13
+ *
14
+ * 1. Reports scroll offset for accurate heatmap touch coords
15
+ * 2. Captures viewport tiles on scroll stops AND during continuous
16
+ * scroll for collaborative screen building
17
+ * 3. Registers its window-relative bounds with the SDK so touches can
18
+ * be auto-classified as "content" (inside the list) or "fixed" UI
19
+ *
20
+ * Note on tiles: FlatList virtualizes — off-screen rows aren't rendered —
21
+ * but that's exactly why the collaborative tile approach works here too:
22
+ * tiles are captured from what's ON screen as the user naturally scrolls.
23
+ *
24
+ * Note on contentHeight: with virtualization, RN estimates the content
25
+ * size until rows render; onContentSizeChange keeps our metadata in sync
26
+ * as the estimate refines.
27
+ */
28
+ function RepliqoFlatListInner({ onScroll, onMomentumScrollEnd, onScrollEndDrag, onLayout, onContentSizeChange, ...props }, ref) {
29
+ const tracking = (0, useRepliqoScrollTracking_1.useRepliqoScrollTracking)(ref, {
30
+ onScroll,
31
+ onMomentumScrollEnd,
32
+ onScrollEndDrag,
33
+ onLayout,
34
+ onContentSizeChange,
35
+ });
36
+ return (<react_native_1.FlatList ref={tracking.setRef} {...props} onScroll={tracking.onScroll} onScrollEndDrag={tracking.onScrollEndDrag} onMomentumScrollEnd={tracking.onMomentumScrollEnd} onLayout={tracking.onLayout} onContentSizeChange={tracking.onContentSizeChange} scrollEventThrottle={props.scrollEventThrottle ?? 16}/>);
37
+ }
38
+ exports.RepliqoFlatList = react_1.default.forwardRef(RepliqoFlatListInner);
39
+ exports.RepliqoFlatList.displayName = 'RepliqoFlatList';
@@ -9,5 +9,8 @@ import { ScrollView, type ScrollViewProps } from 'react-native';
9
9
  * 3. Registers its window-relative bounds with the SDK so touches can
10
10
  * be auto-classified as "content" (inside ScrollView) or "fixed"
11
11
  * (outside, e.g. nav bars/headers)
12
+ *
13
+ * All analytics behavior lives in useRepliqoScrollTracking (shared with
14
+ * RepliqoFlatList).
12
15
  */
13
16
  export declare const RepliqoScrollView: React.ForwardRefExoticComponent<ScrollViewProps & React.RefAttributes<ScrollView>>;
@@ -1,62 +1,12 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
35
5
  Object.defineProperty(exports, "__esModule", { value: true });
36
6
  exports.RepliqoScrollView = void 0;
37
- const react_1 = __importStar(require("react"));
7
+ const react_1 = __importDefault(require("react"));
38
8
  const react_native_1 = require("react-native");
39
- const client_1 = require("../core/client");
40
- /** Minimum scroll delta (px) to trigger a new tile capture. */
41
- const MIN_DELTA_PX = 20;
42
- /** Debounce: wait this long after scroll stops before capturing. */
43
- const DEBOUNCE_MS = 250;
44
- /** Throttle: minimum time between tile captures. */
45
- const THROTTLE_MS = 400;
46
- /**
47
- * During continuous scroll, force a capture every this many ms.
48
- * Ensures tiles are captured even during fast, uninterrupted scrolls
49
- * where the user never pauses long enough for the debounce to fire.
50
- */
51
- const SCROLL_INTERVAL_MS = 1200;
52
- /**
53
- * During scroll, throttle window-bounds re-measurement to at most once per
54
- * this many ms. Bounds rarely change while scrolling, but we still want to
55
- * catch orientation changes / keyboard / overlay animations.
56
- */
57
- const REMEASURE_THROTTLE_MS = 500;
58
- let scrollViewIdCounter = 0;
59
- const nextScrollViewId = () => `repliqo-scroll-${++scrollViewIdCounter}`;
9
+ const useRepliqoScrollTracking_1 = require("./useRepliqoScrollTracking");
60
10
  /**
61
11
  * Drop-in replacement for React Native's ScrollView that:
62
12
  *
@@ -66,217 +16,18 @@ const nextScrollViewId = () => `repliqo-scroll-${++scrollViewIdCounter}`;
66
16
  * 3. Registers its window-relative bounds with the SDK so touches can
67
17
  * be auto-classified as "content" (inside ScrollView) or "fixed"
68
18
  * (outside, e.g. nav bars/headers)
19
+ *
20
+ * All analytics behavior lives in useRepliqoScrollTracking (shared with
21
+ * RepliqoFlatList).
69
22
  */
70
23
  exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumScrollEnd, onScrollEndDrag, onLayout, onContentSizeChange, ...props }, ref) => {
71
- const debounceTimer = (0, react_1.useRef)(null);
72
- const scrollIntervalTimer = (0, react_1.useRef)(null);
73
- const initialLayoutTimer = (0, react_1.useRef)(null);
74
- const lastCaptureTime = (0, react_1.useRef)(0);
75
- const lastCaptureY = (0, react_1.useRef)(-999);
76
- const lastMeasureTime = (0, react_1.useRef)(0);
77
- const initialCaptured = (0, react_1.useRef)(false);
78
- const realContentHeight = (0, react_1.useRef)(0);
79
- const scrollViewId = (0, react_1.useRef)(nextScrollViewId());
80
- const innerRef = (0, react_1.useRef)(null);
81
- const isMounted = (0, react_1.useRef)(true);
82
- // Last measured window-relative top Y of the ScrollView — passed to
83
- // captureScrollTile so the backend can crop each tile to just the
84
- // scroll viewport area (no fixed header/footer duplication).
85
- const lastViewportTop = (0, react_1.useRef)(null);
86
- const scrollState = (0, react_1.useRef)(null);
87
- // Forward the inner ref to the parent's ref (if provided).
88
- // IMPORTANT: do NOT clear innerRef on cleanup — RN sometimes calls the
89
- // ref callback with null mid-update, and clearing would break async
90
- // measurement callbacks that fire after the ref reattaches.
91
- const setRef = (0, react_1.useCallback)((node) => {
92
- if (node !== null) {
93
- innerRef.current = node;
94
- }
95
- if (typeof ref === 'function') {
96
- ref(node);
97
- }
98
- else if (ref) {
99
- ref.current = node;
100
- }
101
- }, [ref]);
102
- /**
103
- * Measure window-relative bounds and register with SDK.
104
- * Idempotent + safe to call after unmount (guards via isMounted).
105
- */
106
- const measureAndRegister = (0, react_1.useCallback)(() => {
107
- if (!isMounted.current)
108
- return;
109
- const node = innerRef.current;
110
- if (!node || typeof node.measureInWindow !== 'function')
111
- return;
112
- node.measureInWindow((x, y, width, height) => {
113
- if (!isMounted.current)
114
- return;
115
- // RN sometimes returns 0/0 dims before the view is positioned —
116
- // ignore those, the next onLayout/onScroll will re-measure.
117
- if (width <= 0 || height <= 0)
118
- return;
119
- lastViewportTop.current = y;
120
- try {
121
- client_1.AppAnalytics.getInstance().registerScrollView(scrollViewId.current, {
122
- x, y, width, height,
123
- });
124
- }
125
- catch { }
126
- });
127
- }, []);
128
- // Cleanup all timers + unregister + listeners on unmount
129
- (0, react_1.useEffect)(() => {
130
- const id = scrollViewId.current;
131
- isMounted.current = true;
132
- // Re-measure when keyboard appears/disappears (causes layout shift)
133
- const showSub = react_native_1.Keyboard.addListener('keyboardDidShow', measureAndRegister);
134
- const hideSub = react_native_1.Keyboard.addListener('keyboardDidHide', measureAndRegister);
135
- return () => {
136
- isMounted.current = false;
137
- if (debounceTimer.current)
138
- clearTimeout(debounceTimer.current);
139
- if (scrollIntervalTimer.current)
140
- clearInterval(scrollIntervalTimer.current);
141
- if (initialLayoutTimer.current)
142
- clearTimeout(initialLayoutTimer.current);
143
- showSub.remove();
144
- hideSub.remove();
145
- try {
146
- client_1.AppAnalytics.getInstance().unregisterScrollView(id);
147
- }
148
- catch { }
149
- };
150
- }, [measureAndRegister]);
151
- /**
152
- * Core tile capture logic. Reads from refs (stable across renders).
153
- * Checks throttle + delta before capturing.
154
- *
155
- * @param force Skip throttle check (used for interval captures)
156
- */
157
- const doCapture = (force = false) => {
158
- if (!isMounted.current)
159
- return;
160
- const state = scrollState.current;
161
- if (!state || state.viewportHeight <= 0)
162
- return;
163
- const now = Date.now();
164
- if (!force && now - lastCaptureTime.current < THROTTLE_MS)
165
- return;
166
- // Always check delta — no point re-capturing the same position
167
- if (Math.abs(state.offsetY - lastCaptureY.current) < MIN_DELTA_PX &&
168
- initialCaptured.current)
169
- return;
170
- lastCaptureTime.current = now;
171
- lastCaptureY.current = state.offsetY;
172
- initialCaptured.current = true;
173
- try {
174
- const windowHeight = react_native_1.Dimensions.get('window').height;
175
- client_1.AppAnalytics.getInstance().captureScrollTile(state.offsetY, state.viewportHeight, state.contentHeight, lastViewportTop.current ?? undefined, windowHeight);
176
- }
177
- catch { }
178
- };
179
- /** Start the interval timer for continuous scroll captures. */
180
- const startScrollInterval = () => {
181
- if (scrollIntervalTimer.current !== null)
182
- return;
183
- scrollIntervalTimer.current = setInterval(() => {
184
- doCapture(true); // force=true skips throttle
185
- }, SCROLL_INTERVAL_MS);
186
- };
187
- /** Stop the interval timer. */
188
- const stopScrollInterval = () => {
189
- if (scrollIntervalTimer.current !== null) {
190
- clearInterval(scrollIntervalTimer.current);
191
- scrollIntervalTimer.current = null;
192
- }
193
- };
194
- const handleScroll = (0, react_1.useCallback)((event) => {
195
- const { contentOffset, layoutMeasurement, contentSize } = event.nativeEvent;
196
- // Update SDK with current scroll offset (both legacy + new registry)
197
- try {
198
- const sdk = client_1.AppAnalytics.getInstance();
199
- sdk.setScrollOffset(contentOffset.y);
200
- sdk.updateScrollViewOffset(scrollViewId.current, contentOffset.y);
201
- }
202
- catch { }
203
- // Re-measure bounds occasionally during scroll — catches layout
204
- // shifts from animations, keyboard, orientation changes, etc.
205
- const now = Date.now();
206
- if (now - lastMeasureTime.current > REMEASURE_THROTTLE_MS) {
207
- lastMeasureTime.current = now;
208
- measureAndRegister();
209
- }
210
- // Update scroll state — use realContentHeight if available
211
- const bestContentHeight = realContentHeight.current > 0
212
- ? realContentHeight.current
213
- : contentSize.height;
214
- scrollState.current = {
215
- offsetY: contentOffset.y,
216
- viewportHeight: layoutMeasurement.height,
217
- contentHeight: bestContentHeight,
218
- };
219
- // Debounce: capture after scroll stops
220
- if (debounceTimer.current)
221
- clearTimeout(debounceTimer.current);
222
- debounceTimer.current = setTimeout(() => doCapture(), DEBOUNCE_MS);
223
- // Start interval for continuous scroll captures
224
- startScrollInterval();
225
- // Capture initial tile (Y~0) on first scroll event with real dims
226
- if (!initialCaptured.current && contentOffset.y < 10) {
227
- if (initialLayoutTimer.current)
228
- clearTimeout(initialLayoutTimer.current);
229
- initialLayoutTimer.current = setTimeout(() => doCapture(), 200);
230
- }
231
- onScroll?.(event);
232
- }, [onScroll, measureAndRegister]);
233
- const handleScrollEndDrag = (0, react_1.useCallback)((event) => {
234
- // User lifted finger — capture immediately
235
- stopScrollInterval();
236
- doCapture(true);
237
- onScrollEndDrag?.(event);
238
- }, [onScrollEndDrag]);
239
- const handleMomentumEnd = (0, react_1.useCallback)((event) => {
240
- if (debounceTimer.current) {
241
- clearTimeout(debounceTimer.current);
242
- debounceTimer.current = null;
243
- }
244
- stopScrollInterval();
245
- doCapture(true);
246
- onMomentumScrollEnd?.(event);
247
- }, [onMomentumScrollEnd]);
248
- const handleLayout = (0, react_1.useCallback)((event) => {
249
- const { height } = event.nativeEvent.layout;
250
- if (!initialCaptured.current && height > 0) {
251
- scrollState.current = {
252
- offsetY: 0,
253
- viewportHeight: height,
254
- contentHeight: realContentHeight.current || height,
255
- };
256
- // Clear any prior pending initial-capture timer before scheduling
257
- if (initialLayoutTimer.current)
258
- 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
- }, [onLayout, measureAndRegister]);
267
- /**
268
- * Track the real content height from onContentSizeChange.
269
- * This is more reliable than the first scroll event and ensures
270
- * the initial tile at Y=0 has correct contentHeight metadata.
271
- */
272
- const handleContentSizeChange = (0, react_1.useCallback)((w, h) => {
273
- realContentHeight.current = h;
274
- // Update scroll state if it exists
275
- if (scrollState.current) {
276
- scrollState.current.contentHeight = h;
277
- }
278
- onContentSizeChange?.(w, h);
279
- }, [onContentSizeChange]);
280
- return (<react_native_1.ScrollView ref={setRef} {...props} onScroll={handleScroll} onScrollEndDrag={handleScrollEndDrag} onMomentumScrollEnd={handleMomentumEnd} onLayout={handleLayout} onContentSizeChange={handleContentSizeChange} scrollEventThrottle={props.scrollEventThrottle ?? 16}/>);
24
+ const tracking = (0, useRepliqoScrollTracking_1.useRepliqoScrollTracking)(ref, {
25
+ onScroll,
26
+ onMomentumScrollEnd,
27
+ onScrollEndDrag,
28
+ onLayout,
29
+ onContentSizeChange,
30
+ });
31
+ return (<react_native_1.ScrollView ref={tracking.setRef} {...props} onScroll={tracking.onScroll} onScrollEndDrag={tracking.onScrollEndDrag} onMomentumScrollEnd={tracking.onMomentumScrollEnd} onLayout={tracking.onLayout} onContentSizeChange={tracking.onContentSizeChange} scrollEventThrottle={props.scrollEventThrottle ?? 16}/>);
281
32
  });
282
33
  exports.RepliqoScrollView.displayName = 'RepliqoScrollView';
@@ -0,0 +1,33 @@
1
+ import { type NativeSyntheticEvent, type NativeScrollEvent, type LayoutChangeEvent } from 'react-native';
2
+ export interface RepliqoScrollHandlers {
3
+ /** Attach as the scrollable's ref (also forwards to the user's ref). */
4
+ setRef: (node: unknown) => void;
5
+ onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
6
+ onScrollEndDrag: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
7
+ onMomentumScrollEnd: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
8
+ onLayout: (event: LayoutChangeEvent) => void;
9
+ onContentSizeChange: (w: number, h: number) => void;
10
+ }
11
+ interface ForwardedHandlers {
12
+ onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
13
+ onScrollEndDrag?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
14
+ onMomentumScrollEnd?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
15
+ onLayout?: (event: LayoutChangeEvent) => void;
16
+ onContentSizeChange?: (w: number, h: number) => void;
17
+ }
18
+ /**
19
+ * Shared scroll-analytics behavior behind RepliqoScrollView and
20
+ * RepliqoFlatList:
21
+ *
22
+ * 1. Reports scroll offset for accurate heatmap touch coords
23
+ * 2. Captures viewport tiles on scroll stops AND during continuous
24
+ * scroll for collaborative screen building
25
+ * 3. Registers window-relative bounds with the SDK so touches are
26
+ * auto-classified as content (inside) vs fixed UI (outside)
27
+ *
28
+ * @param forwardedRef The consumer's ref (function or object).
29
+ * @param userHandlers The consumer's own scroll/layout handlers — always
30
+ * called after the SDK's.
31
+ */
32
+ export declare function useRepliqoScrollTracking(forwardedRef: React.Ref<unknown>, userHandlers: ForwardedHandlers): RepliqoScrollHandlers;
33
+ export {};