@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,142 +1,154 @@
1
- import { SnapshotPayload, ScreenshotData } from '../types/snapshot';
2
- import { captureScreenshot } from './captureScreenshot';
3
-
4
- export interface SnapshotCaptureConfig {
5
- captureInterval: number;
6
- maxSnapshotsPerSession: number;
7
- }
8
-
9
- export type ScreenshotProvider = () => Promise<ScreenshotData | null>;
10
-
11
- type LogFn = (...args: any[]) => void;
12
-
13
- /**
14
- * Captures JPEG screenshots of the RN view hierarchy at a fixed interval
15
- * and forwards them to the host (usually the snapshot BatchQueue).
16
- */
17
- export class SnapshotCapture {
18
- private timeoutId: ReturnType<typeof setTimeout> | null = null;
19
- private sequence: number = 0;
20
- private config: SnapshotCaptureConfig;
21
- private onSnapshot: (snapshot: SnapshotPayload) => void;
22
- private sessionId: string | null = null;
23
- private currentScreen: string = 'unknown';
24
- private capturing: boolean = false;
25
- private running: boolean = false;
26
- private provider: ScreenshotProvider;
27
- private log: LogFn;
28
- private warn: LogFn;
29
-
30
- constructor(
31
- config: SnapshotCaptureConfig,
32
- onSnapshot: (snapshot: SnapshotPayload) => void,
33
- provider: ScreenshotProvider = captureScreenshot,
34
- log: LogFn = () => {},
35
- warn: LogFn = () => {},
36
- ) {
37
- this.config = config;
38
- this.onSnapshot = onSnapshot;
39
- this.provider = provider;
40
- this.log = log;
41
- this.warn = warn;
42
- }
43
-
44
- start(): void {
45
- if (this.running) return;
46
- this.running = true;
47
- this.log('Snapshot capture started (interval:', this.config.captureInterval, 'ms)');
48
- this.scheduleNext();
49
- }
50
-
51
- stop(): void {
52
- this.running = false;
53
- if (this.timeoutId !== null) {
54
- clearTimeout(this.timeoutId);
55
- this.timeoutId = null;
56
- }
57
- this.log('Snapshot capture stopped');
58
- }
59
-
60
- async captureNow(): Promise<void> {
61
- await this.tick();
62
- }
63
-
64
- private scheduleNext(): void {
65
- if (!this.running) return;
66
- this.timeoutId = setTimeout(() => {
67
- Promise.resolve(this.tick()).finally(() => {
68
- if (this.running) {
69
- this.scheduleNext();
70
- }
71
- });
72
- }, this.config.captureInterval);
73
- }
74
-
75
- private async tick(): Promise<void> {
76
- if (!this.sessionId) return;
77
- if (this.capturing) return;
78
-
79
- if (this.sequence >= this.config.maxSnapshotsPerSession) {
80
- this.log('Max snapshots reached:', this.sequence);
81
- this.stop();
82
- return;
83
- }
84
-
85
- this.capturing = true;
86
- let screenshot: ScreenshotData | null = null;
87
- try {
88
- screenshot = await this.provider();
89
- } catch (err) {
90
- this.warn('Snapshot capture failed:', err);
91
- screenshot = null;
92
- } finally {
93
- this.capturing = false;
94
- }
95
-
96
- if (!screenshot) {
97
- this.warn('Snapshot returned null (provider unavailable or failed)');
98
- return;
99
- }
100
-
101
- // Strip `source` field before sending — the backend DTO only
102
- // accepts { image, width, height, format } and rejects unknown props.
103
- const { image, width, height, format } = screenshot;
104
-
105
- const payload: SnapshotPayload = {
106
- sessionId: this.sessionId,
107
- screenName: this.currentScreen,
108
- timestamp: new Date().toISOString(),
109
- sequence: this.sequence,
110
- isDiff: false,
111
- data: { image, width, height, format },
112
- };
113
-
114
- this.sequence++;
115
- this.log(
116
- 'Snapshot captured: seq=', this.sequence - 1,
117
- 'screen=', this.currentScreen,
118
- 'size=', Math.round(screenshot.image.length / 1024), 'KB',
119
- );
120
- this.onSnapshot(payload);
121
- }
122
-
123
- reset(): void {
124
- this.sequence = 0;
125
- }
126
-
127
- setSessionId(sessionId: string | null): void {
128
- this.sessionId = sessionId;
129
- }
130
-
131
- setCurrentScreen(screenName: string): void {
132
- this.currentScreen = screenName;
133
- }
134
-
135
- getSequence(): number {
136
- return this.sequence;
137
- }
138
-
139
- isRunning(): boolean {
140
- return this.running;
141
- }
142
- }
1
+ import { SnapshotPayload, ScreenshotData } from '../types/snapshot';
2
+ import { captureScreenshot } from './captureScreenshot';
3
+
4
+ export interface SnapshotCaptureConfig {
5
+ captureInterval: number;
6
+ maxSnapshotsPerSession: number;
7
+ }
8
+
9
+ export type ScreenshotProvider = () => Promise<ScreenshotData | null>;
10
+
11
+ type LogFn = (...args: any[]) => void;
12
+
13
+ /**
14
+ * Captures JPEG screenshots of the RN view hierarchy at a fixed interval
15
+ * and forwards them to the host (usually the snapshot BatchQueue).
16
+ */
17
+ export class SnapshotCapture {
18
+ private timeoutId: ReturnType<typeof setTimeout> | null = null;
19
+ private sequence: number = 0;
20
+ private config: SnapshotCaptureConfig;
21
+ private onSnapshot: (snapshot: SnapshotPayload) => void;
22
+ private sessionId: string | null = null;
23
+ private currentScreen: string = 'unknown';
24
+ private capturing: boolean = false;
25
+ private running: boolean = false;
26
+ private provider: ScreenshotProvider;
27
+ private log: LogFn;
28
+ private warn: LogFn;
29
+
30
+ constructor(
31
+ config: SnapshotCaptureConfig,
32
+ onSnapshot: (snapshot: SnapshotPayload) => void,
33
+ provider: ScreenshotProvider = captureScreenshot,
34
+ log: LogFn = () => {},
35
+ warn: LogFn = () => {},
36
+ ) {
37
+ this.config = config;
38
+ this.onSnapshot = onSnapshot;
39
+ this.provider = provider;
40
+ this.log = log;
41
+ this.warn = warn;
42
+ }
43
+
44
+ /**
45
+ * Generation counter. Incremented on every start()/stop() so that timer
46
+ * chains from a PREVIOUS generation cannot reschedule themselves. Without
47
+ * it, a fast stop()→start() while a tick's capture is still in flight
48
+ * leaves two parallel timer chains alive (double capture rate).
49
+ */
50
+ private epoch = 0;
51
+
52
+ start(): void {
53
+ if (this.running) return;
54
+ this.running = true;
55
+ this.epoch++;
56
+ this.log('Snapshot capture started (interval:', this.config.captureInterval, 'ms)');
57
+ this.scheduleNext(this.epoch);
58
+ }
59
+
60
+ stop(): void {
61
+ this.running = false;
62
+ this.epoch++;
63
+ if (this.timeoutId !== null) {
64
+ clearTimeout(this.timeoutId);
65
+ this.timeoutId = null;
66
+ }
67
+ this.log('Snapshot capture stopped');
68
+ }
69
+
70
+ async captureNow(): Promise<void> {
71
+ await this.tick();
72
+ }
73
+
74
+ private scheduleNext(epoch: number): void {
75
+ if (!this.running || epoch !== this.epoch) return;
76
+ this.timeoutId = setTimeout(() => {
77
+ // Orphan timer from a previous generation — do nothing.
78
+ if (epoch !== this.epoch) return;
79
+ Promise.resolve(this.tick()).finally(() => {
80
+ if (this.running && epoch === this.epoch) {
81
+ this.scheduleNext(epoch);
82
+ }
83
+ });
84
+ }, this.config.captureInterval);
85
+ }
86
+
87
+ private async tick(): Promise<void> {
88
+ if (!this.sessionId) return;
89
+ if (this.capturing) return;
90
+
91
+ if (this.sequence >= this.config.maxSnapshotsPerSession) {
92
+ this.log('Max snapshots reached:', this.sequence);
93
+ this.stop();
94
+ return;
95
+ }
96
+
97
+ this.capturing = true;
98
+ let screenshot: ScreenshotData | null = null;
99
+ try {
100
+ screenshot = await this.provider();
101
+ } catch (err) {
102
+ this.warn('Snapshot capture failed:', err);
103
+ screenshot = null;
104
+ } finally {
105
+ this.capturing = false;
106
+ }
107
+
108
+ if (!screenshot) {
109
+ this.warn('Snapshot returned null (provider unavailable or failed)');
110
+ return;
111
+ }
112
+
113
+ // Strip `source` field before sending — the backend DTO only
114
+ // accepts { image, width, height, format } and rejects unknown props.
115
+ const { image, width, height, format } = screenshot;
116
+
117
+ const payload: SnapshotPayload = {
118
+ sessionId: this.sessionId,
119
+ screenName: this.currentScreen,
120
+ timestamp: new Date().toISOString(),
121
+ sequence: this.sequence,
122
+ isDiff: false,
123
+ data: { image, width, height, format },
124
+ };
125
+
126
+ this.sequence++;
127
+ this.log(
128
+ 'Snapshot captured: seq=', this.sequence - 1,
129
+ 'screen=', this.currentScreen,
130
+ 'size=', Math.round(screenshot.image.length / 1024), 'KB',
131
+ );
132
+ this.onSnapshot(payload);
133
+ }
134
+
135
+ reset(): void {
136
+ this.sequence = 0;
137
+ }
138
+
139
+ setSessionId(sessionId: string | null): void {
140
+ this.sessionId = sessionId;
141
+ }
142
+
143
+ setCurrentScreen(screenName: string): void {
144
+ this.currentScreen = screenName;
145
+ }
146
+
147
+ getSequence(): number {
148
+ return this.sequence;
149
+ }
150
+
151
+ isRunning(): boolean {
152
+ return this.running;
153
+ }
154
+ }
@@ -1,136 +1,211 @@
1
- import { CrashReport } from '../types/crash';
2
-
3
- type CrashCallback = (crash: CrashReport) => void;
4
-
5
- export class ErrorTracker {
6
- private onCrash: CrashCallback;
7
- private sessionId: string | null = null;
8
- private currentScreen: string | null = null;
9
- private originalHandler:
10
- | ((error: Error, isFatal?: boolean) => void)
11
- | null = null;
12
- private isActive = false;
13
-
14
- constructor(onCrash: CrashCallback) {
15
- this.onCrash = onCrash;
16
- }
17
-
18
- setSessionId(id: string | null): void {
19
- this.sessionId = id;
20
- }
21
-
22
- setCurrentScreen(screen: string | null): void {
23
- this.currentScreen = screen;
24
- }
25
-
26
- start(): void {
27
- if (this.isActive) return;
28
- this.isActive = true;
29
- this.setupGlobalErrorHandler();
30
- this.setupUnhandledRejectionHandler();
31
- }
32
-
33
- stop(): void {
34
- if (!this.isActive) return;
35
- this.isActive = false;
36
- this.restoreGlobalErrorHandler();
37
- }
38
-
39
- /**
40
- * Manual error reporting - allows consumers to report caught errors.
41
- */
42
- reportError(error: Error, metadata?: Record<string, any>): void {
43
- try {
44
- const crash: CrashReport = {
45
- sessionId: this.sessionId || undefined,
46
- type: 'js_error',
47
- message: error.message,
48
- stackTrace: error.stack || undefined,
49
- screenName: this.currentScreen || undefined,
50
- metadata,
51
- timestamp: new Date().toISOString(),
52
- };
53
- this.onCrash(crash);
54
- } catch (_e) {
55
- // Never crash the app due to error reporting
56
- }
57
- }
58
-
59
- private setupGlobalErrorHandler(): void {
60
- try {
61
- const ErrorUtils = (global as any).ErrorUtils;
62
- if (ErrorUtils) {
63
- this.originalHandler = ErrorUtils.getGlobalHandler();
64
- ErrorUtils.setGlobalHandler(
65
- (error: Error, isFatal?: boolean) => {
66
- try {
67
- const crash: CrashReport = {
68
- sessionId: this.sessionId || undefined,
69
- type: isFatal ? 'native_crash' : 'js_error',
70
- message: error.message,
71
- stackTrace: error.stack || undefined,
72
- screenName: this.currentScreen || undefined,
73
- metadata: { isFatal },
74
- timestamp: new Date().toISOString(),
75
- };
76
- this.onCrash(crash);
77
- } catch (_e) {
78
- // Never crash the app due to error reporting
79
- }
80
-
81
- // Always call original handler so RN can handle the error
82
- if (this.originalHandler) {
83
- this.originalHandler(error, isFatal);
84
- }
85
- },
86
- );
87
- }
88
- } catch (_e) {
89
- // Never crash the app due to error handler setup
90
- }
91
- }
92
-
93
- private setupUnhandledRejectionHandler(): void {
94
- try {
95
- if (typeof global !== 'undefined') {
96
- (global as any).__analyticsUnhandledRejection = (
97
- _id: number,
98
- error: any,
99
- ) => {
100
- try {
101
- const message =
102
- error instanceof Error ? error.message : String(error);
103
- const stack =
104
- error instanceof Error ? error.stack : undefined;
105
-
106
- const crash: CrashReport = {
107
- sessionId: this.sessionId || undefined,
108
- type: 'unhandled_rejection',
109
- message,
110
- stackTrace: stack || undefined,
111
- screenName: this.currentScreen || undefined,
112
- timestamp: new Date().toISOString(),
113
- };
114
- this.onCrash(crash);
115
- } catch (_e) {
116
- // Never crash the app due to error reporting
117
- }
118
- };
119
- }
120
- } catch (_e) {
121
- // Never crash the app due to rejection handler setup
122
- }
123
- }
124
-
125
- private restoreGlobalErrorHandler(): void {
126
- try {
127
- const ErrorUtils = (global as any).ErrorUtils;
128
- if (ErrorUtils && this.originalHandler) {
129
- ErrorUtils.setGlobalHandler(this.originalHandler);
130
- this.originalHandler = null;
131
- }
132
- } catch (_e) {
133
- // Never crash the app due to handler restoration
134
- }
135
- }
136
- }
1
+ import { CrashReport } from '../types/crash';
2
+
3
+ type CrashCallback = (crash: CrashReport) => void;
4
+
5
+ /* Dynamic require for React Native's optional promise internals. */
6
+ declare const require: any;
7
+
8
+ export class ErrorTracker {
9
+ private onCrash: CrashCallback;
10
+ private sessionId: string | null = null;
11
+ private currentScreen: string | null = null;
12
+ private originalHandler:
13
+ | ((error: Error, isFatal?: boolean) => void)
14
+ | null = null;
15
+ private wrapperHandler:
16
+ | ((error: Error, isFatal?: boolean) => void)
17
+ | null = null;
18
+ private isActive = false;
19
+
20
+ constructor(onCrash: CrashCallback) {
21
+ this.onCrash = onCrash;
22
+ }
23
+
24
+ setSessionId(id: string | null): void {
25
+ this.sessionId = id;
26
+ }
27
+
28
+ setCurrentScreen(screen: string | null): void {
29
+ this.currentScreen = screen;
30
+ }
31
+
32
+ start(): void {
33
+ if (this.isActive) return;
34
+ this.isActive = true;
35
+ this.setupGlobalErrorHandler();
36
+ this.setupUnhandledRejectionHandler();
37
+ }
38
+
39
+ stop(): void {
40
+ if (!this.isActive) return;
41
+ // Flip the flag FIRST — the rejection trackers can't always be
42
+ // unregistered (Hermes), so their callbacks guard on isActive.
43
+ this.isActive = false;
44
+ this.restoreGlobalErrorHandler();
45
+ this.teardownUnhandledRejectionHandler();
46
+ }
47
+
48
+ /**
49
+ * Manual error reporting - allows consumers to report caught errors.
50
+ */
51
+ reportError(error: Error, metadata?: Record<string, any>): void {
52
+ try {
53
+ const crash: CrashReport = {
54
+ sessionId: this.sessionId || undefined,
55
+ type: 'js_error',
56
+ message: error.message,
57
+ stackTrace: error.stack || undefined,
58
+ screenName: this.currentScreen || undefined,
59
+ metadata,
60
+ timestamp: new Date().toISOString(),
61
+ };
62
+ this.onCrash(crash);
63
+ } catch (_e) {
64
+ // Never crash the app due to error reporting
65
+ }
66
+ }
67
+
68
+ private setupGlobalErrorHandler(): void {
69
+ try {
70
+ const ErrorUtils = (global as any).ErrorUtils;
71
+ if (ErrorUtils) {
72
+ this.originalHandler = ErrorUtils.getGlobalHandler();
73
+ this.wrapperHandler = (error: Error, isFatal?: boolean) => {
74
+ if (this.isActive) {
75
+ try {
76
+ const crash: CrashReport = {
77
+ sessionId: this.sessionId || undefined,
78
+ type: isFatal ? 'native_crash' : 'js_error',
79
+ message: error.message,
80
+ stackTrace: error.stack || undefined,
81
+ screenName: this.currentScreen || undefined,
82
+ metadata: { isFatal },
83
+ timestamp: new Date().toISOString(),
84
+ };
85
+ this.onCrash(crash);
86
+ } catch (_e) {
87
+ // Never crash the app due to error reporting
88
+ }
89
+ }
90
+
91
+ // Always call original handler so RN can handle the error
92
+ if (this.originalHandler) {
93
+ this.originalHandler(error, isFatal);
94
+ }
95
+ };
96
+ ErrorUtils.setGlobalHandler(this.wrapperHandler);
97
+ }
98
+ } catch (_e) {
99
+ // Never crash the app due to error handler setup
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Handle an unhandled promise rejection. Guarded on isActive because the
105
+ * underlying trackers can't always be unregistered.
106
+ */
107
+ private handleUnhandledRejection = (_id: number, error: any): void => {
108
+ if (!this.isActive) return;
109
+ try {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ const stack = error instanceof Error ? error.stack : undefined;
112
+
113
+ const crash: CrashReport = {
114
+ sessionId: this.sessionId || undefined,
115
+ type: 'unhandled_rejection',
116
+ message,
117
+ stackTrace: stack || undefined,
118
+ screenName: this.currentScreen || undefined,
119
+ timestamp: new Date().toISOString(),
120
+ };
121
+ this.onCrash(crash);
122
+ } catch (_e) {
123
+ // Never crash the app due to error reporting
124
+ }
125
+ };
126
+
127
+ /**
128
+ * Register with the mechanisms React Native ACTUALLY uses to surface
129
+ * unhandled promise rejections:
130
+ *
131
+ * 1. Hermes' native promise rejection tracker (Hermes engine).
132
+ * 2. The `promise` polyfill's rejection-tracking module (JSC and older
133
+ * RN versions).
134
+ *
135
+ * A bare global callback (the old approach) is never invoked by RN — it
136
+ * silently captured nothing.
137
+ *
138
+ * KNOWN LIMITATION: these trackers hold a single handler — registering
139
+ * REPLACES whatever was installed before (RN's dev LogBox warning, or
140
+ * another SDK like Sentry if it initialized first), and there is no
141
+ * unregister API, so after stop() an inert handler remains installed.
142
+ * If you run multiple error-tracking SDKs, initialize Repliqo FIRST so
143
+ * the other SDK's handler wins, or disable enableCrashTracking.
144
+ */
145
+ private setupUnhandledRejectionHandler(): void {
146
+ // Kept for manual invocation / test hooks.
147
+ try {
148
+ (global as any).__analyticsUnhandledRejection =
149
+ this.handleUnhandledRejection;
150
+ } catch (_e) {
151
+ /* ignore */
152
+ }
153
+
154
+ // Hermes: native promise rejection tracking.
155
+ try {
156
+ const hermes = (global as any).HermesInternal;
157
+ if (hermes?.hasPromise?.() && hermes?.enablePromiseRejectionTracker) {
158
+ hermes.enablePromiseRejectionTracker({
159
+ allRejections: true,
160
+ onUnhandled: this.handleUnhandledRejection,
161
+ onHandled: () => {},
162
+ });
163
+ return;
164
+ }
165
+ } catch (_e) {
166
+ /* ignore — fall through to the polyfill */
167
+ }
168
+
169
+ // JSC / promise polyfill path.
170
+ try {
171
+ const rejectionTracking = require('promise/setimmediate/rejection-tracking');
172
+ rejectionTracking.enable({
173
+ allRejections: true,
174
+ onUnhandled: this.handleUnhandledRejection,
175
+ onHandled: () => {},
176
+ });
177
+ } catch (_e) {
178
+ // Module unavailable (e.g. jest without the polyfill) — the global
179
+ // hook above still allows manual reporting.
180
+ }
181
+ }
182
+
183
+ private teardownUnhandledRejectionHandler(): void {
184
+ try {
185
+ delete (global as any).__analyticsUnhandledRejection;
186
+ } catch (_e) {
187
+ /* ignore */
188
+ }
189
+ // Hermes offers no unregister API and disabling the polyfill tracker
190
+ // could clobber another library's handler — the isActive guard in
191
+ // handleUnhandledRejection makes lingering registrations inert.
192
+ }
193
+
194
+ private restoreGlobalErrorHandler(): void {
195
+ try {
196
+ const ErrorUtils = (global as any).ErrorUtils;
197
+ if (ErrorUtils && this.originalHandler) {
198
+ // Only restore if WE are still the installed handler — another
199
+ // library may have wrapped us after start(), and restoring would
200
+ // clobber its handler.
201
+ if (ErrorUtils.getGlobalHandler() === this.wrapperHandler) {
202
+ ErrorUtils.setGlobalHandler(this.originalHandler);
203
+ }
204
+ this.originalHandler = null;
205
+ this.wrapperHandler = null;
206
+ }
207
+ } catch (_e) {
208
+ // Never crash the app due to handler restoration
209
+ }
210
+ }
211
+ }