@repliqo/sdk-react-native 0.3.4 → 0.3.5
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.
|
@@ -5,7 +5,9 @@ import { ScrollView, type ScrollViewProps } from 'react-native';
|
|
|
5
5
|
*
|
|
6
6
|
* 1. Reports scroll offset for accurate heatmap touch coords
|
|
7
7
|
* 2. Captures viewport tiles on scroll stops AND during continuous
|
|
8
|
-
* scroll for collaborative screen building
|
|
9
|
-
*
|
|
8
|
+
* scroll for collaborative screen building
|
|
9
|
+
* 3. Registers its window-relative bounds with the SDK so touches can
|
|
10
|
+
* be auto-classified as "content" (inside ScrollView) or "fixed"
|
|
11
|
+
* (outside, e.g. nav bars/headers)
|
|
10
12
|
*/
|
|
11
13
|
export declare const RepliqoScrollView: React.ForwardRefExoticComponent<ScrollViewProps & React.RefAttributes<ScrollView>>;
|
|
@@ -49,13 +49,17 @@ const THROTTLE_MS = 400;
|
|
|
49
49
|
* where the user never pauses long enough for the debounce to fire.
|
|
50
50
|
*/
|
|
51
51
|
const SCROLL_INTERVAL_MS = 1200;
|
|
52
|
+
let scrollViewIdCounter = 0;
|
|
53
|
+
const nextScrollViewId = () => `repliqo-scroll-${++scrollViewIdCounter}`;
|
|
52
54
|
/**
|
|
53
55
|
* Drop-in replacement for React Native's ScrollView that:
|
|
54
56
|
*
|
|
55
57
|
* 1. Reports scroll offset for accurate heatmap touch coords
|
|
56
58
|
* 2. Captures viewport tiles on scroll stops AND during continuous
|
|
57
|
-
* scroll for collaborative screen building
|
|
58
|
-
*
|
|
59
|
+
* scroll for collaborative screen building
|
|
60
|
+
* 3. Registers its window-relative bounds with the SDK so touches can
|
|
61
|
+
* be auto-classified as "content" (inside ScrollView) or "fixed"
|
|
62
|
+
* (outside, e.g. nav bars/headers)
|
|
59
63
|
*/
|
|
60
64
|
exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumScrollEnd, onScrollEndDrag, onLayout, onContentSizeChange, ...props }, ref) => {
|
|
61
65
|
const debounceTimer = (0, react_1.useRef)(null);
|
|
@@ -64,14 +68,47 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
|
|
|
64
68
|
const lastCaptureY = (0, react_1.useRef)(-999);
|
|
65
69
|
const initialCaptured = (0, react_1.useRef)(false);
|
|
66
70
|
const realContentHeight = (0, react_1.useRef)(0);
|
|
71
|
+
const scrollViewId = (0, react_1.useRef)(nextScrollViewId());
|
|
72
|
+
const innerRef = (0, react_1.useRef)(null);
|
|
67
73
|
const scrollState = (0, react_1.useRef)(null);
|
|
68
|
-
//
|
|
74
|
+
// Forward the inner ref to the parent's ref (if provided)
|
|
75
|
+
const setRef = (0, react_1.useCallback)((node) => {
|
|
76
|
+
innerRef.current = node;
|
|
77
|
+
if (typeof ref === 'function') {
|
|
78
|
+
ref(node);
|
|
79
|
+
}
|
|
80
|
+
else if (ref) {
|
|
81
|
+
ref.current = node;
|
|
82
|
+
}
|
|
83
|
+
}, [ref]);
|
|
84
|
+
/** Measure window-relative bounds and register with SDK. */
|
|
85
|
+
const measureAndRegister = (0, react_1.useCallback)(() => {
|
|
86
|
+
const node = innerRef.current;
|
|
87
|
+
if (!node || typeof node.measureInWindow !== 'function')
|
|
88
|
+
return;
|
|
89
|
+
node.measureInWindow((x, y, width, height) => {
|
|
90
|
+
if (width <= 0 || height <= 0)
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
client_1.AppAnalytics.getInstance().registerScrollView(scrollViewId.current, {
|
|
94
|
+
x, y, width, height,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch { }
|
|
98
|
+
});
|
|
99
|
+
}, []);
|
|
100
|
+
// Cleanup timers + unregister on unmount
|
|
69
101
|
(0, react_1.useEffect)(() => {
|
|
102
|
+
const id = scrollViewId.current;
|
|
70
103
|
return () => {
|
|
71
104
|
if (debounceTimer.current)
|
|
72
105
|
clearTimeout(debounceTimer.current);
|
|
73
106
|
if (scrollIntervalTimer.current)
|
|
74
107
|
clearInterval(scrollIntervalTimer.current);
|
|
108
|
+
try {
|
|
109
|
+
client_1.AppAnalytics.getInstance().unregisterScrollView(id);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
75
112
|
};
|
|
76
113
|
}, []);
|
|
77
114
|
/**
|
|
@@ -116,9 +153,11 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
|
|
|
116
153
|
};
|
|
117
154
|
const handleScroll = (0, react_1.useCallback)((event) => {
|
|
118
155
|
const { contentOffset, layoutMeasurement, contentSize } = event.nativeEvent;
|
|
119
|
-
//
|
|
156
|
+
// Update SDK with current scroll offset (both legacy + new registry)
|
|
120
157
|
try {
|
|
121
|
-
client_1.AppAnalytics.getInstance()
|
|
158
|
+
const sdk = client_1.AppAnalytics.getInstance();
|
|
159
|
+
sdk.setScrollOffset(contentOffset.y);
|
|
160
|
+
sdk.updateScrollViewOffset(scrollViewId.current, contentOffset.y);
|
|
122
161
|
}
|
|
123
162
|
catch { }
|
|
124
163
|
// Update scroll state — use realContentHeight if available
|
|
@@ -168,8 +207,12 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
|
|
|
168
207
|
};
|
|
169
208
|
setTimeout(() => doCapture(), 1000);
|
|
170
209
|
}
|
|
210
|
+
// Register window-relative bounds with the SDK.
|
|
211
|
+
// Defer measurement to next tick — onLayout fires before the view
|
|
212
|
+
// is positioned in the window on some Android versions.
|
|
213
|
+
setTimeout(measureAndRegister, 50);
|
|
171
214
|
onLayout?.(event);
|
|
172
|
-
}, [onLayout]);
|
|
215
|
+
}, [onLayout, measureAndRegister]);
|
|
173
216
|
/**
|
|
174
217
|
* Track the real content height from onContentSizeChange.
|
|
175
218
|
* This is more reliable than the first scroll event and ensures
|
|
@@ -183,6 +226,6 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
|
|
|
183
226
|
}
|
|
184
227
|
onContentSizeChange?.(w, h);
|
|
185
228
|
}, [onContentSizeChange]);
|
|
186
|
-
return (<react_native_1.ScrollView ref={
|
|
229
|
+
return (<react_native_1.ScrollView ref={setRef} {...props} onScroll={handleScroll} onScrollEndDrag={handleScrollEndDrag} onMomentumScrollEnd={handleMomentumEnd} onLayout={handleLayout} onContentSizeChange={handleContentSizeChange} scrollEventThrottle={props.scrollEventThrottle ?? 16}/>);
|
|
187
230
|
});
|
|
188
231
|
exports.RepliqoScrollView.displayName = 'RepliqoScrollView';
|
package/dist/core/client.d.ts
CHANGED
|
@@ -14,6 +14,18 @@ export declare class AppAnalytics {
|
|
|
14
14
|
private scrollOffsetY;
|
|
15
15
|
private logger;
|
|
16
16
|
private appStateSubscription;
|
|
17
|
+
/**
|
|
18
|
+
* Registry of active ScrollViews on the current screen.
|
|
19
|
+
* Each RepliqoScrollView registers its window-relative bounds and current
|
|
20
|
+
* scroll state. Used by trackTouch to auto-detect:
|
|
21
|
+
* - If touch is INSIDE a ScrollView → adjust Y by that ScrollView's
|
|
22
|
+
* scrollOffset, mark as content touch
|
|
23
|
+
* - If touch is OUTSIDE all ScrollViews → mark as fixed UI (no adjust)
|
|
24
|
+
*
|
|
25
|
+
* This makes heatmaps render fixed elements (nav bars, headers) at their
|
|
26
|
+
* viewport position, while still mapping scrollable content correctly.
|
|
27
|
+
*/
|
|
28
|
+
private scrollViewRegistry;
|
|
17
29
|
private constructor();
|
|
18
30
|
static init(config: SDKConfig): AppAnalytics;
|
|
19
31
|
static getInstance(): AppAnalytics;
|
|
@@ -29,8 +41,32 @@ export declare class AppAnalytics {
|
|
|
29
41
|
* or manually from the host app's ScrollView onScroll handler.
|
|
30
42
|
* The offset is added to touch Y coordinates for accurate heatmaps
|
|
31
43
|
* on scrollable screens.
|
|
44
|
+
*
|
|
45
|
+
* @deprecated Prefer registerScrollView + updateScrollViewOffset for
|
|
46
|
+
* auto-detection of fixed vs scrollable touches. Kept for backward compat.
|
|
32
47
|
*/
|
|
33
48
|
setScrollOffset(y: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* Register a ScrollView's window-relative bounds with the SDK.
|
|
51
|
+
* Called by RepliqoScrollView on layout. Used to auto-detect whether
|
|
52
|
+
* a touch is on scrollable content or on fixed UI (nav bars, headers).
|
|
53
|
+
*/
|
|
54
|
+
registerScrollView(id: string, bounds: {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
}): void;
|
|
60
|
+
/**
|
|
61
|
+
* Update the scroll offset for a previously-registered ScrollView.
|
|
62
|
+
* Called by RepliqoScrollView on every scroll event.
|
|
63
|
+
*/
|
|
64
|
+
updateScrollViewOffset(id: string, scrollOffsetY: number): void;
|
|
65
|
+
/**
|
|
66
|
+
* Remove a ScrollView from the registry. Called by RepliqoScrollView
|
|
67
|
+
* on unmount.
|
|
68
|
+
*/
|
|
69
|
+
unregisterScrollView(id: string): void;
|
|
34
70
|
/**
|
|
35
71
|
* Capture the current viewport as a tile for collaborative screen
|
|
36
72
|
* building. Called by RepliqoScrollView on scroll stops.
|
package/dist/core/client.js
CHANGED
|
@@ -18,6 +18,18 @@ class AppAnalytics {
|
|
|
18
18
|
this.currentScreenEnteredAt = null;
|
|
19
19
|
this.scrollOffsetY = 0;
|
|
20
20
|
this.appStateSubscription = null;
|
|
21
|
+
/**
|
|
22
|
+
* Registry of active ScrollViews on the current screen.
|
|
23
|
+
* Each RepliqoScrollView registers its window-relative bounds and current
|
|
24
|
+
* scroll state. Used by trackTouch to auto-detect:
|
|
25
|
+
* - If touch is INSIDE a ScrollView → adjust Y by that ScrollView's
|
|
26
|
+
* scrollOffset, mark as content touch
|
|
27
|
+
* - If touch is OUTSIDE all ScrollViews → mark as fixed UI (no adjust)
|
|
28
|
+
*
|
|
29
|
+
* This makes heatmaps render fixed elements (nav bars, headers) at their
|
|
30
|
+
* viewport position, while still mapping scrollable content correctly.
|
|
31
|
+
*/
|
|
32
|
+
this.scrollViewRegistry = new Map();
|
|
21
33
|
this.config = (0, config_1.resolveConfig)(config);
|
|
22
34
|
this.logger = new logger_1.Logger(this.config.debug);
|
|
23
35
|
this.apiClient = new api_client_1.ApiClient(config_1.REPLIQO_API_URL, this.config.apiKey, this.logger);
|
|
@@ -163,14 +175,41 @@ class AppAnalytics {
|
|
|
163
175
|
if (!this.config.enableTouchTracking) {
|
|
164
176
|
return;
|
|
165
177
|
}
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
178
|
+
// Auto-detect: find which (if any) registered ScrollView contains
|
|
179
|
+
// this touch. If inside, adjust Y by that ScrollView's scroll offset.
|
|
180
|
+
// If outside all ScrollViews, treat as fixed UI (no adjustment).
|
|
181
|
+
let activeScrollOffset = 0;
|
|
182
|
+
let isFixed = true;
|
|
183
|
+
if (this.scrollViewRegistry.size > 0) {
|
|
184
|
+
for (const info of this.scrollViewRegistry.values()) {
|
|
185
|
+
const { bounds } = info;
|
|
186
|
+
if (x >= bounds.x &&
|
|
187
|
+
x <= bounds.x + bounds.width &&
|
|
188
|
+
y >= bounds.y &&
|
|
189
|
+
y <= bounds.y + bounds.height) {
|
|
190
|
+
activeScrollOffset = info.scrollOffsetY;
|
|
191
|
+
isFixed = false;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// No ScrollViews registered → fall back to legacy behavior
|
|
198
|
+
// (use the manually-set scrollOffsetY, treat as content touch).
|
|
199
|
+
activeScrollOffset = this.scrollOffsetY;
|
|
200
|
+
isFixed = false;
|
|
201
|
+
}
|
|
202
|
+
const adjustedY = y + activeScrollOffset;
|
|
170
203
|
const event = {
|
|
171
204
|
type: 'touch',
|
|
172
205
|
screenName: screenName || this.currentScreen || undefined,
|
|
173
|
-
data: {
|
|
206
|
+
data: {
|
|
207
|
+
x,
|
|
208
|
+
y: adjustedY,
|
|
209
|
+
scrollOffsetY: activeScrollOffset,
|
|
210
|
+
isFixed,
|
|
211
|
+
...extra,
|
|
212
|
+
},
|
|
174
213
|
timestamp: new Date().toISOString(),
|
|
175
214
|
};
|
|
176
215
|
this.eventQueue.add(event);
|
|
@@ -219,10 +258,42 @@ class AppAnalytics {
|
|
|
219
258
|
* or manually from the host app's ScrollView onScroll handler.
|
|
220
259
|
* The offset is added to touch Y coordinates for accurate heatmaps
|
|
221
260
|
* on scrollable screens.
|
|
261
|
+
*
|
|
262
|
+
* @deprecated Prefer registerScrollView + updateScrollViewOffset for
|
|
263
|
+
* auto-detection of fixed vs scrollable touches. Kept for backward compat.
|
|
222
264
|
*/
|
|
223
265
|
setScrollOffset(y) {
|
|
224
266
|
this.scrollOffsetY = y;
|
|
225
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Register a ScrollView's window-relative bounds with the SDK.
|
|
270
|
+
* Called by RepliqoScrollView on layout. Used to auto-detect whether
|
|
271
|
+
* a touch is on scrollable content or on fixed UI (nav bars, headers).
|
|
272
|
+
*/
|
|
273
|
+
registerScrollView(id, bounds) {
|
|
274
|
+
const existing = this.scrollViewRegistry.get(id);
|
|
275
|
+
this.scrollViewRegistry.set(id, {
|
|
276
|
+
bounds,
|
|
277
|
+
scrollOffsetY: existing?.scrollOffsetY ?? 0,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Update the scroll offset for a previously-registered ScrollView.
|
|
282
|
+
* Called by RepliqoScrollView on every scroll event.
|
|
283
|
+
*/
|
|
284
|
+
updateScrollViewOffset(id, scrollOffsetY) {
|
|
285
|
+
const existing = this.scrollViewRegistry.get(id);
|
|
286
|
+
if (existing) {
|
|
287
|
+
existing.scrollOffsetY = scrollOffsetY;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Remove a ScrollView from the registry. Called by RepliqoScrollView
|
|
292
|
+
* on unmount.
|
|
293
|
+
*/
|
|
294
|
+
unregisterScrollView(id) {
|
|
295
|
+
this.scrollViewRegistry.delete(id);
|
|
296
|
+
}
|
|
226
297
|
/**
|
|
227
298
|
* Capture the current viewport as a tile for collaborative screen
|
|
228
299
|
* building. Called by RepliqoScrollView on scroll stops.
|
|
@@ -282,6 +353,8 @@ class AppAnalytics {
|
|
|
282
353
|
this.currentScreen = screenName;
|
|
283
354
|
this.currentScreenEnteredAt = new Date().toISOString();
|
|
284
355
|
this.scrollOffsetY = 0;
|
|
356
|
+
// Clear the ScrollView registry — old screen's scrollviews are gone
|
|
357
|
+
this.scrollViewRegistry.clear();
|
|
285
358
|
this.errorTracker?.setCurrentScreen(screenName);
|
|
286
359
|
this.snapshotCapture?.setCurrentScreen(screenName);
|
|
287
360
|
// NOTE: autoScanScreen (PixelCopy / programmatic scroll) is DISABLED.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@repliqo/sdk-react-native",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Session replay & analytics SDK for React Native. Captures screenshots (including modals/alerts), navigation, screen visits, and custom events.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -21,13 +21,18 @@ const THROTTLE_MS = 400;
|
|
|
21
21
|
*/
|
|
22
22
|
const SCROLL_INTERVAL_MS = 1200;
|
|
23
23
|
|
|
24
|
+
let scrollViewIdCounter = 0;
|
|
25
|
+
const nextScrollViewId = () => `repliqo-scroll-${++scrollViewIdCounter}`;
|
|
26
|
+
|
|
24
27
|
/**
|
|
25
28
|
* Drop-in replacement for React Native's ScrollView that:
|
|
26
29
|
*
|
|
27
30
|
* 1. Reports scroll offset for accurate heatmap touch coords
|
|
28
31
|
* 2. Captures viewport tiles on scroll stops AND during continuous
|
|
29
|
-
* scroll for collaborative screen building
|
|
30
|
-
*
|
|
32
|
+
* scroll for collaborative screen building
|
|
33
|
+
* 3. Registers its window-relative bounds with the SDK so touches can
|
|
34
|
+
* be auto-classified as "content" (inside ScrollView) or "fixed"
|
|
35
|
+
* (outside, e.g. nav bars/headers)
|
|
31
36
|
*/
|
|
32
37
|
export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
33
38
|
({ onScroll, onMomentumScrollEnd, onScrollEndDrag, onLayout, onContentSizeChange, ...props }, ref) => {
|
|
@@ -37,17 +42,47 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
|
37
42
|
const lastCaptureY = useRef(-999);
|
|
38
43
|
const initialCaptured = useRef(false);
|
|
39
44
|
const realContentHeight = useRef(0);
|
|
45
|
+
const scrollViewId = useRef<string>(nextScrollViewId());
|
|
46
|
+
const innerRef = useRef<ScrollView | null>(null);
|
|
40
47
|
const scrollState = useRef<{
|
|
41
48
|
offsetY: number;
|
|
42
49
|
viewportHeight: number;
|
|
43
50
|
contentHeight: number;
|
|
44
51
|
} | null>(null);
|
|
45
52
|
|
|
46
|
-
//
|
|
53
|
+
// Forward the inner ref to the parent's ref (if provided)
|
|
54
|
+
const setRef = useCallback((node: ScrollView | null) => {
|
|
55
|
+
innerRef.current = node;
|
|
56
|
+
if (typeof ref === 'function') {
|
|
57
|
+
ref(node);
|
|
58
|
+
} else if (ref) {
|
|
59
|
+
(ref as React.MutableRefObject<ScrollView | null>).current = node;
|
|
60
|
+
}
|
|
61
|
+
}, [ref]);
|
|
62
|
+
|
|
63
|
+
/** Measure window-relative bounds and register with SDK. */
|
|
64
|
+
const measureAndRegister = useCallback(() => {
|
|
65
|
+
const node = innerRef.current as any;
|
|
66
|
+
if (!node || typeof node.measureInWindow !== 'function') return;
|
|
67
|
+
node.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
68
|
+
if (width <= 0 || height <= 0) return;
|
|
69
|
+
try {
|
|
70
|
+
AppAnalytics.getInstance().registerScrollView(scrollViewId.current, {
|
|
71
|
+
x, y, width, height,
|
|
72
|
+
});
|
|
73
|
+
} catch {}
|
|
74
|
+
});
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
// Cleanup timers + unregister on unmount
|
|
47
78
|
useEffect(() => {
|
|
79
|
+
const id = scrollViewId.current;
|
|
48
80
|
return () => {
|
|
49
81
|
if (debounceTimer.current) clearTimeout(debounceTimer.current);
|
|
50
82
|
if (scrollIntervalTimer.current) clearInterval(scrollIntervalTimer.current);
|
|
83
|
+
try {
|
|
84
|
+
AppAnalytics.getInstance().unregisterScrollView(id);
|
|
85
|
+
} catch {}
|
|
51
86
|
};
|
|
52
87
|
}, []);
|
|
53
88
|
|
|
@@ -102,9 +137,11 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
|
102
137
|
const { contentOffset, layoutMeasurement, contentSize } =
|
|
103
138
|
event.nativeEvent;
|
|
104
139
|
|
|
105
|
-
//
|
|
140
|
+
// Update SDK with current scroll offset (both legacy + new registry)
|
|
106
141
|
try {
|
|
107
|
-
AppAnalytics.getInstance()
|
|
142
|
+
const sdk = AppAnalytics.getInstance();
|
|
143
|
+
sdk.setScrollOffset(contentOffset.y);
|
|
144
|
+
sdk.updateScrollViewOffset(scrollViewId.current, contentOffset.y);
|
|
108
145
|
} catch {}
|
|
109
146
|
|
|
110
147
|
// Update scroll state — use realContentHeight if available
|
|
@@ -169,9 +206,13 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
|
169
206
|
};
|
|
170
207
|
setTimeout(() => doCapture(), 1000);
|
|
171
208
|
}
|
|
209
|
+
// Register window-relative bounds with the SDK.
|
|
210
|
+
// Defer measurement to next tick — onLayout fires before the view
|
|
211
|
+
// is positioned in the window on some Android versions.
|
|
212
|
+
setTimeout(measureAndRegister, 50);
|
|
172
213
|
onLayout?.(event);
|
|
173
214
|
},
|
|
174
|
-
[onLayout],
|
|
215
|
+
[onLayout, measureAndRegister],
|
|
175
216
|
);
|
|
176
217
|
|
|
177
218
|
/**
|
|
@@ -193,7 +234,7 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
|
193
234
|
|
|
194
235
|
return (
|
|
195
236
|
<ScrollView
|
|
196
|
-
ref={
|
|
237
|
+
ref={setRef}
|
|
197
238
|
{...props}
|
|
198
239
|
onScroll={handleScroll}
|
|
199
240
|
onScrollEndDrag={handleScrollEndDrag}
|
package/src/core/client.ts
CHANGED
|
@@ -31,6 +31,25 @@ export class AppAnalytics {
|
|
|
31
31
|
private logger: Logger;
|
|
32
32
|
private appStateSubscription: { remove: () => void } | null = null;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Registry of active ScrollViews on the current screen.
|
|
36
|
+
* Each RepliqoScrollView registers its window-relative bounds and current
|
|
37
|
+
* scroll state. Used by trackTouch to auto-detect:
|
|
38
|
+
* - If touch is INSIDE a ScrollView → adjust Y by that ScrollView's
|
|
39
|
+
* scrollOffset, mark as content touch
|
|
40
|
+
* - If touch is OUTSIDE all ScrollViews → mark as fixed UI (no adjust)
|
|
41
|
+
*
|
|
42
|
+
* This makes heatmaps render fixed elements (nav bars, headers) at their
|
|
43
|
+
* viewport position, while still mapping scrollable content correctly.
|
|
44
|
+
*/
|
|
45
|
+
private scrollViewRegistry = new Map<
|
|
46
|
+
string,
|
|
47
|
+
{
|
|
48
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
49
|
+
scrollOffsetY: number;
|
|
50
|
+
}
|
|
51
|
+
>();
|
|
52
|
+
|
|
34
53
|
private constructor(config: SDKConfig) {
|
|
35
54
|
this.config = resolveConfig(config);
|
|
36
55
|
this.logger = new Logger(this.config.debug);
|
|
@@ -246,15 +265,45 @@ export class AppAnalytics {
|
|
|
246
265
|
return;
|
|
247
266
|
}
|
|
248
267
|
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
268
|
+
// Auto-detect: find which (if any) registered ScrollView contains
|
|
269
|
+
// this touch. If inside, adjust Y by that ScrollView's scroll offset.
|
|
270
|
+
// If outside all ScrollViews, treat as fixed UI (no adjustment).
|
|
271
|
+
let activeScrollOffset = 0;
|
|
272
|
+
let isFixed = true;
|
|
273
|
+
|
|
274
|
+
if (this.scrollViewRegistry.size > 0) {
|
|
275
|
+
for (const info of this.scrollViewRegistry.values()) {
|
|
276
|
+
const { bounds } = info;
|
|
277
|
+
if (
|
|
278
|
+
x >= bounds.x &&
|
|
279
|
+
x <= bounds.x + bounds.width &&
|
|
280
|
+
y >= bounds.y &&
|
|
281
|
+
y <= bounds.y + bounds.height
|
|
282
|
+
) {
|
|
283
|
+
activeScrollOffset = info.scrollOffsetY;
|
|
284
|
+
isFixed = false;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
// No ScrollViews registered → fall back to legacy behavior
|
|
290
|
+
// (use the manually-set scrollOffsetY, treat as content touch).
|
|
291
|
+
activeScrollOffset = this.scrollOffsetY;
|
|
292
|
+
isFixed = false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const adjustedY = y + activeScrollOffset;
|
|
253
296
|
|
|
254
297
|
const event: AnalyticsEvent = {
|
|
255
298
|
type: 'touch',
|
|
256
299
|
screenName: screenName || this.currentScreen || undefined,
|
|
257
|
-
data: {
|
|
300
|
+
data: {
|
|
301
|
+
x,
|
|
302
|
+
y: adjustedY,
|
|
303
|
+
scrollOffsetY: activeScrollOffset,
|
|
304
|
+
isFixed,
|
|
305
|
+
...extra,
|
|
306
|
+
},
|
|
258
307
|
timestamp: new Date().toISOString(),
|
|
259
308
|
};
|
|
260
309
|
|
|
@@ -316,11 +365,49 @@ export class AppAnalytics {
|
|
|
316
365
|
* or manually from the host app's ScrollView onScroll handler.
|
|
317
366
|
* The offset is added to touch Y coordinates for accurate heatmaps
|
|
318
367
|
* on scrollable screens.
|
|
368
|
+
*
|
|
369
|
+
* @deprecated Prefer registerScrollView + updateScrollViewOffset for
|
|
370
|
+
* auto-detection of fixed vs scrollable touches. Kept for backward compat.
|
|
319
371
|
*/
|
|
320
372
|
setScrollOffset(y: number): void {
|
|
321
373
|
this.scrollOffsetY = y;
|
|
322
374
|
}
|
|
323
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Register a ScrollView's window-relative bounds with the SDK.
|
|
378
|
+
* Called by RepliqoScrollView on layout. Used to auto-detect whether
|
|
379
|
+
* a touch is on scrollable content or on fixed UI (nav bars, headers).
|
|
380
|
+
*/
|
|
381
|
+
registerScrollView(
|
|
382
|
+
id: string,
|
|
383
|
+
bounds: { x: number; y: number; width: number; height: number },
|
|
384
|
+
): void {
|
|
385
|
+
const existing = this.scrollViewRegistry.get(id);
|
|
386
|
+
this.scrollViewRegistry.set(id, {
|
|
387
|
+
bounds,
|
|
388
|
+
scrollOffsetY: existing?.scrollOffsetY ?? 0,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Update the scroll offset for a previously-registered ScrollView.
|
|
394
|
+
* Called by RepliqoScrollView on every scroll event.
|
|
395
|
+
*/
|
|
396
|
+
updateScrollViewOffset(id: string, scrollOffsetY: number): void {
|
|
397
|
+
const existing = this.scrollViewRegistry.get(id);
|
|
398
|
+
if (existing) {
|
|
399
|
+
existing.scrollOffsetY = scrollOffsetY;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Remove a ScrollView from the registry. Called by RepliqoScrollView
|
|
405
|
+
* on unmount.
|
|
406
|
+
*/
|
|
407
|
+
unregisterScrollView(id: string): void {
|
|
408
|
+
this.scrollViewRegistry.delete(id);
|
|
409
|
+
}
|
|
410
|
+
|
|
324
411
|
/**
|
|
325
412
|
* Capture the current viewport as a tile for collaborative screen
|
|
326
413
|
* building. Called by RepliqoScrollView on scroll stops.
|
|
@@ -414,6 +501,8 @@ export class AppAnalytics {
|
|
|
414
501
|
this.currentScreen = screenName;
|
|
415
502
|
this.currentScreenEnteredAt = new Date().toISOString();
|
|
416
503
|
this.scrollOffsetY = 0;
|
|
504
|
+
// Clear the ScrollView registry — old screen's scrollviews are gone
|
|
505
|
+
this.scrollViewRegistry.clear();
|
|
417
506
|
this.errorTracker?.setCurrentScreen(screenName);
|
|
418
507
|
this.snapshotCapture?.setCurrentScreen(screenName);
|
|
419
508
|
// NOTE: autoScanScreen (PixelCopy / programmatic scroll) is DISABLED.
|