@merittdev/horus-lens 0.5.1 → 0.6.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.
@@ -0,0 +1,2105 @@
1
+ import {
2
+ DEFAULT_ENDPOINT,
3
+ InitiateReportResponseSchema,
4
+ RingBuffer,
5
+ SDK_VERSION,
6
+ Transport,
7
+ installConsole
8
+ } from "./chunk-XGZ7BZTS.js";
9
+
10
+ // ../react-native/dist/index.js
11
+ import { AppState as AppState2 } from "react-native";
12
+ import {
13
+ Appearance,
14
+ Dimensions,
15
+ I18nManager,
16
+ PixelRatio,
17
+ Platform
18
+ } from "react-native";
19
+ import { AppState, Platform as Platform2 } from "react-native";
20
+ import { releaseCapture } from "react-native-view-shot";
21
+ import {
22
+ createContext,
23
+ useCallback,
24
+ useContext,
25
+ useEffect as useEffect3,
26
+ useMemo as useMemo2,
27
+ useRef as useRef2,
28
+ useState as useState3
29
+ } from "react";
30
+ import {
31
+ ActivityIndicator as ActivityIndicator2,
32
+ I18nManager as I18nManager2,
33
+ PanResponder as PanResponder2,
34
+ Pressable as Pressable3,
35
+ Text as Text3,
36
+ View as View3,
37
+ useWindowDimensions as useWindowDimensions2
38
+ } from "react-native";
39
+ import { useEffect as useEffect2, useState as useState2 } from "react";
40
+ import {
41
+ ActivityIndicator,
42
+ Image as Image2,
43
+ KeyboardAvoidingView,
44
+ Modal,
45
+ Platform as Platform3,
46
+ Pressable as Pressable2,
47
+ ScrollView as ScrollView2,
48
+ Share,
49
+ StyleSheet as StyleSheet2,
50
+ Text as Text2,
51
+ TextInput as TextInput2,
52
+ View as View2
53
+ } from "react-native";
54
+ import { useEffect, useMemo, useRef, useState } from "react";
55
+ import {
56
+ Image,
57
+ PanResponder,
58
+ Pressable,
59
+ ScrollView,
60
+ StyleSheet,
61
+ Text,
62
+ TextInput,
63
+ View,
64
+ useWindowDimensions
65
+ } from "react-native";
66
+ import Svg, {
67
+ Circle,
68
+ G,
69
+ Ellipse,
70
+ Line,
71
+ Path,
72
+ Polygon,
73
+ Rect,
74
+ Text as SvgText
75
+ } from "react-native-svg";
76
+ import { captureRef } from "react-native-view-shot";
77
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
78
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
79
+ import Svg2, { Circle as Circle2, Path as Path2 } from "react-native-svg";
80
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
81
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
82
+ function collectNativeEnv(origin, route, language) {
83
+ const viewport = Dimensions.get("window");
84
+ const screen = Dimensions.get("screen");
85
+ const locale = Intl.DateTimeFormat().resolvedOptions();
86
+ const theme = Appearance.getColorScheme();
87
+ return {
88
+ url: `${origin}${route}`,
89
+ route,
90
+ userAgent: `ReactNative (${Platform.OS} ${Platform.Version})`,
91
+ os: `${Platform.OS} ${Platform.Version}`,
92
+ device: "mobile",
93
+ viewport: { width: viewport.width, height: viewport.height },
94
+ screen: { width: screen.width, height: screen.height },
95
+ devicePixelRatio: PixelRatio.get(),
96
+ language: language ?? locale.locale,
97
+ timezone: locale.timeZone,
98
+ ...theme === "light" || theme === "dark" ? { theme } : {},
99
+ capturedAt: Date.now()
100
+ };
101
+ }
102
+ function nativeDetails() {
103
+ return {
104
+ framework: "react-native",
105
+ platform: Platform.OS,
106
+ platformVersion: String(Platform.Version),
107
+ rtl: I18nManager.isRTL
108
+ };
109
+ }
110
+ function sanitizeUrl(value) {
111
+ const withoutQuery = value.split(/[?#]/, 1)[0] ?? "";
112
+ return withoutQuery.replace(/^((?:[a-z][a-z\d+.-]*:)?\/\/)[^/]*@/i, "$1");
113
+ }
114
+ function installFetch(buffer) {
115
+ const original = globalThis.fetch;
116
+ const dispatch = original.bind(globalThis);
117
+ let active = true;
118
+ const wrapped = async (input, init) => {
119
+ let url = "";
120
+ let method = "GET";
121
+ try {
122
+ url = sanitizeUrl(
123
+ typeof input === "string" ? input : "url" in input ? input.url : input.href
124
+ );
125
+ method = (init?.method ?? (typeof input === "object" && "method" in input ? input.method : "GET")).toUpperCase();
126
+ } catch {
127
+ }
128
+ const startedAt = Date.now();
129
+ const entry = {
130
+ method,
131
+ url,
132
+ startedAt,
133
+ requestType: "fetch",
134
+ status: null,
135
+ ok: null,
136
+ durationMs: null
137
+ };
138
+ try {
139
+ const response = await dispatch(input, init);
140
+ entry.status = response.status;
141
+ entry.ok = response.ok;
142
+ return response;
143
+ } catch (error) {
144
+ entry.error = "Network request failed";
145
+ throw error;
146
+ } finally {
147
+ entry.durationMs = Math.max(0, Date.now() - startedAt);
148
+ if (active) buffer.push(entry);
149
+ }
150
+ };
151
+ globalThis.fetch = wrapped;
152
+ return () => {
153
+ active = false;
154
+ if (globalThis.fetch === wrapped) globalThis.fetch = original;
155
+ };
156
+ }
157
+ function imageUri(uri) {
158
+ return uri.startsWith("/") ? `file://${uri}` : uri;
159
+ }
160
+ function releaseImage(uri) {
161
+ if (!uri.startsWith("file:")) return;
162
+ releaseCapture(Platform2.OS === "ios" ? uri.replace(/^file:\/\//, "") : uri);
163
+ }
164
+ function validateImage(image, dataOnly = false) {
165
+ if (!/^data:image\/(?:png|jpeg);base64,[A-Za-z0-9+/=]+$/.test(image.uri) && (dataOnly || !/^file:\/\/\/[^?#]+\.(?:png|jpe?g)$/i.test(image.uri)) || image.uri.length > 8e6 || !Number.isFinite(image.width) || !Number.isFinite(image.height) || image.width <= 0 || image.height <= 0) {
166
+ throw new Error("Invalid captured image");
167
+ }
168
+ }
169
+ var NativeRecording = class {
170
+ constructor(capture, route) {
171
+ this.capture = capture;
172
+ this.route = route;
173
+ }
174
+ capture;
175
+ route;
176
+ frames = [];
177
+ timer;
178
+ pending = null;
179
+ generation = 0;
180
+ active = false;
181
+ startedAt = 0;
182
+ bytes = 0;
183
+ failure;
184
+ snapshot() {
185
+ return {
186
+ active: this.active,
187
+ frameCount: this.frames.length,
188
+ durationMs: this.active ? Date.now() - this.startedAt : this.frames.length > 1 ? this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp : 0,
189
+ error: this.failure
190
+ };
191
+ }
192
+ start() {
193
+ this.discard();
194
+ this.active = true;
195
+ this.startedAt = Date.now();
196
+ void this.tick();
197
+ this.timer = setInterval(() => {
198
+ void this.tick();
199
+ }, 1e3);
200
+ }
201
+ halt() {
202
+ this.active = false;
203
+ if (this.timer) clearInterval(this.timer);
204
+ this.timer = void 0;
205
+ }
206
+ async tick() {
207
+ if (!this.active) return;
208
+ if (Date.now() - this.startedAt >= 3e4 || this.frames.length >= 30) {
209
+ this.halt();
210
+ this.generation++;
211
+ return;
212
+ }
213
+ if (this.pending) return;
214
+ if (AppState.currentState !== "active") {
215
+ this.halt();
216
+ return;
217
+ }
218
+ const generation = this.generation;
219
+ const timestamp = Date.now();
220
+ const route = this.route();
221
+ const pending = (async () => {
222
+ try {
223
+ const image = await this.capture("replay");
224
+ if (generation !== this.generation) return;
225
+ if (!image || route !== this.route() || AppState.currentState !== "active") {
226
+ this.halt();
227
+ return;
228
+ }
229
+ validateImage(image, true);
230
+ if (this.bytes + image.uri.length > 6e6) {
231
+ this.halt();
232
+ return;
233
+ }
234
+ this.frames.push({ ...image, timestamp, route });
235
+ this.bytes += image.uri.length;
236
+ } catch {
237
+ if (generation !== this.generation) return;
238
+ this.failure = "Could not capture the screen recording.";
239
+ this.halt();
240
+ }
241
+ })();
242
+ this.pending = pending;
243
+ await pending;
244
+ if (this.pending === pending) this.pending = null;
245
+ }
246
+ async stop() {
247
+ this.halt();
248
+ if (this.pending) {
249
+ let timeout;
250
+ await Promise.race([
251
+ this.pending,
252
+ new Promise((resolve) => {
253
+ timeout = setTimeout(() => {
254
+ this.generation++;
255
+ this.failure = "Screen capture did not finish in time.";
256
+ resolve();
257
+ }, 5e3);
258
+ })
259
+ ]);
260
+ if (timeout) clearTimeout(timeout);
261
+ }
262
+ return this.frames.map((frame) => ({ ...frame }));
263
+ }
264
+ discard() {
265
+ this.halt();
266
+ this.generation++;
267
+ this.frames = [];
268
+ this.bytes = 0;
269
+ this.failure = void 0;
270
+ }
271
+ };
272
+ function encodeVisualReplay(frames, origin) {
273
+ if (!frames.length) return [];
274
+ for (const frame of frames) validateImage(frame, true);
275
+ const first = frames[0];
276
+ const element = (id, tagName, attributes, childNodes = []) => ({ id, type: 2, tagName, attributes, childNodes });
277
+ const node = {
278
+ type: 0,
279
+ id: 1,
280
+ childNodes: [
281
+ { type: 1, id: 2, name: "html", publicId: "", systemId: "" },
282
+ element(3, "html", {}, [
283
+ element(4, "head", {}, [
284
+ element(5, "style", {}, [
285
+ {
286
+ type: 3,
287
+ id: 6,
288
+ textContent: "html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#fff}img{display:block;width:100%;height:100%;object-fit:contain}"
289
+ }
290
+ ])
291
+ ]),
292
+ element(7, "body", {}, [
293
+ element(8, "img", { src: first.uri, alt: "Native screen recording" })
294
+ ])
295
+ ])
296
+ ]
297
+ };
298
+ const events = [
299
+ {
300
+ type: 4,
301
+ timestamp: first.timestamp,
302
+ data: {
303
+ href: origin + first.route,
304
+ width: first.width,
305
+ height: first.height
306
+ }
307
+ },
308
+ {
309
+ type: 2,
310
+ timestamp: first.timestamp,
311
+ data: { node, initialOffset: { top: 0, left: 0 } }
312
+ }
313
+ ];
314
+ for (const frame of frames.slice(1)) {
315
+ events.push({
316
+ type: 3,
317
+ timestamp: frame.timestamp,
318
+ data: {
319
+ source: 0,
320
+ texts: [],
321
+ removes: [],
322
+ adds: [],
323
+ attributes: [{ id: 8, attributes: { src: frame.uri } }]
324
+ }
325
+ });
326
+ }
327
+ events.push({
328
+ type: 5,
329
+ timestamp: Math.max(
330
+ first.timestamp + 1e3,
331
+ frames[frames.length - 1].timestamp + 1e3
332
+ ),
333
+ data: { tag: "native-recording-end", payload: { frames: frames.length } }
334
+ });
335
+ return events;
336
+ }
337
+ var activeClient = null;
338
+ var NativeLensClient = class {
339
+ constructor(config) {
340
+ this.config = config;
341
+ if (!config.accessId.startsWith("lai_"))
342
+ throw new Error("[lens] A public lai_ access id is required");
343
+ const origin = new URL(config.origin);
344
+ if (!["https:", "http:"].includes(origin.protocol) || !origin.host || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash) {
345
+ throw new Error(
346
+ "[lens] origin must be an HTTP(S) site origin without a path"
347
+ );
348
+ }
349
+ this.origin = origin.origin;
350
+ this.app = config.app ?? {};
351
+ this.rawFetch = globalThis.fetch.bind(globalThis);
352
+ if (config.captureScreen)
353
+ this.recording = new NativeRecording(
354
+ config.captureScreen,
355
+ () => this.route
356
+ );
357
+ }
358
+ config;
359
+ origin;
360
+ transport;
361
+ lifecycle = new AbortController();
362
+ logs = new RingBuffer(200);
363
+ requests = new RingBuffer(100);
364
+ errors = new RingBuffer(30);
365
+ breadcrumbs = new RingBuffer(30);
366
+ teardowns = [];
367
+ started = false;
368
+ submitting = false;
369
+ retryCapture;
370
+ route = "/";
371
+ language;
372
+ app;
373
+ recording;
374
+ rawFetch;
375
+ start() {
376
+ if (this.started) return;
377
+ if (activeClient)
378
+ throw new Error(
379
+ "[lens] Only one native Lens client can capture at a time"
380
+ );
381
+ this.lifecycle = new AbortController();
382
+ this.transport = new Transport({
383
+ accessId: this.config.accessId,
384
+ endpoint: (this.config.endpoint ?? DEFAULT_ENDPOINT).replace(/\/+$/, ""),
385
+ origin: this.origin,
386
+ fetch: this.rawFetch,
387
+ signal: this.lifecycle.signal,
388
+ requestTimeoutMs: 2e4
389
+ });
390
+ activeClient = this;
391
+ try {
392
+ if (this.config.captureConsole)
393
+ this.teardowns.push(installConsole(this.logs, this.errors));
394
+ if (this.config.captureNetwork !== false)
395
+ this.teardowns.push(installFetch(this.requests));
396
+ this.addBreadcrumb("app-state", AppState2.currentState ?? "unknown");
397
+ const subscription = AppState2.addEventListener(
398
+ "change",
399
+ (state) => this.addBreadcrumb("app-state", state)
400
+ );
401
+ this.teardowns.push(() => subscription.remove());
402
+ this.started = true;
403
+ } catch (error) {
404
+ this.stop();
405
+ throw error;
406
+ }
407
+ }
408
+ stop() {
409
+ this.lifecycle.abort();
410
+ for (const teardown of this.teardowns.reverse()) teardown();
411
+ this.teardowns = [];
412
+ this.started = false;
413
+ if (activeClient === this) activeClient = null;
414
+ this.logs.clear();
415
+ this.requests.clear();
416
+ this.errors.clear();
417
+ this.breadcrumbs.clear();
418
+ this.recording?.discard();
419
+ this.retryCapture = void 0;
420
+ }
421
+ async captureScreen() {
422
+ if (!this.started || !this.config.captureScreen) return null;
423
+ const route = this.route;
424
+ const signal = this.lifecycle.signal;
425
+ let expired = false;
426
+ let timeout;
427
+ let abort = () => {
428
+ };
429
+ const operation = this.config.captureScreen("screenshot").then((captured) => {
430
+ const image2 = captured ? { ...captured, uri: imageUri(captured.uri) } : null;
431
+ if (expired || signal.aborted || route !== this.route) {
432
+ if (image2) releaseImage(image2.uri);
433
+ return null;
434
+ }
435
+ return image2;
436
+ });
437
+ let image;
438
+ try {
439
+ image = await Promise.race([
440
+ operation,
441
+ new Promise((_resolve, reject) => {
442
+ abort = () => reject(new Error("Capture session ended"));
443
+ signal.addEventListener("abort", abort);
444
+ if (signal.aborted) abort();
445
+ timeout = setTimeout(
446
+ () => reject(new Error("Screen capture timed out")),
447
+ 5e3
448
+ );
449
+ })
450
+ ]);
451
+ } finally {
452
+ expired = true;
453
+ if (timeout) clearTimeout(timeout);
454
+ signal.removeEventListener("abort", abort);
455
+ }
456
+ if (signal.aborted || !this.started || activeClient !== this || route !== this.route)
457
+ return null;
458
+ if (image) validateImage(image);
459
+ return image;
460
+ }
461
+ startRecording() {
462
+ if (!this.started || this.submitting || !this.recording)
463
+ throw new Error("Screen recording is unavailable");
464
+ this.retryCapture = void 0;
465
+ this.recording.start();
466
+ }
467
+ async stopRecording() {
468
+ return this.recording?.stop() ?? [];
469
+ }
470
+ discardDraft() {
471
+ this.retryCapture = void 0;
472
+ this.recording?.discard();
473
+ }
474
+ discardRecording() {
475
+ this.recording?.discard();
476
+ if (this.retryCapture) this.retryCapture.replay = [];
477
+ }
478
+ getRecording() {
479
+ const state = this.recording?.snapshot() ?? {
480
+ active: false,
481
+ frameCount: 0,
482
+ durationMs: 0,
483
+ error: void 0
484
+ };
485
+ const frames = this.retryCapture?.replay;
486
+ return frames?.length && !state.active && !state.frameCount ? {
487
+ ...state,
488
+ frameCount: frames.length,
489
+ durationMs: frames[frames.length - 1].timestamp - frames[0].timestamp
490
+ } : state;
491
+ }
492
+ getContext() {
493
+ return collectNativeEnv(this.origin, this.route, this.language);
494
+ }
495
+ setRoute(route, language) {
496
+ const pathname = route.split(/[?#]/, 1)[0] ?? "/";
497
+ const next = pathname.startsWith("/") && !pathname.startsWith("//") ? pathname : "/";
498
+ if (next !== this.route) this.addBreadcrumb("navigation", next);
499
+ this.route = next;
500
+ this.language = language;
501
+ }
502
+ setApp(app) {
503
+ this.app = {
504
+ ...this.app,
505
+ ...app,
506
+ extra: { ...this.app.extra, ...app.extra }
507
+ };
508
+ }
509
+ captureError(error, componentStack) {
510
+ if (!this.started) return;
511
+ this.errors.push({
512
+ message: error.message.slice(0, 2e3),
513
+ stack: [error.stack, componentStack].filter(Boolean).join("\n").slice(0, 8e3),
514
+ source: "react-boundary",
515
+ timestamp: Date.now()
516
+ });
517
+ }
518
+ addBreadcrumb(type, value) {
519
+ this.breadcrumbs.push({
520
+ type,
521
+ value: value.slice(0, 300),
522
+ timestamp: Date.now()
523
+ });
524
+ }
525
+ async submitReport(input = {}) {
526
+ if (!this.started) throw new Error("[lens] Capture is disabled");
527
+ if (this.submitting)
528
+ throw new Error("[lens] A report is already being submitted");
529
+ this.submitting = true;
530
+ const transport = this.transport;
531
+ const signal = this.lifecycle.signal;
532
+ try {
533
+ const include = input.include ?? {};
534
+ const frames = await this.stopRecording();
535
+ const redacted = input.annotations?.some(
536
+ (annotation) => annotation.kind === "blur"
537
+ );
538
+ let capture = {
539
+ console: include.console === false ? [] : this.logs.drain(),
540
+ network: include.network === false ? [] : this.requests.drain(),
541
+ screenshot: include.screenshot === false ? void 0 : input.screenshot ?? await this.captureScreen() ?? void 0,
542
+ replay: include.replay === false || redacted ? [] : frames,
543
+ metadata: {
544
+ comment: input.comment?.slice(0, 2e3),
545
+ annotations: input.annotations?.slice(0, 100) ?? [],
546
+ target: input.target,
547
+ env: collectNativeEnv(this.origin, this.route, this.language),
548
+ app: {
549
+ ...this.app,
550
+ extra: {
551
+ ...this.app.extra,
552
+ ...nativeDetails(),
553
+ breadcrumbs: this.breadcrumbs.drain(),
554
+ nativeReplay: {
555
+ format: "visual-frames",
556
+ frameCount: include.replay === false || redacted ? 0 : frames.length,
557
+ sampledFramesPerSecond: 1
558
+ }
559
+ }
560
+ },
561
+ errors: include.errors === false ? [] : this.errors.drain().map((entry) => ({
562
+ ...entry,
563
+ message: entry.message.slice(0, 2e3),
564
+ stack: entry.stack?.slice(0, 8e3)
565
+ })),
566
+ consoleTail: [],
567
+ networkTail: [],
568
+ trigger: input.trigger ?? "api",
569
+ sdk: {
570
+ name: "@merittdev/horus-lens/react-native",
571
+ version: SDK_VERSION
572
+ }
573
+ }
574
+ };
575
+ if (this.retryCapture) {
576
+ const saved = this.retryCapture;
577
+ capture = {
578
+ ...capture,
579
+ console: include.console === false ? [] : saved.console,
580
+ network: include.network === false ? [] : saved.network,
581
+ replay: include.replay === false || redacted ? [] : saved.replay,
582
+ screenshot: include.screenshot === false ? void 0 : input.screenshot ?? saved.screenshot,
583
+ metadata: {
584
+ ...saved.metadata,
585
+ comment: input.comment?.slice(0, 2e3),
586
+ annotations: input.annotations?.slice(0, 100) ?? saved.metadata.annotations,
587
+ target: input.target,
588
+ errors: include.errors === false ? [] : saved.metadata.errors
589
+ }
590
+ };
591
+ }
592
+ this.logs.clear();
593
+ this.requests.clear();
594
+ this.errors.clear();
595
+ this.breadcrumbs.clear();
596
+ this.recording?.discard();
597
+ try {
598
+ if (this.config.beforeSend) {
599
+ const redacted2 = this.config.beforeSend(capture);
600
+ if (!redacted2)
601
+ throw new Error("[lens] Report cancelled by beforeSend");
602
+ capture = redacted2;
603
+ }
604
+ if (signal.aborted) throw new Error("Capture session ended");
605
+ } catch (error) {
606
+ this.retryCapture = void 0;
607
+ throw error;
608
+ }
609
+ this.retryCapture = capture;
610
+ capture.metadata.consoleTail = capture.console.slice(-50);
611
+ capture.metadata.networkTail = capture.network.slice(-30);
612
+ if (capture.metadata.app?.extra)
613
+ capture.metadata.app.extra.nativeReplay = {
614
+ format: "visual-frames",
615
+ frameCount: capture.replay.length,
616
+ sampledFramesPerSecond: 1
617
+ };
618
+ const payloads = [
619
+ ...capture.console.length ? [{ kind: "console", entries: capture.console }] : [],
620
+ ...capture.network.length ? [{ kind: "network", entries: capture.network }] : []
621
+ ].map(({ kind, entries }) => ({
622
+ kind,
623
+ contentType: "application/json",
624
+ blob: new Blob([JSON.stringify(entries)], { type: "application/json" })
625
+ }));
626
+ if (capture.screenshot) {
627
+ validateImage(capture.screenshot);
628
+ const response = await this.rawFetch(capture.screenshot.uri, {
629
+ signal
630
+ });
631
+ if (!response.ok)
632
+ throw new Error("Could not read the captured screenshot");
633
+ const blob = await response.blob();
634
+ if (blob.size > 8e6)
635
+ throw new Error("Captured screenshot is too large");
636
+ payloads.push({
637
+ kind: "screenshot",
638
+ contentType: capture.screenshot.uri.startsWith("data:image/png;") || /\.png$/i.test(capture.screenshot.uri) ? "image/png" : "image/jpeg",
639
+ blob
640
+ });
641
+ }
642
+ if (capture.replay.length) {
643
+ payloads.push({
644
+ kind: "replay",
645
+ contentType: "application/json",
646
+ blob: new Blob(
647
+ [JSON.stringify(encodeVisualReplay(capture.replay, this.origin))],
648
+ { type: "application/json" }
649
+ )
650
+ });
651
+ }
652
+ if (signal.aborted) throw new Error("Capture session ended");
653
+ const initiated = InitiateReportResponseSchema.parse(
654
+ await transport.initiate(
655
+ payloads.map(({ kind, contentType }) => ({ kind, contentType }))
656
+ )
657
+ );
658
+ const uploaded = [];
659
+ for (const payload of payloads) {
660
+ const upload = initiated.uploads.find(
661
+ (item) => item.kind === payload.kind
662
+ );
663
+ if (!upload)
664
+ throw new Error(`[lens] Missing upload slot for ${payload.kind}`);
665
+ const controller = new AbortController();
666
+ const timeout = setTimeout(() => controller.abort(), 2e4);
667
+ try {
668
+ await transport.uploadAsset(upload, payload.blob, controller.signal);
669
+ } finally {
670
+ clearTimeout(timeout);
671
+ }
672
+ uploaded.push({
673
+ kind: payload.kind,
674
+ key: upload.key,
675
+ byteSize: payload.blob.size
676
+ });
677
+ }
678
+ await transport.finalize(initiated.reportId, capture.metadata, uploaded);
679
+ this.retryCapture = void 0;
680
+ return { reportId: initiated.reportId };
681
+ } finally {
682
+ this.submitting = false;
683
+ }
684
+ }
685
+ };
686
+ var tools = [
687
+ ["rect", "Rectangle"],
688
+ ["ellipse", "Ellipse"],
689
+ ["arrow", "Arrow"],
690
+ ["line", "Line"],
691
+ ["pencil", "Draw"],
692
+ ["highlighter", "Highlight"],
693
+ ["text", "Text"],
694
+ ["step", "Step"],
695
+ ["blur", "Redact"],
696
+ ["crop", "Crop"],
697
+ ["target", "Target"]
698
+ ];
699
+ var colors = [
700
+ "#ef4444",
701
+ "#f59e0b",
702
+ "#22c55e",
703
+ "#3b82f6",
704
+ "#ffffff",
705
+ "#171717"
706
+ ];
707
+ function bounds(a, b) {
708
+ return {
709
+ x: Math.min(a.x, b.x),
710
+ y: Math.min(a.y, b.y),
711
+ width: Math.abs(b.x - a.x),
712
+ height: Math.abs(b.y - a.y)
713
+ };
714
+ }
715
+ function shape(tool, points, color, text, n) {
716
+ const a = points[0], b = points[points.length - 1];
717
+ if (!a || !b) return null;
718
+ const common = { color, strokeWidth: tool === "highlighter" ? 18 : 3 };
719
+ if (tool === "text")
720
+ return {
721
+ ...common,
722
+ kind: "text",
723
+ ...a,
724
+ text: text.trim().slice(0, 300) || "Note",
725
+ fontSize: 20
726
+ };
727
+ if (tool === "step") return { ...common, kind: "step", ...a, n };
728
+ if (tool === "pencil" || tool === "highlighter")
729
+ return points.length < 2 ? null : { ...common, kind: tool, points: [...points] };
730
+ if (tool === "arrow" || tool === "line")
731
+ return { ...common, kind: tool, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
732
+ if (tool === "rect" || tool === "ellipse" || tool === "blur")
733
+ return { ...common, kind: tool, ...bounds(a, b) };
734
+ return null;
735
+ }
736
+ function AnnotationShapes({
737
+ annotations
738
+ }) {
739
+ return /* @__PURE__ */ jsx(Fragment, { children: annotations.map((a, i) => {
740
+ const color = a.color ?? "#ef4444", stroke = a.strokeWidth ?? 3;
741
+ if (a.kind === "rect")
742
+ return /* @__PURE__ */ jsx(
743
+ Rect,
744
+ {
745
+ x: a.x,
746
+ y: a.y,
747
+ width: a.width,
748
+ height: a.height,
749
+ fill: "none",
750
+ stroke: color,
751
+ strokeWidth: stroke
752
+ },
753
+ i
754
+ );
755
+ if (a.kind === "ellipse")
756
+ return /* @__PURE__ */ jsx(
757
+ Ellipse,
758
+ {
759
+ cx: a.x + a.width / 2,
760
+ cy: a.y + a.height / 2,
761
+ rx: a.width / 2,
762
+ ry: a.height / 2,
763
+ fill: "none",
764
+ stroke: color,
765
+ strokeWidth: stroke
766
+ },
767
+ i
768
+ );
769
+ if (a.kind === "blur")
770
+ return /* @__PURE__ */ jsx(
771
+ Rect,
772
+ {
773
+ x: a.x,
774
+ y: a.y,
775
+ width: a.width,
776
+ height: a.height,
777
+ fill: "#171717"
778
+ },
779
+ i
780
+ );
781
+ if (a.kind === "line" || a.kind === "arrow") {
782
+ const angle = Math.atan2(a.y2 - a.y1, a.x2 - a.x1), length = 13;
783
+ const head = `${a.x2},${a.y2} ${a.x2 - length * Math.cos(angle - 0.5)},${a.y2 - length * Math.sin(angle - 0.5)} ${a.x2 - length * Math.cos(angle + 0.5)},${a.y2 - length * Math.sin(angle + 0.5)}`;
784
+ return /* @__PURE__ */ jsxs(G, { children: [
785
+ /* @__PURE__ */ jsx(
786
+ Line,
787
+ {
788
+ x1: a.x1,
789
+ y1: a.y1,
790
+ x2: a.x2,
791
+ y2: a.y2,
792
+ stroke: color,
793
+ strokeWidth: stroke,
794
+ strokeLinecap: "round"
795
+ }
796
+ ),
797
+ a.kind === "arrow" && /* @__PURE__ */ jsx(Polygon, { points: head, fill: color })
798
+ ] }, i);
799
+ }
800
+ if (a.kind === "pencil" || a.kind === "highlighter")
801
+ return /* @__PURE__ */ jsx(
802
+ Path,
803
+ {
804
+ d: a.points.map((p, index) => `${index ? "L" : "M"}${p.x} ${p.y}`).join(" "),
805
+ fill: "none",
806
+ stroke: color,
807
+ strokeWidth: stroke,
808
+ strokeOpacity: a.kind === "highlighter" ? 0.35 : 1,
809
+ strokeLinecap: "round",
810
+ strokeLinejoin: "round"
811
+ },
812
+ i
813
+ );
814
+ if (a.kind === "text")
815
+ return /* @__PURE__ */ jsx(
816
+ SvgText,
817
+ {
818
+ x: a.x,
819
+ y: a.y,
820
+ fill: color,
821
+ fontSize: a.fontSize ?? 20,
822
+ fontWeight: "600",
823
+ children: a.text
824
+ },
825
+ i
826
+ );
827
+ if (a.kind === "step")
828
+ return /* @__PURE__ */ jsxs(G, { children: [
829
+ /* @__PURE__ */ jsx(Circle, { cx: a.x, cy: a.y, r: 13, fill: color }),
830
+ /* @__PURE__ */ jsx(
831
+ SvgText,
832
+ {
833
+ x: a.x,
834
+ y: a.y + 5,
835
+ fill: "#fff",
836
+ fontSize: 15,
837
+ fontWeight: "700",
838
+ textAnchor: "middle",
839
+ children: a.n
840
+ }
841
+ )
842
+ ] }, i);
843
+ return null;
844
+ }) });
845
+ }
846
+ function AnnotationEditor({
847
+ image,
848
+ initial = [],
849
+ initialTarget,
850
+ initialCrop,
851
+ onDone,
852
+ onCancel
853
+ }) {
854
+ const [marks, setMarks] = useState(initial);
855
+ const [draft, setDraft] = useState(null);
856
+ const [tool, setTool] = useState("rect");
857
+ const [color, setColor] = useState(colors[0]);
858
+ const [text, setText] = useState("");
859
+ const [crop, setCrop] = useState(initialCrop ?? null);
860
+ const [target, setTarget] = useState(
861
+ initialTarget
862
+ );
863
+ const [exporting, setExporting] = useState(false);
864
+ const [error, setError] = useState("");
865
+ const canvas = useRef(null);
866
+ const mounted = useRef(true);
867
+ useEffect(() => {
868
+ mounted.current = true;
869
+ return () => {
870
+ mounted.current = false;
871
+ };
872
+ }, []);
873
+ const points = useRef([]);
874
+ const screen = useWindowDimensions();
875
+ const [stageSize, setStageSize] = useState({ width: 0, height: 0 });
876
+ const scale = Math.min(
877
+ Math.max(1, (stageSize.width || screen.width) - 32) / image.width,
878
+ Math.max(1, (stageSize.height || screen.height - 260) - 16) / image.height
879
+ );
880
+ const shownCrop = exporting ? crop : null;
881
+ const responder = useMemo(
882
+ () => PanResponder.create({
883
+ onStartShouldSetPanResponder: () => !exporting,
884
+ onMoveShouldSetPanResponder: () => !exporting,
885
+ onPanResponderGrant: (event) => {
886
+ const { locationX, locationY } = event.nativeEvent;
887
+ points.current = [
888
+ {
889
+ x: Math.max(0, Math.min(image.width, locationX / scale)),
890
+ y: Math.max(0, Math.min(image.height, locationY / scale))
891
+ }
892
+ ];
893
+ setDraft(
894
+ shape(
895
+ tool,
896
+ points.current,
897
+ color,
898
+ text,
899
+ marks.filter((a) => a.kind === "step").length + 1
900
+ )
901
+ );
902
+ },
903
+ onPanResponderMove: (event) => {
904
+ const { locationX, locationY } = event.nativeEvent;
905
+ const p = {
906
+ x: Math.max(0, Math.min(image.width, locationX / scale)),
907
+ y: Math.max(0, Math.min(image.height, locationY / scale))
908
+ };
909
+ if (tool === "pencil" || tool === "highlighter") {
910
+ if (points.current.length < 500) points.current.push(p);
911
+ } else points.current = [points.current[0], p];
912
+ setDraft(
913
+ shape(
914
+ tool,
915
+ points.current,
916
+ color,
917
+ text,
918
+ marks.filter((a) => a.kind === "step").length + 1
919
+ )
920
+ );
921
+ },
922
+ onPanResponderRelease: (event) => {
923
+ const first = points.current[0];
924
+ if (first && tool !== "text" && tool !== "step") {
925
+ const { locationX, locationY } = event.nativeEvent;
926
+ const endpoint = {
927
+ x: Math.max(0, Math.min(image.width, locationX / scale)),
928
+ y: Math.max(0, Math.min(image.height, locationY / scale))
929
+ };
930
+ points.current = tool === "pencil" || tool === "highlighter" ? [...points.current, endpoint].slice(0, 500) : [first, endpoint];
931
+ }
932
+ const a = points.current[0], b = points.current[points.current.length - 1];
933
+ if (a && b) {
934
+ if (tool === "crop") {
935
+ const rect = bounds(a, b);
936
+ if (rect.width > 8 && rect.height > 8) setCrop(rect);
937
+ } else if (tool === "target") {
938
+ const rect = bounds(a, b);
939
+ setTarget({
940
+ tagName: "Native screen region",
941
+ boundingRect: {
942
+ ...rect,
943
+ width: Math.max(24, rect.width),
944
+ height: Math.max(24, rect.height)
945
+ }
946
+ });
947
+ } else {
948
+ const next = shape(
949
+ tool,
950
+ points.current,
951
+ color,
952
+ text,
953
+ marks.filter((a2) => a2.kind === "step").length + 1
954
+ );
955
+ if (next && marks.length < 100) setMarks([...marks, next]);
956
+ }
957
+ }
958
+ setDraft(null);
959
+ points.current = [];
960
+ },
961
+ onPanResponderTerminate: () => {
962
+ setDraft(null);
963
+ points.current = [];
964
+ }
965
+ }),
966
+ [color, exporting, image.height, image.width, marks, scale, text, tool]
967
+ );
968
+ async function done() {
969
+ if (exporting) return;
970
+ setExporting(true);
971
+ setError("");
972
+ try {
973
+ await new Promise(
974
+ (resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
975
+ );
976
+ if (!mounted.current) return;
977
+ const uri = await captureRef(canvas, {
978
+ format: "png",
979
+ result: "tmpfile",
980
+ useRenderInContext: true
981
+ });
982
+ if (!mounted.current) {
983
+ releaseImage(imageUri(uri));
984
+ return;
985
+ }
986
+ onDone({
987
+ image: {
988
+ uri: imageUri(uri),
989
+ width: crop?.width ?? image.width,
990
+ height: crop?.height ?? image.height
991
+ },
992
+ annotations: marks,
993
+ target,
994
+ crop
995
+ });
996
+ } catch {
997
+ if (mounted.current)
998
+ setError("Could not save the annotated screenshot. Try again.");
999
+ } finally {
1000
+ if (mounted.current) setExporting(false);
1001
+ }
1002
+ }
1003
+ return /* @__PURE__ */ jsxs(View, { style: s.page, children: [
1004
+ /* @__PURE__ */ jsxs(View, { style: s.header, children: [
1005
+ /* @__PURE__ */ jsx(Text, { style: s.title, children: "Annotate screenshot" }),
1006
+ /* @__PURE__ */ jsx(
1007
+ Pressable,
1008
+ {
1009
+ accessibilityRole: "button",
1010
+ accessibilityLabel: "Cancel annotations",
1011
+ disabled: exporting,
1012
+ onPress: onCancel,
1013
+ children: /* @__PURE__ */ jsx(Text, { style: s.link, children: "Cancel" })
1014
+ }
1015
+ ),
1016
+ /* @__PURE__ */ jsx(
1017
+ Pressable,
1018
+ {
1019
+ accessibilityRole: "button",
1020
+ accessibilityLabel: "Save annotations",
1021
+ disabled: exporting,
1022
+ onPress: () => {
1023
+ void done();
1024
+ },
1025
+ children: /* @__PURE__ */ jsx(Text, { style: s.link, children: exporting ? "Saving\u2026" : "Done" })
1026
+ }
1027
+ )
1028
+ ] }),
1029
+ /* @__PURE__ */ jsx(
1030
+ ScrollView,
1031
+ {
1032
+ horizontal: true,
1033
+ style: s.tools,
1034
+ contentContainerStyle: s.row,
1035
+ showsHorizontalScrollIndicator: false,
1036
+ children: tools.map(([value, label]) => /* @__PURE__ */ jsx(
1037
+ Pressable,
1038
+ {
1039
+ accessibilityRole: "button",
1040
+ accessibilityLabel: `Annotation tool: ${label}`,
1041
+ accessibilityState: { selected: tool === value },
1042
+ disabled: exporting,
1043
+ onPress: () => setTool(value),
1044
+ style: [s.tool, tool === value && s.selected],
1045
+ children: /* @__PURE__ */ jsx(Text, { style: tool === value ? s.selectedText : s.toolText, children: label })
1046
+ },
1047
+ value
1048
+ ))
1049
+ }
1050
+ ),
1051
+ /* @__PURE__ */ jsxs(View, { style: s.row, children: [
1052
+ colors.map((value) => /* @__PURE__ */ jsx(
1053
+ Pressable,
1054
+ {
1055
+ accessibilityRole: "button",
1056
+ accessibilityLabel: `Annotation color ${value}`,
1057
+ disabled: exporting,
1058
+ onPress: () => setColor(value),
1059
+ style: [
1060
+ s.swatch,
1061
+ { backgroundColor: value, borderWidth: color === value ? 3 : 1 }
1062
+ ]
1063
+ },
1064
+ value
1065
+ )),
1066
+ /* @__PURE__ */ jsx(
1067
+ Pressable,
1068
+ {
1069
+ accessibilityRole: "button",
1070
+ accessibilityLabel: "Undo annotation",
1071
+ disabled: exporting,
1072
+ onPress: () => {
1073
+ if (crop) setCrop(null);
1074
+ else setMarks(marks.slice(0, -1));
1075
+ },
1076
+ children: /* @__PURE__ */ jsx(Text, { style: s.link, children: "Undo" })
1077
+ }
1078
+ ),
1079
+ /* @__PURE__ */ jsx(
1080
+ Pressable,
1081
+ {
1082
+ accessibilityRole: "button",
1083
+ accessibilityLabel: "Clear annotations",
1084
+ disabled: exporting,
1085
+ onPress: () => {
1086
+ setMarks([]);
1087
+ setCrop(null);
1088
+ setTarget(void 0);
1089
+ },
1090
+ children: /* @__PURE__ */ jsx(Text, { style: s.link, children: "Clear" })
1091
+ }
1092
+ )
1093
+ ] }),
1094
+ tool === "text" && /* @__PURE__ */ jsx(
1095
+ TextInput,
1096
+ {
1097
+ accessibilityLabel: "Annotation text",
1098
+ placeholder: "Text for the annotation",
1099
+ value: text,
1100
+ onChangeText: setText,
1101
+ maxLength: 300,
1102
+ style: s.textInput
1103
+ }
1104
+ ),
1105
+ /* @__PURE__ */ jsx(
1106
+ View,
1107
+ {
1108
+ style: s.stage,
1109
+ onLayout: ({ nativeEvent: { layout } }) => {
1110
+ setStageSize(
1111
+ (current) => current.width === layout.width && current.height === layout.height ? current : { width: layout.width, height: layout.height }
1112
+ );
1113
+ },
1114
+ children: /* @__PURE__ */ jsxs(
1115
+ View,
1116
+ {
1117
+ ref: canvas,
1118
+ collapsable: false,
1119
+ style: {
1120
+ width: (shownCrop?.width ?? image.width) * scale,
1121
+ height: (shownCrop?.height ?? image.height) * scale,
1122
+ overflow: "hidden",
1123
+ backgroundColor: "#fff"
1124
+ },
1125
+ children: [
1126
+ /* @__PURE__ */ jsxs(
1127
+ View,
1128
+ {
1129
+ style: {
1130
+ position: "absolute",
1131
+ left: -(shownCrop?.x ?? 0) * scale,
1132
+ top: -(shownCrop?.y ?? 0) * scale,
1133
+ width: image.width * scale,
1134
+ height: image.height * scale
1135
+ },
1136
+ children: [
1137
+ /* @__PURE__ */ jsx(
1138
+ Image,
1139
+ {
1140
+ source: { uri: image.uri },
1141
+ style: {
1142
+ width: image.width * scale,
1143
+ height: image.height * scale
1144
+ },
1145
+ resizeMode: "stretch"
1146
+ }
1147
+ ),
1148
+ /* @__PURE__ */ jsxs(
1149
+ Svg,
1150
+ {
1151
+ pointerEvents: "none",
1152
+ style: StyleSheet.absoluteFill,
1153
+ width: image.width * scale,
1154
+ height: image.height * scale,
1155
+ viewBox: `0 0 ${image.width} ${image.height}`,
1156
+ children: [
1157
+ /* @__PURE__ */ jsx(
1158
+ AnnotationShapes,
1159
+ {
1160
+ annotations: [...marks, ...draft ? [draft] : []]
1161
+ }
1162
+ ),
1163
+ !exporting && crop && /* @__PURE__ */ jsx(
1164
+ Rect,
1165
+ {
1166
+ ...crop,
1167
+ fill: "none",
1168
+ stroke: "#3b82f6",
1169
+ strokeWidth: 2,
1170
+ strokeDasharray: "6 4"
1171
+ }
1172
+ ),
1173
+ target?.boundingRect && /* @__PURE__ */ jsx(
1174
+ Rect,
1175
+ {
1176
+ ...target.boundingRect,
1177
+ fill: "none",
1178
+ stroke: "#22c55e",
1179
+ strokeWidth: 2,
1180
+ strokeDasharray: "4 4"
1181
+ }
1182
+ )
1183
+ ]
1184
+ }
1185
+ )
1186
+ ]
1187
+ }
1188
+ ),
1189
+ !exporting && /* @__PURE__ */ jsx(
1190
+ View,
1191
+ {
1192
+ style: StyleSheet.absoluteFill,
1193
+ ...responder.panHandlers,
1194
+ accessibilityLabel: "Screenshot annotation canvas"
1195
+ }
1196
+ )
1197
+ ]
1198
+ }
1199
+ )
1200
+ }
1201
+ ),
1202
+ /* @__PURE__ */ jsx(Text, { style: s.hint, children: error || (tool === "target" ? "Mark the native screen region related to the issue." : tool === "blur" ? "Redaction permanently covers screenshot pixels and removes earlier recording frames." : "Draw on the screenshot. Changes are attached when you submit.") })
1203
+ ] });
1204
+ }
1205
+ var s = StyleSheet.create({
1206
+ page: {
1207
+ flex: 1,
1208
+ backgroundColor: "#f4f4f5",
1209
+ paddingTop: 24,
1210
+ paddingBottom: 20
1211
+ },
1212
+ header: {
1213
+ flexDirection: "row",
1214
+ alignItems: "center",
1215
+ gap: 16,
1216
+ paddingHorizontal: 16,
1217
+ paddingVertical: 12
1218
+ },
1219
+ title: {
1220
+ flex: 1,
1221
+ fontSize: 16,
1222
+ fontWeight: "600",
1223
+ color: "#171717",
1224
+ textAlign: "left"
1225
+ },
1226
+ link: { fontSize: 14, fontWeight: "600", color: "#171717" },
1227
+ tools: { flexGrow: 0, maxHeight: 48 },
1228
+ row: {
1229
+ flexDirection: "row",
1230
+ alignItems: "center",
1231
+ gap: 10,
1232
+ paddingHorizontal: 16,
1233
+ paddingVertical: 8
1234
+ },
1235
+ tool: {
1236
+ paddingHorizontal: 12,
1237
+ paddingVertical: 8,
1238
+ backgroundColor: "#fff",
1239
+ borderRadius: 8
1240
+ },
1241
+ selected: { backgroundColor: "#171717" },
1242
+ toolText: { fontSize: 13, color: "#171717" },
1243
+ selectedText: { fontSize: 13, color: "#fff" },
1244
+ swatch: { width: 24, height: 24, borderRadius: 12, borderColor: "#a1a1aa" },
1245
+ stage: {
1246
+ flex: 1,
1247
+ alignItems: "flex-start",
1248
+ justifyContent: "flex-start",
1249
+ paddingHorizontal: 16,
1250
+ paddingVertical: 8
1251
+ },
1252
+ hint: {
1253
+ paddingHorizontal: 16,
1254
+ fontSize: 12,
1255
+ lineHeight: 18,
1256
+ color: "#525252"
1257
+ },
1258
+ textInput: {
1259
+ marginHorizontal: 16,
1260
+ backgroundColor: "#fff",
1261
+ borderRadius: 8,
1262
+ padding: 12,
1263
+ color: "#171717"
1264
+ }
1265
+ });
1266
+ function ReportPanel({
1267
+ client,
1268
+ visible,
1269
+ close,
1270
+ screenshot,
1271
+ baseScreenshot,
1272
+ crop,
1273
+ annotations,
1274
+ target,
1275
+ updateScreenshot,
1276
+ record,
1277
+ changed,
1278
+ onSubmitted
1279
+ }) {
1280
+ const [comment, setComment] = useState2("");
1281
+ const [pending, setPending] = useState2(false);
1282
+ const [message, setMessage] = useState2("");
1283
+ const [reportId, setReportId] = useState2("");
1284
+ const [editing, setEditing] = useState2(false);
1285
+ const [include, setInclude] = useState2({
1286
+ screenshot: true,
1287
+ replay: true,
1288
+ console: true,
1289
+ network: true,
1290
+ errors: true
1291
+ });
1292
+ useEffect2(() => {
1293
+ if (!visible) setEditing(false);
1294
+ else setReportId("");
1295
+ }, [visible]);
1296
+ const env = client.getContext();
1297
+ const recording = client.getRecording();
1298
+ const redacted = annotations.some((a) => a.kind === "blur");
1299
+ async function submit() {
1300
+ if (!comment.trim() && !screenshot && !recording.frameCount || pending)
1301
+ return;
1302
+ setPending(true);
1303
+ setMessage("");
1304
+ try {
1305
+ const result = await client.submitReport({
1306
+ comment: comment.trim(),
1307
+ trigger: "user",
1308
+ include,
1309
+ screenshot: screenshot ?? void 0,
1310
+ annotations,
1311
+ target
1312
+ });
1313
+ setReportId(result.reportId);
1314
+ setComment("");
1315
+ onSubmitted();
1316
+ } catch {
1317
+ setMessage(
1318
+ "Could not submit the report. Check your connection and project settings, then retry."
1319
+ );
1320
+ } finally {
1321
+ setPending(false);
1322
+ }
1323
+ }
1324
+ return /* @__PURE__ */ jsx2(
1325
+ Modal,
1326
+ {
1327
+ visible,
1328
+ animationType: "slide",
1329
+ presentationStyle: "pageSheet",
1330
+ onRequestClose: close,
1331
+ children: editing && baseScreenshot ? /* @__PURE__ */ jsx2(
1332
+ AnnotationEditor,
1333
+ {
1334
+ image: baseScreenshot,
1335
+ initial: annotations,
1336
+ initialTarget: target,
1337
+ initialCrop: crop,
1338
+ onDone: (result) => {
1339
+ updateScreenshot(result);
1340
+ setEditing(false);
1341
+ },
1342
+ onCancel: () => setEditing(false)
1343
+ }
1344
+ ) : /* @__PURE__ */ jsx2(
1345
+ KeyboardAvoidingView,
1346
+ {
1347
+ style: styles.page,
1348
+ behavior: Platform3.OS === "ios" ? "padding" : void 0,
1349
+ children: /* @__PURE__ */ jsxs2(
1350
+ ScrollView2,
1351
+ {
1352
+ contentContainerStyle: styles.content,
1353
+ keyboardShouldPersistTaps: "handled",
1354
+ children: [
1355
+ /* @__PURE__ */ jsxs2(View2, { style: styles.header, children: [
1356
+ /* @__PURE__ */ jsx2(Text2, { accessibilityRole: "header", style: styles.title, children: "Report an issue" }),
1357
+ /* @__PURE__ */ jsx2(
1358
+ Pressable2,
1359
+ {
1360
+ accessibilityRole: "button",
1361
+ accessibilityLabel: "Close report panel",
1362
+ onPress: close,
1363
+ hitSlop: 12,
1364
+ children: /* @__PURE__ */ jsx2(Text2, { style: styles.link, children: "Close" })
1365
+ }
1366
+ )
1367
+ ] }),
1368
+ reportId ? /* @__PURE__ */ jsxs2(View2, { style: styles.success, children: [
1369
+ /* @__PURE__ */ jsx2(Text2, { style: styles.successMark, children: "\u2713" }),
1370
+ /* @__PURE__ */ jsx2(Text2, { style: styles.title, children: "Report submitted" }),
1371
+ /* @__PURE__ */ jsx2(Text2, { style: styles.description, children: "The team can see the full context." }),
1372
+ /* @__PURE__ */ jsx2(Text2, { selectable: true, style: styles.reportId, children: reportId }),
1373
+ /* @__PURE__ */ jsx2(
1374
+ Pressable2,
1375
+ {
1376
+ accessibilityRole: "button",
1377
+ accessibilityLabel: "Share report ID",
1378
+ style: [styles.secondary, styles.successAction],
1379
+ onPress: () => {
1380
+ void Share.share({ message: reportId });
1381
+ },
1382
+ children: /* @__PURE__ */ jsx2(Text2, { style: styles.link, children: "Share report ID" })
1383
+ }
1384
+ ),
1385
+ /* @__PURE__ */ jsx2(
1386
+ Pressable2,
1387
+ {
1388
+ accessibilityRole: "button",
1389
+ style: [styles.secondary, styles.successAction],
1390
+ onPress: () => {
1391
+ setReportId("");
1392
+ close();
1393
+ },
1394
+ children: /* @__PURE__ */ jsx2(Text2, { style: styles.link, children: "Done" })
1395
+ }
1396
+ )
1397
+ ] }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
1398
+ /* @__PURE__ */ jsx2(
1399
+ TextInput2,
1400
+ {
1401
+ accessibilityLabel: "Issue description",
1402
+ placeholder: "What's wrong? Describe the issue\u2026",
1403
+ multiline: true,
1404
+ maxLength: 2e3,
1405
+ editable: !pending,
1406
+ value: comment,
1407
+ onChangeText: (value) => {
1408
+ setComment(value);
1409
+ setMessage("");
1410
+ },
1411
+ style: styles.input,
1412
+ textAlignVertical: "top"
1413
+ }
1414
+ ),
1415
+ /* @__PURE__ */ jsxs2(Text2, { style: styles.context, children: [
1416
+ env.route,
1417
+ " \xB7 ",
1418
+ env.os,
1419
+ " \xB7 ",
1420
+ env.viewport.width,
1421
+ "\xD7",
1422
+ env.viewport.height
1423
+ ] }),
1424
+ screenshot && /* @__PURE__ */ jsxs2(
1425
+ Pressable2,
1426
+ {
1427
+ accessibilityRole: "button",
1428
+ accessibilityLabel: "Annotate captured screenshot",
1429
+ disabled: pending,
1430
+ onPress: () => setEditing(true),
1431
+ style: styles.preview,
1432
+ children: [
1433
+ /* @__PURE__ */ jsx2(
1434
+ Image2,
1435
+ {
1436
+ source: { uri: screenshot.uri },
1437
+ style: [
1438
+ styles.previewImage,
1439
+ {
1440
+ width: Math.min(
1441
+ 144,
1442
+ 160 * screenshot.width / screenshot.height
1443
+ )
1444
+ }
1445
+ ],
1446
+ resizeMode: "contain"
1447
+ }
1448
+ ),
1449
+ /* @__PURE__ */ jsxs2(View2, { style: styles.previewDetails, children: [
1450
+ /* @__PURE__ */ jsx2(Text2, { style: styles.previewTitle, children: "Screenshot" }),
1451
+ /* @__PURE__ */ jsx2(Text2, { style: styles.previewLabel, children: "Tap to annotate" })
1452
+ ] })
1453
+ ]
1454
+ }
1455
+ ),
1456
+ /* @__PURE__ */ jsxs2(View2, { style: styles.chips, children: [
1457
+ !!annotations.length && /* @__PURE__ */ jsxs2(Text2, { style: styles.chip, children: [
1458
+ annotations.length,
1459
+ " annotation",
1460
+ annotations.length === 1 ? "" : "s"
1461
+ ] }),
1462
+ target && /* @__PURE__ */ jsx2(
1463
+ Pressable2,
1464
+ {
1465
+ accessibilityRole: "button",
1466
+ accessibilityLabel: "Remove target region",
1467
+ disabled: pending,
1468
+ onPress: () => {
1469
+ if (screenshot)
1470
+ updateScreenshot({
1471
+ image: screenshot,
1472
+ annotations,
1473
+ crop
1474
+ });
1475
+ },
1476
+ children: /* @__PURE__ */ jsx2(Text2, { style: styles.chip, children: "Target region \xD7" })
1477
+ }
1478
+ ),
1479
+ !!recording.frameCount && /* @__PURE__ */ jsx2(
1480
+ Pressable2,
1481
+ {
1482
+ accessibilityRole: "button",
1483
+ accessibilityLabel: "Discard recording",
1484
+ disabled: pending,
1485
+ onPress: () => {
1486
+ client.discardRecording();
1487
+ changed();
1488
+ },
1489
+ children: /* @__PURE__ */ jsxs2(Text2, { style: styles.chip, children: [
1490
+ "Recording \xB7 ",
1491
+ Math.round(recording.durationMs / 1e3),
1492
+ "s \xB7",
1493
+ " ",
1494
+ recording.frameCount,
1495
+ " frames \xD7"
1496
+ ] })
1497
+ }
1498
+ )
1499
+ ] }),
1500
+ redacted && !!recording.frameCount && /* @__PURE__ */ jsx2(Text2, { style: styles.description, children: "Earlier recording frames will be excluded because the screenshot was redacted." }),
1501
+ !!recording.error && /* @__PURE__ */ jsx2(Text2, { accessibilityRole: "alert", style: styles.description, children: recording.error }),
1502
+ /* @__PURE__ */ jsx2(View2, { style: styles.chips, children: [
1503
+ "screenshot",
1504
+ "replay",
1505
+ "console",
1506
+ "network",
1507
+ "errors"
1508
+ ].map((key) => /* @__PURE__ */ jsx2(
1509
+ Pressable2,
1510
+ {
1511
+ accessibilityRole: "checkbox",
1512
+ accessibilityLabel: `Include ${key}`,
1513
+ accessibilityState: {
1514
+ checked: include[key] && !(key === "replay" && redacted),
1515
+ disabled: pending || key === "replay" && redacted
1516
+ },
1517
+ disabled: pending || key === "replay" && redacted,
1518
+ onPress: () => setInclude({ ...include, [key]: !include[key] }),
1519
+ style: [
1520
+ styles.toggle,
1521
+ include[key] && !(key === "replay" && redacted) && styles.toggleOn
1522
+ ],
1523
+ children: /* @__PURE__ */ jsxs2(
1524
+ Text2,
1525
+ {
1526
+ style: include[key] && !(key === "replay" && redacted) ? styles.toggleTextOn : styles.toggleText,
1527
+ children: [
1528
+ include[key] && !(key === "replay" && redacted) ? "\u2713 " : "",
1529
+ key === "replay" ? "Recording" : key === "console" ? "Logs" : key[0].toUpperCase() + key.slice(1)
1530
+ ]
1531
+ }
1532
+ )
1533
+ },
1534
+ key
1535
+ )) }),
1536
+ /* @__PURE__ */ jsxs2(View2, { style: styles.actions, children: [
1537
+ /* @__PURE__ */ jsx2(
1538
+ Pressable2,
1539
+ {
1540
+ accessibilityRole: "button",
1541
+ accessibilityLabel: "Annotate screenshot",
1542
+ disabled: !baseScreenshot || pending,
1543
+ onPress: () => setEditing(true),
1544
+ style: [
1545
+ styles.secondary,
1546
+ (!baseScreenshot || pending) && styles.disabled
1547
+ ],
1548
+ children: /* @__PURE__ */ jsx2(Text2, { style: styles.link, children: "Annotate" })
1549
+ }
1550
+ ),
1551
+ /* @__PURE__ */ jsx2(
1552
+ Pressable2,
1553
+ {
1554
+ accessibilityRole: "button",
1555
+ accessibilityLabel: "Record steps",
1556
+ disabled: pending || !baseScreenshot,
1557
+ onPress: record,
1558
+ style: [
1559
+ styles.secondary,
1560
+ (!baseScreenshot || pending) && styles.disabled
1561
+ ],
1562
+ children: /* @__PURE__ */ jsx2(Text2, { style: styles.link, children: "Record steps" })
1563
+ }
1564
+ )
1565
+ ] }),
1566
+ !!message && /* @__PURE__ */ jsx2(
1567
+ Text2,
1568
+ {
1569
+ accessibilityRole: "alert",
1570
+ accessibilityLiveRegion: "polite",
1571
+ style: styles.description,
1572
+ children: message
1573
+ }
1574
+ ),
1575
+ /* @__PURE__ */ jsxs2(View2, { style: styles.actions, children: [
1576
+ /* @__PURE__ */ jsx2(
1577
+ Pressable2,
1578
+ {
1579
+ accessibilityRole: "button",
1580
+ disabled: pending,
1581
+ onPress: close,
1582
+ style: styles.secondary,
1583
+ children: /* @__PURE__ */ jsx2(Text2, { style: styles.link, children: "Cancel" })
1584
+ }
1585
+ ),
1586
+ /* @__PURE__ */ jsx2(
1587
+ Pressable2,
1588
+ {
1589
+ accessibilityRole: "button",
1590
+ accessibilityLabel: "Submit report",
1591
+ accessibilityState: {
1592
+ disabled: pending || !comment.trim() && !screenshot && !recording.frameCount,
1593
+ busy: pending
1594
+ },
1595
+ disabled: pending || !comment.trim() && !screenshot && !recording.frameCount,
1596
+ onPress: () => {
1597
+ void submit();
1598
+ },
1599
+ style: [styles.button, pending && styles.disabled],
1600
+ children: pending ? /* @__PURE__ */ jsx2(ActivityIndicator, { color: "#fff" }) : /* @__PURE__ */ jsx2(Text2, { style: styles.buttonText, children: "Submit" })
1601
+ }
1602
+ )
1603
+ ] })
1604
+ ] })
1605
+ ]
1606
+ }
1607
+ )
1608
+ }
1609
+ )
1610
+ }
1611
+ );
1612
+ }
1613
+ var styles = StyleSheet2.create({
1614
+ page: { flex: 1, backgroundColor: "#fff" },
1615
+ content: {
1616
+ padding: 24,
1617
+ paddingTop: 32,
1618
+ paddingBottom: 48,
1619
+ gap: 16,
1620
+ alignItems: "stretch"
1621
+ },
1622
+ header: {
1623
+ flexDirection: "row",
1624
+ alignItems: "center",
1625
+ justifyContent: "space-between",
1626
+ gap: 16
1627
+ },
1628
+ title: { fontSize: 20, fontWeight: "600", color: "#171717", flexShrink: 1 },
1629
+ description: { fontSize: 13, lineHeight: 20, color: "#525252" },
1630
+ context: { fontSize: 12, color: "#71717a" },
1631
+ input: {
1632
+ minHeight: 100,
1633
+ borderWidth: 1,
1634
+ borderColor: "#d4d4d4",
1635
+ borderRadius: 12,
1636
+ padding: 14,
1637
+ fontSize: 16,
1638
+ color: "#171717"
1639
+ },
1640
+ button: {
1641
+ flex: 1,
1642
+ backgroundColor: "#171717",
1643
+ padding: 14,
1644
+ borderRadius: 10,
1645
+ alignItems: "center",
1646
+ justifyContent: "center"
1647
+ },
1648
+ buttonText: { color: "#fff", fontSize: 14, fontWeight: "600" },
1649
+ disabled: { opacity: 0.5 },
1650
+ link: { color: "#171717", fontSize: 14, fontWeight: "500" },
1651
+ launcher: {
1652
+ position: "absolute",
1653
+ end: 16,
1654
+ bottom: 82,
1655
+ width: 48,
1656
+ height: 48,
1657
+ alignItems: "center",
1658
+ justifyContent: "center",
1659
+ borderRadius: 24,
1660
+ backgroundColor: "#171717",
1661
+ zIndex: 9999,
1662
+ elevation: 8
1663
+ },
1664
+ recording: {
1665
+ position: "absolute",
1666
+ end: 16,
1667
+ bottom: 82,
1668
+ flexDirection: "row",
1669
+ alignItems: "center",
1670
+ gap: 10,
1671
+ padding: 14,
1672
+ borderRadius: 24,
1673
+ backgroundColor: "#171717",
1674
+ zIndex: 9999,
1675
+ elevation: 8
1676
+ },
1677
+ preview: {
1678
+ borderWidth: 1,
1679
+ borderColor: "#e4e4e7",
1680
+ backgroundColor: "#fafafa",
1681
+ borderRadius: 10,
1682
+ overflow: "hidden",
1683
+ flexDirection: "row",
1684
+ alignItems: "flex-start",
1685
+ padding: 12,
1686
+ gap: 16
1687
+ },
1688
+ previewImage: { height: 160, borderRadius: 4 },
1689
+ previewDetails: { flex: 1, gap: 6, paddingTop: 4 },
1690
+ previewTitle: {
1691
+ fontSize: 14,
1692
+ fontWeight: "600",
1693
+ color: "#171717",
1694
+ textAlign: "left"
1695
+ },
1696
+ previewLabel: {
1697
+ fontSize: 12,
1698
+ color: "#525252",
1699
+ textAlign: "left"
1700
+ },
1701
+ chips: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
1702
+ chip: {
1703
+ fontSize: 12,
1704
+ color: "#525252",
1705
+ backgroundColor: "#f4f4f5",
1706
+ padding: 8,
1707
+ borderRadius: 8
1708
+ },
1709
+ actions: { flexDirection: "row", gap: 10 },
1710
+ secondary: {
1711
+ flex: 1,
1712
+ padding: 14,
1713
+ alignItems: "center",
1714
+ justifyContent: "center",
1715
+ borderRadius: 10,
1716
+ backgroundColor: "#f4f4f5"
1717
+ },
1718
+ toggle: {
1719
+ paddingHorizontal: 10,
1720
+ paddingVertical: 8,
1721
+ borderRadius: 8,
1722
+ backgroundColor: "#f4f4f5"
1723
+ },
1724
+ toggleOn: { backgroundColor: "#e4e4e7" },
1725
+ toggleText: { color: "#71717a", fontSize: 12 },
1726
+ toggleTextOn: { color: "#171717", fontSize: 12 },
1727
+ success: { alignItems: "stretch", gap: 16, paddingVertical: 24 },
1728
+ successAction: { flex: 0 },
1729
+ successMark: { fontSize: 32, color: "#16a34a" },
1730
+ reportId: { color: "#525252", fontSize: 12 }
1731
+ });
1732
+ function LensIcon({
1733
+ color = "#ffffff",
1734
+ size = 24
1735
+ }) {
1736
+ return /* @__PURE__ */ jsxs3(Svg2, { width: size, height: size, viewBox: "0 0 24 24", accessible: false, children: [
1737
+ /* @__PURE__ */ jsx3(
1738
+ Circle2,
1739
+ {
1740
+ cx: "12",
1741
+ cy: "12",
1742
+ r: "9",
1743
+ stroke: color,
1744
+ strokeWidth: "1.7",
1745
+ fill: "none"
1746
+ }
1747
+ ),
1748
+ /* @__PURE__ */ jsx3(
1749
+ Circle2,
1750
+ {
1751
+ cx: "12",
1752
+ cy: "12",
1753
+ r: "5",
1754
+ stroke: color,
1755
+ strokeWidth: "1.7",
1756
+ fill: "none"
1757
+ }
1758
+ ),
1759
+ /* @__PURE__ */ jsx3(
1760
+ Path2,
1761
+ {
1762
+ d: "M9 10a3 3 0 0 1 3-2",
1763
+ stroke: color,
1764
+ strokeWidth: "1.7",
1765
+ strokeLinecap: "round",
1766
+ fill: "none"
1767
+ }
1768
+ )
1769
+ ] });
1770
+ }
1771
+ var LensContext = createContext({
1772
+ client: null,
1773
+ open: () => {
1774
+ },
1775
+ close: () => {
1776
+ }
1777
+ });
1778
+ function Lens(props) {
1779
+ const [client, setClient] = useState3(null);
1780
+ const [visible, setVisible] = useState3(false);
1781
+ const [opening, setOpening] = useState3(false);
1782
+ const [screenshot, setScreenshot] = useState3(null);
1783
+ const [baseScreenshot, setBaseScreenshot] = useState3(
1784
+ null
1785
+ );
1786
+ const [crop, setCrop] = useState3(null);
1787
+ const ownedImages = useRef2(/* @__PURE__ */ new Set());
1788
+ const releaseImages = useCallback(() => {
1789
+ for (const uri of ownedImages.current) releaseImage(uri);
1790
+ ownedImages.current.clear();
1791
+ }, []);
1792
+ const [annotations, setAnnotations] = useState3([]);
1793
+ const [target, setTarget] = useState3();
1794
+ const [recording, setRecording] = useState3({
1795
+ active: false,
1796
+ frameCount: 0,
1797
+ durationMs: 0,
1798
+ error: void 0
1799
+ });
1800
+ const beforeSend = useRef2(props.beforeSend), captureScreen = useRef2(props.captureScreen);
1801
+ beforeSend.current = props.beforeSend;
1802
+ captureScreen.current = props.captureScreen;
1803
+ const openGeneration = useRef2(0);
1804
+ const openingRef = useRef2(false);
1805
+ const stopping = useRef2(false);
1806
+ const draftRoute = useRef2(props.route ?? "/");
1807
+ const hasScreenCapture = !!props.captureScreen;
1808
+ const window = useWindowDimensions2();
1809
+ const windowRef = useRef2(window);
1810
+ windowRef.current = window;
1811
+ const positionRef = useRef2({ x: 0, y: 0 });
1812
+ const dragStart = useRef2({ x: 0, y: 0 });
1813
+ const [position, setPosition] = useState3({ x: 0, y: 0 });
1814
+ const openAction = useRef2(() => {
1815
+ });
1816
+ const dragPoint = useRef2({ x: 0, y: 0 });
1817
+ const drag = useMemo2(() => {
1818
+ const move = (pageX, pageY) => {
1819
+ const { width, height } = windowRef.current;
1820
+ const baseX = I18nManager2.isRTL ? 16 : width - 64;
1821
+ const dx = pageX - dragPoint.current.x, dy = pageY - dragPoint.current.y;
1822
+ const next = {
1823
+ x: Math.max(
1824
+ 16 - baseX,
1825
+ Math.min(width - 64 - baseX, dragStart.current.x + dx)
1826
+ ),
1827
+ y: Math.max(
1828
+ 48 - (height - 130),
1829
+ Math.min(34, dragStart.current.y + dy)
1830
+ )
1831
+ };
1832
+ positionRef.current = next;
1833
+ setPosition(next);
1834
+ return Math.abs(dx) + Math.abs(dy);
1835
+ };
1836
+ return PanResponder2.create({
1837
+ onStartShouldSetPanResponder: () => !openingRef.current,
1838
+ onPanResponderGrant: (event) => {
1839
+ dragStart.current = positionRef.current;
1840
+ dragPoint.current = {
1841
+ x: event.nativeEvent.pageX,
1842
+ y: event.nativeEvent.pageY
1843
+ };
1844
+ },
1845
+ onPanResponderMove: (event) => {
1846
+ move(event.nativeEvent.pageX, event.nativeEvent.pageY);
1847
+ },
1848
+ onPanResponderRelease: (event) => {
1849
+ const distance = move(event.nativeEvent.pageX, event.nativeEvent.pageY);
1850
+ if (distance <= 8) openAction.current();
1851
+ }
1852
+ });
1853
+ }, []);
1854
+ useEffect3(() => {
1855
+ openGeneration.current++;
1856
+ openingRef.current = false;
1857
+ releaseImages();
1858
+ setVisible(false);
1859
+ setOpening(false);
1860
+ setScreenshot(null);
1861
+ setBaseScreenshot(null);
1862
+ setCrop(null);
1863
+ setAnnotations([]);
1864
+ setTarget(void 0);
1865
+ if (!props.enabled) {
1866
+ setClient(null);
1867
+ return;
1868
+ }
1869
+ let disposed = false;
1870
+ const instance = new NativeLensClient({
1871
+ accessId: props.accessId,
1872
+ origin: props.origin,
1873
+ endpoint: props.endpoint,
1874
+ captureConsole: props.captureConsole,
1875
+ captureNetwork: props.captureNetwork,
1876
+ beforeSend: (capture) => beforeSend.current ? beforeSend.current(capture) : capture,
1877
+ captureScreen: hasScreenCapture ? async (purpose) => {
1878
+ const captured = await captureScreen.current?.(purpose) ?? null;
1879
+ const image = captured ? { ...captured, uri: imageUri(captured.uri) } : null;
1880
+ if (disposed) {
1881
+ if (image?.uri.startsWith("file:"))
1882
+ releaseImage(imageUri(image.uri));
1883
+ return null;
1884
+ }
1885
+ if (image?.uri.startsWith("file:"))
1886
+ ownedImages.current.add(image.uri);
1887
+ return image;
1888
+ } : void 0
1889
+ });
1890
+ instance.start();
1891
+ setClient(instance);
1892
+ return () => {
1893
+ openGeneration.current++;
1894
+ disposed = true;
1895
+ instance.stop();
1896
+ releaseImages();
1897
+ };
1898
+ }, [
1899
+ props.enabled,
1900
+ props.accessId,
1901
+ props.origin,
1902
+ props.endpoint,
1903
+ props.captureConsole,
1904
+ props.captureNetwork,
1905
+ hasScreenCapture,
1906
+ releaseImages
1907
+ ]);
1908
+ useEffect3(() => {
1909
+ const route = props.route ?? "/";
1910
+ if (route !== draftRoute.current && !client?.getRecording().active) {
1911
+ releaseImages();
1912
+ setScreenshot(null);
1913
+ setBaseScreenshot(null);
1914
+ setCrop(null);
1915
+ setAnnotations([]);
1916
+ setTarget(void 0);
1917
+ client?.discardDraft();
1918
+ }
1919
+ draftRoute.current = route;
1920
+ client?.setRoute(route, props.language);
1921
+ }, [client, props.route, props.language, releaseImages]);
1922
+ useEffect3(() => {
1923
+ if (props.app) client?.setApp(props.app);
1924
+ }, [client, props.app]);
1925
+ useEffect3(() => {
1926
+ if (!client) return;
1927
+ let wasActive = false;
1928
+ const timer = setInterval(() => {
1929
+ const state = client.getRecording();
1930
+ if (state.active || wasActive) setRecording(state);
1931
+ if (wasActive && !state.active && !stopping.current) setVisible(true);
1932
+ wasActive = state.active;
1933
+ }, 250);
1934
+ return () => clearInterval(timer);
1935
+ }, [client]);
1936
+ const close = useCallback(() => {
1937
+ openGeneration.current++;
1938
+ openingRef.current = false;
1939
+ setOpening(false);
1940
+ setVisible(false);
1941
+ }, []);
1942
+ const open = useCallback(() => {
1943
+ if (!client || openingRef.current || client.getRecording().active) return;
1944
+ openingRef.current = true;
1945
+ const generation = ++openGeneration.current;
1946
+ setOpening(true);
1947
+ void (async () => {
1948
+ let image = null;
1949
+ try {
1950
+ if (!baseScreenshot) image = await client.captureScreen();
1951
+ } catch (error) {
1952
+ console.warn(
1953
+ "[issue capture] Screenshot unavailable",
1954
+ error instanceof Error ? error.message : String(error)
1955
+ );
1956
+ }
1957
+ if (generation !== openGeneration.current) return;
1958
+ if (!baseScreenshot) {
1959
+ setBaseScreenshot(image);
1960
+ setScreenshot(image);
1961
+ }
1962
+ openingRef.current = false;
1963
+ setOpening(false);
1964
+ setRecording(client.getRecording());
1965
+ setVisible(true);
1966
+ })();
1967
+ }, [baseScreenshot, client]);
1968
+ useEffect3(() => {
1969
+ openAction.current = open;
1970
+ }, [open]);
1971
+ const context = useMemo2(
1972
+ () => ({ client, open, close }),
1973
+ [client, open, close]
1974
+ );
1975
+ const active = props.enabled && client;
1976
+ const stop = () => {
1977
+ if (!client || stopping.current) return;
1978
+ stopping.current = true;
1979
+ void client.stopRecording().then(() => {
1980
+ setRecording(client.getRecording());
1981
+ setVisible(true);
1982
+ }).finally(() => {
1983
+ stopping.current = false;
1984
+ });
1985
+ };
1986
+ const clearDraft = () => {
1987
+ releaseImages();
1988
+ setScreenshot(null);
1989
+ setBaseScreenshot(null);
1990
+ setCrop(null);
1991
+ setAnnotations([]);
1992
+ setTarget(void 0);
1993
+ setRecording(client?.getRecording() ?? recording);
1994
+ };
1995
+ return /* @__PURE__ */ jsxs4(LensContext.Provider, { value: context, children: [
1996
+ props.children,
1997
+ active && /* @__PURE__ */ jsxs4(Fragment3, { children: [
1998
+ props.showButton !== false && (recording.active ? /* @__PURE__ */ jsxs4(View3, { style: styles.recording, children: [
1999
+ /* @__PURE__ */ jsxs4(Text3, { style: styles.buttonText, children: [
2000
+ "\u25CF Recording ",
2001
+ Math.floor(recording.durationMs / 1e3),
2002
+ "s"
2003
+ ] }),
2004
+ /* @__PURE__ */ jsx4(
2005
+ Pressable3,
2006
+ {
2007
+ accessibilityRole: "button",
2008
+ accessibilityLabel: "Stop recording",
2009
+ onPress: stop,
2010
+ children: /* @__PURE__ */ jsx4(Text3, { style: styles.buttonText, children: "Stop" })
2011
+ }
2012
+ )
2013
+ ] }) : /* @__PURE__ */ jsx4(
2014
+ View3,
2015
+ {
2016
+ style: [
2017
+ styles.launcher,
2018
+ {
2019
+ transform: [
2020
+ { translateX: position.x },
2021
+ { translateY: position.y }
2022
+ ]
2023
+ }
2024
+ ],
2025
+ accessible: true,
2026
+ accessibilityRole: "button",
2027
+ accessibilityLabel: "Report an issue",
2028
+ accessibilityHint: "Drag to move",
2029
+ accessibilityState: { busy: opening },
2030
+ onAccessibilityTap: open,
2031
+ ...drag.panHandlers,
2032
+ children: /* @__PURE__ */ jsx4(
2033
+ View3,
2034
+ {
2035
+ style: {
2036
+ width: 48,
2037
+ height: 48,
2038
+ alignItems: "center",
2039
+ justifyContent: "center"
2040
+ },
2041
+ children: opening ? /* @__PURE__ */ jsx4(ActivityIndicator2, { color: "#fff" }) : /* @__PURE__ */ jsx4(LensIcon, {})
2042
+ }
2043
+ )
2044
+ }
2045
+ )),
2046
+ /* @__PURE__ */ jsx4(
2047
+ ReportPanel,
2048
+ {
2049
+ client,
2050
+ visible,
2051
+ close,
2052
+ screenshot,
2053
+ baseScreenshot,
2054
+ crop,
2055
+ annotations,
2056
+ target,
2057
+ updateScreenshot: (result) => {
2058
+ if (result.image.uri.startsWith("file:"))
2059
+ ownedImages.current.add(result.image.uri);
2060
+ if (screenshot?.uri.startsWith("file:") && screenshot.uri !== baseScreenshot?.uri && screenshot.uri !== result.image.uri) {
2061
+ releaseImage(screenshot.uri);
2062
+ ownedImages.current.delete(screenshot.uri);
2063
+ }
2064
+ setCrop(result.crop);
2065
+ setScreenshot(result.image);
2066
+ setAnnotations(result.annotations);
2067
+ setTarget(result.target);
2068
+ },
2069
+ record: () => {
2070
+ client.startRecording();
2071
+ setRecording(client.getRecording());
2072
+ setVisible(false);
2073
+ },
2074
+ changed: () => setRecording(client.getRecording()),
2075
+ onSubmitted: clearDraft
2076
+ },
2077
+ `${props.accessId}:${props.origin}:${props.endpoint ?? ""}`
2078
+ )
2079
+ ] })
2080
+ ] });
2081
+ }
2082
+ var LensProvider = Lens;
2083
+ function useLens() {
2084
+ const { client, open, close } = useContext(LensContext);
2085
+ return {
2086
+ active: client !== null,
2087
+ open: () => {
2088
+ if (client) open();
2089
+ },
2090
+ close,
2091
+ identify: (user) => client?.setApp({ user }),
2092
+ setApp: (app) => client?.setApp(app),
2093
+ captureError: (error, componentStack) => client?.captureError(error, componentStack),
2094
+ record: () => client?.startRecording(),
2095
+ stopRecording: () => client?.stopRecording() ?? Promise.resolve([]),
2096
+ report: async (input) => client ? client.submitReport({ ...input, trigger: input?.trigger ?? "api" }) : null
2097
+ };
2098
+ }
2099
+ export {
2100
+ Lens,
2101
+ LensProvider,
2102
+ NativeLensClient,
2103
+ useLens
2104
+ };
2105
+ //# sourceMappingURL=react-native.js.map