@repliqo/sdk-react-native 0.3.7 → 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 (48) hide show
  1. package/INTEGRATION_GUIDE.md +1384 -1312
  2. package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +242 -230
  3. package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -170
  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 -5
  11. package/dist/core/client.js +310 -48
  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 +3 -2
  17. package/dist/index.js +3 -2
  18. package/dist/snapshot/NativeScreenCapture.d.ts +6 -44
  19. package/dist/snapshot/NativeScreenCapture.js +6 -64
  20. package/dist/snapshot/capture.d.ts +7 -0
  21. package/dist/snapshot/capture.js +17 -5
  22. package/dist/trackers/error.tracker.d.ts +25 -0
  23. package/dist/trackers/error.tracker.js +114 -37
  24. package/dist/trackers/navigation.tracker.js +11 -0
  25. package/dist/transport/api.client.d.ts +34 -1
  26. package/dist/transport/api.client.js +102 -19
  27. package/dist/transport/batch-queue.d.ts +53 -1
  28. package/dist/transport/batch-queue.js +126 -19
  29. package/dist/types/events.d.ts +12 -0
  30. package/package.json +68 -64
  31. package/src/components/RepliqoFlatList.tsx +64 -0
  32. package/src/components/RepliqoScrollView.tsx +53 -302
  33. package/src/components/useRepliqoScrollTracking.ts +366 -0
  34. package/src/core/client.ts +953 -672
  35. package/src/core/config.ts +38 -30
  36. package/src/core/storage.ts +51 -0
  37. package/src/index.ts +54 -52
  38. package/src/snapshot/NativeScreenCapture.ts +62 -157
  39. package/src/snapshot/capture.ts +154 -142
  40. package/src/trackers/error.tracker.ts +211 -136
  41. package/src/trackers/navigation.tracker.ts +107 -98
  42. package/src/transport/api.client.ts +430 -327
  43. package/src/transport/batch-queue.ts +212 -95
  44. package/src/types/events.ts +72 -60
  45. package/android/src/main/java/com/repliqo/screencapture/FullContentCapture.java +0 -273
  46. package/android/src/main/java/com/repliqo/screencapture/PixelCopyScan.java +0 -279
  47. package/android/src/main/java/com/repliqo/screencapture/ScrollScanCapture.java +0 -248
  48. package/src/snapshot/ScreenCaptureManager.ts +0 -156
@@ -1,273 +0,0 @@
1
- package com.repliqo.screencapture;
2
-
3
- import android.app.Activity;
4
- import android.graphics.Bitmap;
5
- import android.graphics.Canvas;
6
- import android.util.Base64;
7
- import android.util.Log;
8
- import android.view.View;
9
- import android.view.ViewGroup;
10
- import android.widget.ScrollView;
11
-
12
- import java.io.ByteArrayOutputStream;
13
- import java.lang.reflect.Field;
14
- import java.util.ArrayList;
15
-
16
- /**
17
- * Captures the FULL content of the primary vertical ScrollView found in
18
- * the app's view hierarchy — including content scrolled off-screen.
19
- *
20
- * Used for heatmap backgrounds where we need the complete screen layout,
21
- * not just the visible viewport.
22
- *
23
- * Supports:
24
- * - Android ScrollView
25
- * - AndroidX NestedScrollView
26
- * - React Native ReactScrollView (old arch)
27
- * - React Native ReactScrollViewManager (Fabric / new arch)
28
- *
29
- * Known limitation: FlatList / SectionList use RecyclerView which only
30
- * renders visible items. Full-content capture is not possible for those.
31
- * The method falls back to a viewport capture in that case.
32
- *
33
- * No permissions needed. Runs on UI thread.
34
- */
35
- public class FullContentCapture {
36
- private static final String TAG = "RepliqoCapture";
37
- private static final int TARGET_WIDTH = 500;
38
- private static final int JPEG_QUALITY = 60;
39
- private static final int MAX_HEIGHT = 5000;
40
-
41
- /** Result that includes whether this is a full-content or viewport capture. */
42
- public static class FullCaptureResult {
43
- public final String image;
44
- public final int width;
45
- public final int height;
46
- public final boolean isFullContent;
47
-
48
- FullCaptureResult(String image, int width, int height, boolean isFullContent) {
49
- this.image = image;
50
- this.width = width;
51
- this.height = height;
52
- this.isFullContent = isFullContent;
53
- }
54
- }
55
-
56
- /**
57
- * Capture the full content of the primary vertical ScrollView.
58
- * Falls back to viewport capture if no suitable ScrollView is found.
59
- *
60
- * @param activity Current activity
61
- * @return FullCaptureResult with isFullContent flag
62
- */
63
- public static FullCaptureResult capture(Activity activity) {
64
- if (activity == null || activity.isFinishing()) return null;
65
-
66
- try {
67
- // Find the primary vertical ScrollView
68
- View scrollView = findVerticalScrollView(activity);
69
- if (scrollView == null) {
70
- Log.d(TAG, "No vertical ScrollView found, falling back to viewport");
71
- return viewportFallback(activity);
72
- }
73
-
74
- // Get the content child
75
- View contentView = getContentChild(scrollView);
76
- if (contentView == null) {
77
- return viewportFallback(activity);
78
- }
79
-
80
- int contentWidth = contentView.getWidth();
81
- int contentHeight = contentView.getHeight();
82
-
83
- if (contentWidth <= 0 || contentHeight <= 0) {
84
- return viewportFallback(activity);
85
- }
86
-
87
- // If content height is same as viewport, it's not scrollable
88
- int viewportHeight = scrollView.getHeight();
89
- if (contentHeight <= viewportHeight) {
90
- Log.d(TAG, "Content fits in viewport, no scroll needed");
91
- return viewportFallback(activity);
92
- }
93
-
94
- // Cap height to avoid OOM
95
- int captureHeight = Math.min(contentHeight, MAX_HEIGHT);
96
-
97
- // Scale to output dimensions
98
- float scale = (float) TARGET_WIDTH / contentWidth;
99
- int outWidth = TARGET_WIDTH & ~1;
100
- int outHeight = Math.round(captureHeight * scale) & ~1;
101
-
102
- if (outWidth <= 0 || outHeight <= 0) {
103
- return viewportFallback(activity);
104
- }
105
-
106
- // Force the content view to measure and layout at its full
107
- // height. React Native's ScrollView optimization may skip
108
- // laying out off-screen children, causing draw() to only
109
- // render the visible portion. This forces a full layout pass.
110
- int widthSpec = View.MeasureSpec.makeMeasureSpec(
111
- contentWidth, View.MeasureSpec.EXACTLY);
112
- int heightSpec = View.MeasureSpec.makeMeasureSpec(
113
- captureHeight, View.MeasureSpec.EXACTLY);
114
- contentView.measure(widthSpec, heightSpec);
115
- contentView.layout(0, 0, contentWidth, captureHeight);
116
-
117
- // Create bitmap — wrapped in OOM catch
118
- Bitmap bitmap;
119
- try {
120
- bitmap = Bitmap.createBitmap(outWidth, outHeight,
121
- Bitmap.Config.ARGB_8888);
122
- } catch (OutOfMemoryError oom) {
123
- Log.w(TAG, "OOM creating bitmap " + outWidth + "x" + outHeight +
124
- ", falling back to viewport");
125
- return viewportFallback(activity);
126
- }
127
-
128
- Canvas canvas = new Canvas(bitmap);
129
- canvas.scale(scale, scale);
130
-
131
- // Draw the full content
132
- contentView.draw(canvas);
133
-
134
- // Restore the original layout so the UI isn't affected.
135
- // requestLayout triggers an async re-layout on the next frame.
136
- contentView.requestLayout();
137
-
138
- // Compress to JPEG
139
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
140
- bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, baos);
141
- String base64 = Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP);
142
- bitmap.recycle();
143
-
144
- Log.d(TAG, "Full content captured: " + outWidth + "x" + outHeight +
145
- " (content: " + contentWidth + "x" + contentHeight +
146
- ", viewport: " + contentWidth + "x" + viewportHeight + ")");
147
-
148
- return new FullCaptureResult(base64, outWidth, outHeight, true);
149
-
150
- } catch (Exception e) {
151
- Log.w(TAG, "FullContentCapture failed: " + e.getMessage());
152
- return viewportFallback(activity);
153
- }
154
- }
155
-
156
- /**
157
- * Fallback: capture just the visible viewport via MultiWindowCapture.
158
- */
159
- private static FullCaptureResult viewportFallback(Activity activity) {
160
- MultiWindowCapture.CaptureResult vp = MultiWindowCapture.capture(activity);
161
- if (vp == null) return null;
162
- return new FullCaptureResult(vp.image, vp.width, vp.height, false);
163
- }
164
-
165
- /**
166
- * Find the first VERTICAL ScrollView in the view hierarchy.
167
- * Searches all root views (including modals).
168
- *
169
- * Matches by walking the class hierarchy for known scroll classes:
170
- * - android.widget.ScrollView
171
- * - androidx.core.widget.NestedScrollView
172
- * - com.facebook.react.views.scroll.ReactScrollView (old arch)
173
- * - Any class with "ScrollView" in the name (catches Fabric variants)
174
- *
175
- * Explicitly SKIPS HorizontalScrollView and classes containing
176
- * "Horizontal" to avoid capturing carousels.
177
- */
178
- private static View findVerticalScrollView(Activity activity) {
179
- View decorView = activity.getWindow().getDecorView();
180
- View found = findVerticalScrollInTree(decorView);
181
- if (found != null) return found;
182
-
183
- // Check other windows (modals)
184
- try {
185
- Class<?> wmgClass = Class.forName("android.view.WindowManagerGlobal");
186
- Object wmg = wmgClass.getMethod("getInstance").invoke(null);
187
- Field viewsField = wmgClass.getDeclaredField("mViews");
188
- viewsField.setAccessible(true);
189
- @SuppressWarnings("unchecked")
190
- ArrayList<View> views = (ArrayList<View>) viewsField.get(wmg);
191
- if (views != null) {
192
- for (View root : views) {
193
- if (root == decorView) continue;
194
- found = findVerticalScrollInTree(root);
195
- if (found != null) return found;
196
- }
197
- }
198
- } catch (Exception e) {
199
- // Reflection failed
200
- }
201
-
202
- return null;
203
- }
204
-
205
- /**
206
- * DFS for a vertical scroll view in the tree.
207
- */
208
- private static View findVerticalScrollInTree(View view) {
209
- if (view == null) return null;
210
-
211
- if (isVerticalScrollView(view)) {
212
- if (view instanceof ViewGroup && ((ViewGroup) view).getChildCount() > 0) {
213
- return view;
214
- }
215
- }
216
-
217
- if (view instanceof ViewGroup) {
218
- ViewGroup group = (ViewGroup) view;
219
- for (int i = 0; i < group.getChildCount(); i++) {
220
- View found = findVerticalScrollInTree(group.getChildAt(i));
221
- if (found != null) return found;
222
- }
223
- }
224
-
225
- return null;
226
- }
227
-
228
- /**
229
- * Check if a view is a vertical scroll view.
230
- * Walks the class hierarchy to catch subclasses.
231
- */
232
- private static boolean isVerticalScrollView(View view) {
233
- // Direct instanceof for standard Android ScrollView
234
- if (view instanceof ScrollView) return true;
235
-
236
- // Walk class hierarchy for name-based matching
237
- Class<?> clazz = view.getClass();
238
- while (clazz != null && clazz != View.class) {
239
- String name = clazz.getName();
240
-
241
- // Skip horizontal variants
242
- if (name.contains("Horizontal")) return false;
243
-
244
- // Match known vertical scroll classes:
245
- // - androidx.core.widget.NestedScrollView
246
- // - com.facebook.react.views.scroll.ReactScrollView (old arch)
247
- // - com.facebook.react.views.scroll.ReactScrollViewManager$... (Fabric)
248
- // - Any other class ending in "ScrollView" that isn't horizontal
249
- if (name.contains("NestedScrollView") ||
250
- name.contains("ReactScrollView") ||
251
- (name.endsWith("ScrollView") && !name.contains("Horizontal"))) {
252
- return true;
253
- }
254
-
255
- clazz = clazz.getSuperclass();
256
- }
257
-
258
- return false;
259
- }
260
-
261
- /**
262
- * Get the content child of a ScrollView.
263
- */
264
- private static View getContentChild(View scrollView) {
265
- if (scrollView instanceof ViewGroup) {
266
- ViewGroup group = (ViewGroup) scrollView;
267
- if (group.getChildCount() > 0) {
268
- return group.getChildAt(0);
269
- }
270
- }
271
- return null;
272
- }
273
- }
@@ -1,279 +0,0 @@
1
- package com.repliqo.screencapture;
2
-
3
- import android.app.Activity;
4
- import android.graphics.Bitmap;
5
- import android.graphics.Rect;
6
- import android.os.Build;
7
- import android.os.Handler;
8
- import android.os.HandlerThread;
9
- import android.util.Base64;
10
- import android.util.Log;
11
- import android.view.PixelCopy;
12
- import android.view.View;
13
- import android.view.ViewGroup;
14
- import android.view.Window;
15
- import android.widget.ScrollView;
16
-
17
- import com.facebook.react.bridge.Arguments;
18
- import com.facebook.react.bridge.WritableArray;
19
- import com.facebook.react.bridge.WritableMap;
20
-
21
- import java.io.ByteArrayOutputStream;
22
- import java.lang.reflect.Field;
23
- import java.util.ArrayList;
24
- import java.util.concurrent.CountDownLatch;
25
- import java.util.concurrent.TimeUnit;
26
-
27
- /**
28
- * Captures the FULL content of a ScrollView using PixelCopy API +
29
- * programmatic scrolling.
30
- *
31
- * Unlike View.draw() which only renders what the view tree has laid out,
32
- * PixelCopy captures directly from the GPU's Surface — it sees exactly
33
- * what's on screen, including content that React Native has rendered
34
- * but that View.draw() misses.
35
- *
36
- * Flow:
37
- * 1. Find the main ScrollView
38
- * 2. Save scroll position
39
- * 3. For each viewport-sized step:
40
- * a. scrollTo(y) — instant
41
- * b. Wait 2 frames for RN to render
42
- * c. PixelCopy the Window surface (captures what's actually visible)
43
- * d. Crop to the ScrollView's bounds on screen
44
- * 4. Restore scroll position
45
- * 5. Return all tiles
46
- *
47
- * Requires API 26+ (Android 8.0). Falls back to MultiWindowCapture on older.
48
- */
49
- public class PixelCopyScan {
50
- private static final String TAG = "RepliqoCapture";
51
- private static final int TARGET_WIDTH = 390;
52
- private static final int JPEG_QUALITY = 45;
53
- private static final int FRAME_WAIT_MS = 50; // Wait for RN to render
54
-
55
- /**
56
- * Scan the ScrollView content using PixelCopy + programmatic scroll.
57
- * Must be called on the UI thread. Blocks for ~100ms per tile.
58
- *
59
- * @return WritableArray of tiles, or null on failure
60
- */
61
- public static WritableArray scan(Activity activity) {
62
- if (activity == null || activity.isFinishing()) return null;
63
-
64
- // PixelCopy requires API 26+
65
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
66
- Log.d(TAG, "PixelCopy not available (API < 26), using fallback");
67
- return null;
68
- }
69
-
70
- try {
71
- View scrollView = findVerticalScrollView(activity);
72
- if (scrollView == null) {
73
- Log.d(TAG, "No ScrollView found for PixelCopy scan");
74
- return null;
75
- }
76
-
77
- View contentView = getContentChild(scrollView);
78
- if (contentView == null) return null;
79
-
80
- int viewportHeight = scrollView.getHeight();
81
- int contentHeight = contentView.getHeight();
82
- int viewportWidth = scrollView.getWidth();
83
-
84
- if (viewportWidth <= 0 || viewportHeight <= 0 || contentHeight <= 0) {
85
- return null;
86
- }
87
-
88
- // Get ScrollView position on screen (for cropping)
89
- int[] scrollViewLoc = new int[2];
90
- scrollView.getLocationOnScreen(scrollViewLoc);
91
- Rect scrollViewBounds = new Rect(
92
- scrollViewLoc[0], scrollViewLoc[1],
93
- scrollViewLoc[0] + viewportWidth,
94
- scrollViewLoc[1] + viewportHeight
95
- );
96
-
97
- Window window = activity.getWindow();
98
- int originalScrollY = scrollView.getScrollY();
99
-
100
- WritableArray tiles = Arguments.createArray();
101
- int step = (int) (viewportHeight * 0.75); // 25% overlap
102
-
103
- // Create a handler thread for PixelCopy callbacks
104
- HandlerThread handlerThread = new HandlerThread("PixelCopyThread");
105
- handlerThread.start();
106
- Handler handler = new Handler(handlerThread.getLooper());
107
-
108
- try {
109
- int y = 0;
110
- while (y < contentHeight) {
111
- int scrollTarget = Math.min(y, contentHeight - viewportHeight);
112
- if (scrollTarget < 0) scrollTarget = 0;
113
-
114
- // Scroll to position
115
- scrollView.scrollTo(0, scrollTarget);
116
-
117
- // Wait for React Native to render the new content
118
- Thread.sleep(FRAME_WAIT_MS);
119
-
120
- // Capture using PixelCopy
121
- WritableMap tile = captureWithPixelCopy(
122
- window, scrollViewBounds, handler,
123
- scrollTarget, viewportWidth, viewportHeight, contentHeight
124
- );
125
-
126
- if (tile != null) {
127
- tiles.pushMap(tile);
128
- }
129
-
130
- if (y >= contentHeight - viewportHeight) break;
131
- y += step;
132
- if (y > contentHeight - viewportHeight) {
133
- y = contentHeight - viewportHeight;
134
- }
135
- }
136
- } finally {
137
- // Restore scroll position
138
- scrollView.scrollTo(0, originalScrollY);
139
- handlerThread.quitSafely();
140
- }
141
-
142
- Log.d(TAG, "PixelCopy scan: " + tiles.size() + " tiles, " +
143
- "content=" + contentHeight + "px");
144
-
145
- return tiles;
146
-
147
- } catch (Exception e) {
148
- Log.w(TAG, "PixelCopyScan failed: " + e.getMessage());
149
- return null;
150
- }
151
- }
152
-
153
- /**
154
- * Capture a tile using PixelCopy, cropping to the ScrollView's bounds.
155
- */
156
- private static WritableMap captureWithPixelCopy(
157
- Window window, Rect cropBounds, Handler handler,
158
- int scrollY, int viewportWidth, int viewportHeight, int contentHeight) {
159
-
160
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
161
-
162
- try {
163
- // Create bitmap for the crop area
164
- Bitmap bitmap = Bitmap.createBitmap(
165
- cropBounds.width(), cropBounds.height(),
166
- Bitmap.Config.ARGB_8888
167
- );
168
-
169
- CountDownLatch latch = new CountDownLatch(1);
170
- final int[] resultCode = new int[]{-1};
171
-
172
- PixelCopy.request(window, cropBounds, bitmap, (result) -> {
173
- resultCode[0] = result;
174
- latch.countDown();
175
- }, handler);
176
-
177
- // Wait for PixelCopy to complete (max 1 second)
178
- if (!latch.await(1, TimeUnit.SECONDS)) {
179
- bitmap.recycle();
180
- return null;
181
- }
182
-
183
- if (resultCode[0] != PixelCopy.SUCCESS) {
184
- bitmap.recycle();
185
- return null;
186
- }
187
-
188
- // Scale to target width
189
- float scale = (float) TARGET_WIDTH / viewportWidth;
190
- int outWidth = TARGET_WIDTH;
191
- int outHeight = Math.round(viewportHeight * scale);
192
-
193
- Bitmap scaled = Bitmap.createScaledBitmap(bitmap, outWidth, outHeight, true);
194
- bitmap.recycle();
195
-
196
- // Compress to JPEG
197
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
198
- scaled.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, baos);
199
- String base64 = Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP);
200
- scaled.recycle();
201
-
202
- WritableMap tile = Arguments.createMap();
203
- tile.putString("image", base64);
204
- tile.putInt("width", outWidth);
205
- tile.putInt("height", outHeight);
206
- tile.putInt("scrollOffsetY", scrollY);
207
- tile.putInt("viewportHeight", viewportHeight);
208
- tile.putInt("contentHeight", contentHeight);
209
- tile.putString("format", "jpeg");
210
- return tile;
211
-
212
- } catch (Exception e) {
213
- Log.w(TAG, "PixelCopy at Y=" + scrollY + " failed: " + e.getMessage());
214
- return null;
215
- }
216
- }
217
-
218
- // --- View finders (same as ScrollScanCapture) ---
219
-
220
- @SuppressWarnings("unchecked")
221
- private static View findVerticalScrollView(Activity activity) {
222
- View decorView = activity.getWindow().getDecorView();
223
- View found = findInTree(decorView);
224
- if (found != null) return found;
225
- try {
226
- Class<?> wmgClass = Class.forName("android.view.WindowManagerGlobal");
227
- Object wmg = wmgClass.getMethod("getInstance").invoke(null);
228
- Field viewsField = wmgClass.getDeclaredField("mViews");
229
- viewsField.setAccessible(true);
230
- ArrayList<View> views = (ArrayList<View>) viewsField.get(wmg);
231
- if (views != null) {
232
- for (View root : views) {
233
- if (root == decorView) continue;
234
- found = findInTree(root);
235
- if (found != null) return found;
236
- }
237
- }
238
- } catch (Exception e) { }
239
- return null;
240
- }
241
-
242
- private static View findInTree(View view) {
243
- if (view == null) return null;
244
- if (isVerticalScroll(view) && view instanceof ViewGroup
245
- && ((ViewGroup) view).getChildCount() > 0) {
246
- return view;
247
- }
248
- if (view instanceof ViewGroup) {
249
- ViewGroup group = (ViewGroup) view;
250
- for (int i = 0; i < group.getChildCount(); i++) {
251
- View found = findInTree(group.getChildAt(i));
252
- if (found != null) return found;
253
- }
254
- }
255
- return null;
256
- }
257
-
258
- private static boolean isVerticalScroll(View view) {
259
- if (view instanceof ScrollView) return true;
260
- Class<?> clazz = view.getClass();
261
- while (clazz != null && clazz != View.class) {
262
- String name = clazz.getName();
263
- if (name.contains("Horizontal")) return false;
264
- if (name.contains("NestedScrollView") ||
265
- name.contains("ReactScrollView") ||
266
- name.endsWith("ScrollView")) return true;
267
- clazz = clazz.getSuperclass();
268
- }
269
- return false;
270
- }
271
-
272
- private static View getContentChild(View scrollView) {
273
- if (scrollView instanceof ViewGroup) {
274
- ViewGroup group = (ViewGroup) scrollView;
275
- if (group.getChildCount() > 0) return group.getChildAt(0);
276
- }
277
- return null;
278
- }
279
- }