@repliqo/sdk-react-native 0.1.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 (58) hide show
  1. package/INTEGRATION_GUIDE.md +1312 -0
  2. package/android/build.gradle +24 -0
  3. package/android/src/main/AndroidManifest.xml +4 -0
  4. package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +230 -0
  5. package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -0
  6. package/android/src/main/java/com/repliqo/screencapture/ScreenCapturePackage.java +38 -0
  7. package/dist/components/ScreenCapturePermission.d.ts +55 -0
  8. package/dist/components/ScreenCapturePermission.js +112 -0
  9. package/dist/core/client.d.ts +41 -0
  10. package/dist/core/client.js +309 -0
  11. package/dist/core/config.d.ts +4 -0
  12. package/dist/core/config.js +26 -0
  13. package/dist/core/logger.d.ts +8 -0
  14. package/dist/core/logger.js +25 -0
  15. package/dist/index.d.ts +15 -0
  16. package/dist/index.js +28 -0
  17. package/dist/snapshot/NativeScreenCapture.d.ts +17 -0
  18. package/dist/snapshot/NativeScreenCapture.js +38 -0
  19. package/dist/snapshot/capture.d.ts +36 -0
  20. package/dist/snapshot/capture.js +109 -0
  21. package/dist/snapshot/captureScreenshot.d.ts +26 -0
  22. package/dist/snapshot/captureScreenshot.js +89 -0
  23. package/dist/trackers/error.tracker.d.ts +22 -0
  24. package/dist/trackers/error.tracker.js +123 -0
  25. package/dist/trackers/navigation.tracker.d.ts +6 -0
  26. package/dist/trackers/navigation.tracker.js +78 -0
  27. package/dist/trackers/screen.tracker.d.ts +2 -0
  28. package/dist/trackers/screen.tracker.js +23 -0
  29. package/dist/trackers/touch.tracker.d.ts +8 -0
  30. package/dist/trackers/touch.tracker.js +30 -0
  31. package/dist/transport/api.client.d.ts +29 -0
  32. package/dist/transport/api.client.js +156 -0
  33. package/dist/transport/batch-queue.d.ts +18 -0
  34. package/dist/transport/batch-queue.js +80 -0
  35. package/dist/types/crash.d.ts +10 -0
  36. package/dist/types/crash.js +2 -0
  37. package/dist/types/events.d.ts +53 -0
  38. package/dist/types/events.js +2 -0
  39. package/dist/types/snapshot.d.ts +21 -0
  40. package/dist/types/snapshot.js +9 -0
  41. package/package.json +64 -0
  42. package/src/components/ScreenCapturePermission.tsx +160 -0
  43. package/src/core/client.ts +425 -0
  44. package/src/core/config.ts +27 -0
  45. package/src/core/logger.ts +27 -0
  46. package/src/index.ts +47 -0
  47. package/src/snapshot/NativeScreenCapture.ts +54 -0
  48. package/src/snapshot/capture.ts +142 -0
  49. package/src/snapshot/captureScreenshot.ts +109 -0
  50. package/src/trackers/error.tracker.ts +136 -0
  51. package/src/trackers/navigation.tracker.ts +98 -0
  52. package/src/trackers/screen.tracker.ts +19 -0
  53. package/src/trackers/touch.tracker.tsx +44 -0
  54. package/src/transport/api.client.ts +225 -0
  55. package/src/transport/batch-queue.ts +95 -0
  56. package/src/types/crash.ts +10 -0
  57. package/src/types/events.ts +59 -0
  58. package/src/types/snapshot.ts +23 -0
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AppAnalytics = void 0;
4
+ const react_native_1 = require("react-native");
5
+ const capture_1 = require("../snapshot/capture");
6
+ const config_1 = require("./config");
7
+ const logger_1 = require("./logger");
8
+ const api_client_1 = require("../transport/api.client");
9
+ const batch_queue_1 = require("../transport/batch-queue");
10
+ const error_tracker_1 = require("../trackers/error.tracker");
11
+ // Native multi-window capture is used automatically via captureScreenshot()
12
+ // when the ScreenCaptureModule is linked. No permission or service needed.
13
+ class AppAnalytics {
14
+ constructor(config) {
15
+ this.snapshotCapture = null;
16
+ this.errorTracker = null;
17
+ this.sessionId = null;
18
+ this.currentScreen = null;
19
+ this.currentScreenEnteredAt = null;
20
+ this.appStateSubscription = null;
21
+ this.config = (0, config_1.resolveConfig)(config);
22
+ this.logger = new logger_1.Logger(this.config.debug);
23
+ this.apiClient = new api_client_1.ApiClient(this.config.baseUrl, this.config.apiKey, this.logger);
24
+ this.eventQueue = new batch_queue_1.BatchQueue(this.config.batchSize, this.config.flushInterval, async (events) => {
25
+ if (!this.sessionId) {
26
+ this.logger.warn('Cannot flush events: no active session');
27
+ throw new Error('No active session');
28
+ }
29
+ await this.apiClient.sendEventsBatch(this.sessionId, events);
30
+ });
31
+ this.screenVisitQueue = new batch_queue_1.BatchQueue(this.config.batchSize, this.config.flushInterval, async (visits) => {
32
+ await this.apiClient.sendScreenVisitsBatch(visits);
33
+ });
34
+ // Snapshots are heavier (~15-30 KB JPEG each) so they use a smaller
35
+ // batch size, shorter flush interval, and a tighter memory cap than
36
+ // the event queue. All configurable via SDKConfig.
37
+ this.snapshotQueue = new batch_queue_1.BatchQueue(this.config.snapshotBatchSize, this.config.snapshotFlushInterval, async (snapshots) => {
38
+ await this.apiClient.sendSnapshotsBatch(snapshots);
39
+ }, this.config.snapshotMaxBufferSize);
40
+ this.eventQueue.startAutoFlush();
41
+ this.screenVisitQueue.startAutoFlush();
42
+ this.snapshotQueue.startAutoFlush();
43
+ if (this.config.enableSnapshots) {
44
+ this.snapshotCapture = new capture_1.SnapshotCapture({
45
+ captureInterval: this.config.snapshotInterval,
46
+ maxSnapshotsPerSession: this.config.maxSnapshotsPerSession,
47
+ }, (snapshot) => {
48
+ this.snapshotQueue.add(snapshot);
49
+ }, undefined, // default captureScreenshot provider
50
+ (...args) => this.logger.log(...args), (...args) => this.logger.warn(...args));
51
+ }
52
+ if (this.config.enableCrashTracking) {
53
+ this.errorTracker = new error_tracker_1.ErrorTracker((crash) => {
54
+ // Crash reports are sent immediately (fire-and-forget), not batched
55
+ this.apiClient.sendCrashReport(crash).catch(() => { });
56
+ });
57
+ this.errorTracker.start();
58
+ }
59
+ this.setupAppStateListener();
60
+ this.logger.log('SDK initialized with config:', {
61
+ baseUrl: this.config.baseUrl,
62
+ appId: this.config.appId,
63
+ batchSize: this.config.batchSize,
64
+ flushInterval: this.config.flushInterval,
65
+ enableSnapshots: this.config.enableSnapshots,
66
+ snapshotInterval: this.config.snapshotInterval,
67
+ enableCrashTracking: this.config.enableCrashTracking,
68
+ });
69
+ }
70
+ static init(config) {
71
+ if (AppAnalytics.instance) {
72
+ AppAnalytics.instance.logger.warn('AppAnalytics already initialized. Returning existing instance.');
73
+ return AppAnalytics.instance;
74
+ }
75
+ AppAnalytics.instance = new AppAnalytics(config);
76
+ return AppAnalytics.instance;
77
+ }
78
+ static getInstance() {
79
+ if (!AppAnalytics.instance) {
80
+ throw new Error('AppAnalytics not initialized. Call AppAnalytics.init(config) first.');
81
+ }
82
+ return AppAnalytics.instance;
83
+ }
84
+ static destroy() {
85
+ if (AppAnalytics.instance) {
86
+ const instance = AppAnalytics.instance;
87
+ instance.logger.log('Destroying AppAnalytics instance');
88
+ if (instance.errorTracker) {
89
+ instance.errorTracker.stop();
90
+ instance.errorTracker = null;
91
+ }
92
+ if (instance.snapshotCapture) {
93
+ instance.snapshotCapture.stop();
94
+ instance.snapshotCapture = null;
95
+ }
96
+ instance.eventQueue.stopAutoFlush();
97
+ instance.screenVisitQueue.stopAutoFlush();
98
+ instance.snapshotQueue.stopAutoFlush();
99
+ if (instance.appStateSubscription) {
100
+ instance.appStateSubscription.remove();
101
+ instance.appStateSubscription = null;
102
+ }
103
+ // Attempt a final flush (fire-and-forget)
104
+ instance.flush().catch(() => { });
105
+ AppAnalytics.instance = null;
106
+ }
107
+ }
108
+ async startSession(deviceId, deviceInfo) {
109
+ try {
110
+ const response = await this.apiClient.startSession(deviceId, deviceInfo);
111
+ this.sessionId = response.id;
112
+ this.logger.log('Session started with ID:', this.sessionId);
113
+ this.errorTracker?.setSessionId(this.sessionId);
114
+ if (this.snapshotCapture) {
115
+ this.snapshotCapture.setSessionId(this.sessionId);
116
+ this.snapshotCapture.reset();
117
+ // Native capture (MediaProjection) is NOT started automatically.
118
+ // It requires a system dialog that is confusing if shown on every
119
+ // launch. The host app can call enableNativeCapture() explicitly
120
+ // when the user opts in. Until then, view-shot is used.
121
+ this.startSnapshotCapture();
122
+ }
123
+ return this.sessionId;
124
+ }
125
+ catch (error) {
126
+ this.logger.error('Failed to start session:', error);
127
+ throw error;
128
+ }
129
+ }
130
+ async endSession() {
131
+ if (!this.sessionId) {
132
+ this.logger.warn('No active session to end');
133
+ return;
134
+ }
135
+ // Stop snapshot capture
136
+ this.stopSnapshotCapture();
137
+ // Flush any pending screen exit
138
+ if (this.currentScreen && this.currentScreenEnteredAt) {
139
+ this.onScreenExit(this.currentScreen);
140
+ }
141
+ // Flush remaining data
142
+ await this.flush();
143
+ try {
144
+ await this.apiClient.endSession(this.sessionId);
145
+ this.logger.log('Session ended:', this.sessionId);
146
+ }
147
+ catch (error) {
148
+ this.logger.error('Failed to end session:', error);
149
+ }
150
+ this.sessionId = null;
151
+ this.currentScreen = null;
152
+ this.currentScreenEnteredAt = null;
153
+ this.errorTracker?.setSessionId(null);
154
+ if (this.snapshotCapture) {
155
+ this.snapshotCapture.setSessionId(null);
156
+ }
157
+ }
158
+ trackTouch(x, y, screenName, extra) {
159
+ if (!this.sessionId) {
160
+ this.logger.warn('Cannot track touch: no active session');
161
+ return;
162
+ }
163
+ if (!this.config.enableTouchTracking) {
164
+ return;
165
+ }
166
+ const event = {
167
+ type: 'touch',
168
+ screenName: screenName || this.currentScreen || undefined,
169
+ data: { x, y, ...extra },
170
+ timestamp: new Date().toISOString(),
171
+ };
172
+ this.eventQueue.add(event);
173
+ this.logger.log('Touch tracked:', event);
174
+ }
175
+ trackNavigation(fromScreen, toScreen) {
176
+ if (!this.sessionId) {
177
+ this.logger.warn('Cannot track navigation: no active session');
178
+ return;
179
+ }
180
+ if (!this.config.enableNavigationTracking) {
181
+ return;
182
+ }
183
+ const event = {
184
+ type: 'navigation',
185
+ screenName: toScreen,
186
+ data: { fromScreen, toScreen },
187
+ timestamp: new Date().toISOString(),
188
+ };
189
+ this.eventQueue.add(event);
190
+ this.logger.log('Navigation tracked:', fromScreen, '->', toScreen);
191
+ }
192
+ trackCustomEvent(eventName, data) {
193
+ if (!this.sessionId) {
194
+ this.logger.warn('Cannot track custom event: no active session');
195
+ return;
196
+ }
197
+ const event = {
198
+ type: 'custom',
199
+ screenName: this.currentScreen || undefined,
200
+ data: { eventName, ...data },
201
+ timestamp: new Date().toISOString(),
202
+ };
203
+ this.eventQueue.add(event);
204
+ this.logger.log('Custom event tracked:', eventName);
205
+ }
206
+ reportError(error, metadata) {
207
+ if (!this.errorTracker) {
208
+ this.logger.warn('Cannot report error: crash tracking is not enabled');
209
+ return;
210
+ }
211
+ this.errorTracker.reportError(error, metadata);
212
+ }
213
+ onScreenEnter(screenName) {
214
+ this.currentScreen = screenName;
215
+ this.currentScreenEnteredAt = new Date().toISOString();
216
+ this.errorTracker?.setCurrentScreen(screenName);
217
+ this.snapshotCapture?.setCurrentScreen(screenName);
218
+ this.logger.log('Screen entered:', screenName);
219
+ }
220
+ onScreenExit(screenName) {
221
+ if (this.sessionId &&
222
+ this.currentScreen === screenName &&
223
+ this.currentScreenEnteredAt) {
224
+ const enteredAt = this.currentScreenEnteredAt;
225
+ const exitedAt = new Date().toISOString();
226
+ const duration = new Date(exitedAt).getTime() - new Date(enteredAt).getTime();
227
+ const visit = {
228
+ sessionId: this.sessionId,
229
+ screenName,
230
+ enteredAt,
231
+ exitedAt,
232
+ duration,
233
+ };
234
+ this.screenVisitQueue.add(visit);
235
+ this.logger.log('Screen exited:', screenName, 'duration:', duration);
236
+ }
237
+ if (this.currentScreen === screenName) {
238
+ this.currentScreen = null;
239
+ this.currentScreenEnteredAt = null;
240
+ this.errorTracker?.setCurrentScreen(null);
241
+ }
242
+ }
243
+ async flush() {
244
+ this.logger.log('Flushing queues...');
245
+ await Promise.all([
246
+ this.eventQueue.flush(),
247
+ this.screenVisitQueue.flush(),
248
+ this.snapshotQueue.flush(),
249
+ ]);
250
+ this.logger.log('Flush complete');
251
+ }
252
+ getSessionId() {
253
+ return this.sessionId;
254
+ }
255
+ getCurrentScreen() {
256
+ return this.currentScreen;
257
+ }
258
+ /**
259
+ * Force an immediate screenshot capture, outside the periodic schedule.
260
+ * Useful for capturing specific moments (e.g. right after a critical
261
+ * user action). Respects the in-flight lock and session cap.
262
+ */
263
+ async captureSnapshot(screenName) {
264
+ if (!this.sessionId || !this.snapshotCapture) {
265
+ return;
266
+ }
267
+ if (screenName) {
268
+ this.snapshotCapture.setCurrentScreen(screenName);
269
+ }
270
+ await this.snapshotCapture.captureNow();
271
+ }
272
+ startSnapshotCapture() {
273
+ if (this.snapshotCapture) {
274
+ this.snapshotCapture.start();
275
+ this.logger.log('Snapshot capture started');
276
+ }
277
+ }
278
+ stopSnapshotCapture() {
279
+ if (this.snapshotCapture) {
280
+ this.snapshotCapture.stop();
281
+ this.logger.log('Snapshot capture stopped');
282
+ }
283
+ }
284
+ isInitialized() {
285
+ return true;
286
+ }
287
+ setupAppStateListener() {
288
+ let wasInBackground = false;
289
+ this.appStateSubscription = react_native_1.AppState.addEventListener('change', (nextAppState) => {
290
+ if (nextAppState === 'background' || nextAppState === 'inactive') {
291
+ this.logger.log('App going to background, pausing snapshots + flushing');
292
+ this.stopSnapshotCapture();
293
+ wasInBackground = true;
294
+ this.flush().catch((error) => {
295
+ this.logger.error('Error flushing on app state change:', error);
296
+ });
297
+ }
298
+ else if (nextAppState === 'active' && wasInBackground) {
299
+ wasInBackground = false;
300
+ this.logger.log('App returned to foreground, resuming snapshots');
301
+ if (this.snapshotCapture && this.sessionId) {
302
+ this.startSnapshotCapture();
303
+ }
304
+ }
305
+ });
306
+ }
307
+ }
308
+ exports.AppAnalytics = AppAnalytics;
309
+ AppAnalytics.instance = null;
@@ -0,0 +1,4 @@
1
+ import { SDKConfig } from '../types/events';
2
+ export declare const DEFAULT_CONFIG: Required<Omit<SDKConfig, 'apiKey' | 'appId' | 'baseUrl'>>;
3
+ export type ResolvedConfig = SDKConfig & typeof DEFAULT_CONFIG;
4
+ export declare function resolveConfig(config: SDKConfig): ResolvedConfig;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_CONFIG = void 0;
4
+ exports.resolveConfig = resolveConfig;
5
+ exports.DEFAULT_CONFIG = {
6
+ batchSize: 20,
7
+ flushInterval: 30000,
8
+ enableTouchTracking: true,
9
+ enableNavigationTracking: true,
10
+ enableSnapshots: true,
11
+ // 0.5 fps. JPEG screenshots at 390px/40% quality are ~15-30 KB each,
12
+ // so 2 s gives a good quality/bandwidth tradeoff for MVP session replay.
13
+ snapshotInterval: 2000,
14
+ // Cap sessions at ~30 minutes of continuous capture (900 frames).
15
+ maxSnapshotsPerSession: 900,
16
+ // Smaller batches than events - JPEGs are heavier.
17
+ snapshotBatchSize: 8,
18
+ snapshotFlushInterval: 15000,
19
+ // ~32 * 30KB ≈ 1 MB cap on in-memory buffer if the backend is down.
20
+ snapshotMaxBufferSize: 32,
21
+ enableCrashTracking: true,
22
+ debug: false,
23
+ };
24
+ function resolveConfig(config) {
25
+ return { ...exports.DEFAULT_CONFIG, ...config };
26
+ }
@@ -0,0 +1,8 @@
1
+ export declare class Logger {
2
+ private debugEnabled;
3
+ private prefix;
4
+ constructor(debugEnabled: boolean, prefix?: string);
5
+ log(...args: any[]): void;
6
+ warn(...args: any[]): void;
7
+ error(...args: any[]): void;
8
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Logger = void 0;
4
+ class Logger {
5
+ constructor(debugEnabled, prefix = '[Repliqo]') {
6
+ this.debugEnabled = debugEnabled;
7
+ this.prefix = prefix;
8
+ }
9
+ log(...args) {
10
+ if (this.debugEnabled) {
11
+ console.log(this.prefix, ...args);
12
+ }
13
+ }
14
+ warn(...args) {
15
+ if (this.debugEnabled) {
16
+ console.warn(this.prefix, ...args);
17
+ }
18
+ }
19
+ error(...args) {
20
+ if (this.debugEnabled) {
21
+ console.error(this.prefix, ...args);
22
+ }
23
+ }
24
+ }
25
+ exports.Logger = Logger;
@@ -0,0 +1,15 @@
1
+ export { AppAnalytics } from './core/client';
2
+ export { TouchTracker } from './trackers/touch.tracker';
3
+ export { useNavigationTracker, trackScreenChange, } from './trackers/navigation.tracker';
4
+ export { trackScreenEnter, trackScreenExit, } from './trackers/screen.tracker';
5
+ export { ErrorTracker } from './trackers/error.tracker';
6
+ export { SnapshotCapture } from './snapshot/capture';
7
+ export { captureScreenshot } from './snapshot/captureScreenshot';
8
+ export { isNativeScreenCaptureAvailable, captureNativeFrame, } from './snapshot/NativeScreenCapture';
9
+ export type { EventType, AnalyticsEvent, TouchEventData, NavigationEventData, ScreenVisit, DeviceInfo, SDKConfig, } from './types/events';
10
+ export type { ScreenshotData, SnapshotPayload, } from './types/snapshot';
11
+ export type { CrashReport } from './types/crash';
12
+ export type { SnapshotCaptureConfig } from './snapshot/capture';
13
+ export type { ScreenshotResult } from './snapshot/captureScreenshot';
14
+ export { DEFAULT_CONFIG } from './core/config';
15
+ export type { ResolvedConfig } from './core/config';
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_CONFIG = exports.captureNativeFrame = exports.isNativeScreenCaptureAvailable = exports.captureScreenshot = exports.SnapshotCapture = exports.ErrorTracker = exports.trackScreenExit = exports.trackScreenEnter = exports.trackScreenChange = exports.useNavigationTracker = exports.TouchTracker = exports.AppAnalytics = void 0;
4
+ // Main class
5
+ var client_1 = require("./core/client");
6
+ Object.defineProperty(exports, "AppAnalytics", { enumerable: true, get: function () { return client_1.AppAnalytics; } });
7
+ // Trackers
8
+ var touch_tracker_1 = require("./trackers/touch.tracker");
9
+ Object.defineProperty(exports, "TouchTracker", { enumerable: true, get: function () { return touch_tracker_1.TouchTracker; } });
10
+ var navigation_tracker_1 = require("./trackers/navigation.tracker");
11
+ Object.defineProperty(exports, "useNavigationTracker", { enumerable: true, get: function () { return navigation_tracker_1.useNavigationTracker; } });
12
+ Object.defineProperty(exports, "trackScreenChange", { enumerable: true, get: function () { return navigation_tracker_1.trackScreenChange; } });
13
+ var screen_tracker_1 = require("./trackers/screen.tracker");
14
+ Object.defineProperty(exports, "trackScreenEnter", { enumerable: true, get: function () { return screen_tracker_1.trackScreenEnter; } });
15
+ Object.defineProperty(exports, "trackScreenExit", { enumerable: true, get: function () { return screen_tracker_1.trackScreenExit; } });
16
+ var error_tracker_1 = require("./trackers/error.tracker");
17
+ Object.defineProperty(exports, "ErrorTracker", { enumerable: true, get: function () { return error_tracker_1.ErrorTracker; } });
18
+ // Snapshot
19
+ var capture_1 = require("./snapshot/capture");
20
+ Object.defineProperty(exports, "SnapshotCapture", { enumerable: true, get: function () { return capture_1.SnapshotCapture; } });
21
+ var captureScreenshot_1 = require("./snapshot/captureScreenshot");
22
+ Object.defineProperty(exports, "captureScreenshot", { enumerable: true, get: function () { return captureScreenshot_1.captureScreenshot; } });
23
+ var NativeScreenCapture_1 = require("./snapshot/NativeScreenCapture");
24
+ Object.defineProperty(exports, "isNativeScreenCaptureAvailable", { enumerable: true, get: function () { return NativeScreenCapture_1.isNativeScreenCaptureAvailable; } });
25
+ Object.defineProperty(exports, "captureNativeFrame", { enumerable: true, get: function () { return NativeScreenCapture_1.captureNativeFrame; } });
26
+ // Config
27
+ var config_1 = require("./core/config");
28
+ Object.defineProperty(exports, "DEFAULT_CONFIG", { enumerable: true, get: function () { return config_1.DEFAULT_CONFIG; } });
@@ -0,0 +1,17 @@
1
+ export interface NativeFrameResult {
2
+ image: string;
3
+ width: number;
4
+ height: number;
5
+ format: 'jpeg';
6
+ }
7
+ /** Whether the native multi-window capture module is available. */
8
+ export declare function isNativeScreenCaptureAvailable(): boolean;
9
+ /**
10
+ * Capture a single frame using native multi-window capture.
11
+ * Captures ALL visible windows: main view + modals + alerts + dialogs.
12
+ *
13
+ * No permissions required. No dialog. No foreground service.
14
+ *
15
+ * @returns NativeFrameResult or null if capture failed / not available
16
+ */
17
+ export declare function captureNativeFrame(): Promise<NativeFrameResult | null>;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isNativeScreenCaptureAvailable = isNativeScreenCaptureAvailable;
4
+ exports.captureNativeFrame = captureNativeFrame;
5
+ const react_native_1 = require("react-native");
6
+ const NativeModule = react_native_1.Platform.OS === 'android'
7
+ ? react_native_1.NativeModules.ScreenCaptureModule ?? null
8
+ : null;
9
+ /** Whether the native multi-window capture module is available. */
10
+ function isNativeScreenCaptureAvailable() {
11
+ return NativeModule !== null;
12
+ }
13
+ /**
14
+ * Capture a single frame using native multi-window capture.
15
+ * Captures ALL visible windows: main view + modals + alerts + dialogs.
16
+ *
17
+ * No permissions required. No dialog. No foreground service.
18
+ *
19
+ * @returns NativeFrameResult or null if capture failed / not available
20
+ */
21
+ async function captureNativeFrame() {
22
+ if (!NativeModule)
23
+ return null;
24
+ try {
25
+ const result = await NativeModule.captureFrame();
26
+ if (!result || !result.image)
27
+ return null;
28
+ return {
29
+ image: result.image,
30
+ width: result.width,
31
+ height: result.height,
32
+ format: 'jpeg',
33
+ };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
@@ -0,0 +1,36 @@
1
+ import { SnapshotPayload, ScreenshotData } from '../types/snapshot';
2
+ export interface SnapshotCaptureConfig {
3
+ captureInterval: number;
4
+ maxSnapshotsPerSession: number;
5
+ }
6
+ export type ScreenshotProvider = () => Promise<ScreenshotData | null>;
7
+ type LogFn = (...args: any[]) => void;
8
+ /**
9
+ * Captures JPEG screenshots of the RN view hierarchy at a fixed interval
10
+ * and forwards them to the host (usually the snapshot BatchQueue).
11
+ */
12
+ export declare class SnapshotCapture {
13
+ private timeoutId;
14
+ private sequence;
15
+ private config;
16
+ private onSnapshot;
17
+ private sessionId;
18
+ private currentScreen;
19
+ private capturing;
20
+ private running;
21
+ private provider;
22
+ private log;
23
+ private warn;
24
+ constructor(config: SnapshotCaptureConfig, onSnapshot: (snapshot: SnapshotPayload) => void, provider?: ScreenshotProvider, log?: LogFn, warn?: LogFn);
25
+ start(): void;
26
+ stop(): void;
27
+ captureNow(): Promise<void>;
28
+ private scheduleNext;
29
+ private tick;
30
+ reset(): void;
31
+ setSessionId(sessionId: string | null): void;
32
+ setCurrentScreen(screenName: string): void;
33
+ getSequence(): number;
34
+ isRunning(): boolean;
35
+ }
36
+ export {};
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SnapshotCapture = void 0;
4
+ const captureScreenshot_1 = require("./captureScreenshot");
5
+ /**
6
+ * Captures JPEG screenshots of the RN view hierarchy at a fixed interval
7
+ * and forwards them to the host (usually the snapshot BatchQueue).
8
+ */
9
+ class SnapshotCapture {
10
+ constructor(config, onSnapshot, provider = captureScreenshot_1.captureScreenshot, log = () => { }, warn = () => { }) {
11
+ this.timeoutId = null;
12
+ this.sequence = 0;
13
+ this.sessionId = null;
14
+ this.currentScreen = 'unknown';
15
+ this.capturing = false;
16
+ this.running = false;
17
+ this.config = config;
18
+ this.onSnapshot = onSnapshot;
19
+ this.provider = provider;
20
+ this.log = log;
21
+ this.warn = warn;
22
+ }
23
+ start() {
24
+ if (this.running)
25
+ return;
26
+ this.running = true;
27
+ this.log('Snapshot capture started (interval:', this.config.captureInterval, 'ms)');
28
+ this.scheduleNext();
29
+ }
30
+ stop() {
31
+ this.running = false;
32
+ if (this.timeoutId !== null) {
33
+ clearTimeout(this.timeoutId);
34
+ this.timeoutId = null;
35
+ }
36
+ this.log('Snapshot capture stopped');
37
+ }
38
+ async captureNow() {
39
+ await this.tick();
40
+ }
41
+ scheduleNext() {
42
+ if (!this.running)
43
+ return;
44
+ this.timeoutId = setTimeout(() => {
45
+ Promise.resolve(this.tick()).finally(() => {
46
+ if (this.running) {
47
+ this.scheduleNext();
48
+ }
49
+ });
50
+ }, this.config.captureInterval);
51
+ }
52
+ async tick() {
53
+ if (!this.sessionId)
54
+ return;
55
+ if (this.capturing)
56
+ return;
57
+ if (this.sequence >= this.config.maxSnapshotsPerSession) {
58
+ this.log('Max snapshots reached:', this.sequence);
59
+ this.stop();
60
+ return;
61
+ }
62
+ this.capturing = true;
63
+ let screenshot = null;
64
+ try {
65
+ screenshot = await this.provider();
66
+ }
67
+ catch (err) {
68
+ this.warn('Snapshot capture failed:', err);
69
+ screenshot = null;
70
+ }
71
+ finally {
72
+ this.capturing = false;
73
+ }
74
+ if (!screenshot) {
75
+ this.warn('Snapshot returned null (provider unavailable or failed)');
76
+ return;
77
+ }
78
+ // Strip `source` field before sending — the backend DTO only
79
+ // accepts { image, width, height, format } and rejects unknown props.
80
+ const { image, width, height, format } = screenshot;
81
+ const payload = {
82
+ sessionId: this.sessionId,
83
+ screenName: this.currentScreen,
84
+ timestamp: new Date().toISOString(),
85
+ sequence: this.sequence,
86
+ isDiff: false,
87
+ data: { image, width, height, format },
88
+ };
89
+ this.sequence++;
90
+ this.log('Snapshot captured: seq=', this.sequence - 1, 'screen=', this.currentScreen, 'size=', Math.round(screenshot.image.length / 1024), 'KB');
91
+ this.onSnapshot(payload);
92
+ }
93
+ reset() {
94
+ this.sequence = 0;
95
+ }
96
+ setSessionId(sessionId) {
97
+ this.sessionId = sessionId;
98
+ }
99
+ setCurrentScreen(screenName) {
100
+ this.currentScreen = screenName;
101
+ }
102
+ getSequence() {
103
+ return this.sequence;
104
+ }
105
+ isRunning() {
106
+ return this.running;
107
+ }
108
+ }
109
+ exports.SnapshotCapture = SnapshotCapture;
@@ -0,0 +1,26 @@
1
+ export type CaptureSource = 'native' | 'viewshot';
2
+ export interface ScreenshotResult {
3
+ image: string;
4
+ width: number;
5
+ height: number;
6
+ format: 'jpeg';
7
+ /** Which capture method produced this frame. */
8
+ source: CaptureSource;
9
+ }
10
+ /**
11
+ * Capture a JPEG screenshot using the best available method:
12
+ *
13
+ * 1. Native multi-window capture (Android) — uses View.draw() on all
14
+ * app windows via WindowManagerGlobal reflection. Captures modals,
15
+ * alerts, dialogs, overlays. No permissions needed.
16
+ * 2. react-native-view-shot fallback — captures the main RN view only
17
+ * (no modals or native dialogs)
18
+ * 3. null — if both are unavailable
19
+ *
20
+ * Never throws. Returns null on any failure.
21
+ */
22
+ export declare function captureScreenshot(): Promise<ScreenshotResult | null>;
23
+ /**
24
+ * Test-only: reset cached view-shot resolution.
25
+ */
26
+ export declare function __resetCaptureRefForTests(): void;