@reproapp/react-sdk 0.0.1

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.
package/dist/index.js ADDED
@@ -0,0 +1,1261 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { record } from "rrweb";
4
+ import { gzip } from 'pako';
5
+ // config
6
+ const MAX_BYTES = 900 * 1024; // 900 KB target per POST (tune)
7
+ const SESSION_MAX_MS = 60 * 1000; // cap each session at 1 minute
8
+ // estimate JSON bytes
9
+ function jsonBytes(obj) {
10
+ try {
11
+ return new TextEncoder().encode(JSON.stringify(obj)).length;
12
+ }
13
+ catch {
14
+ return Infinity;
15
+ }
16
+ }
17
+ // split [events] into <= MAX_BYTES chunks by bisection
18
+ function splitEventsBySize(events, mkEnvelope) {
19
+ const out = [];
20
+ const stack = [events.slice(0)]; // LIFO for fewer allocs
21
+ while (stack.length) {
22
+ const cur = stack.pop();
23
+ const env = mkEnvelope(cur);
24
+ if (jsonBytes(env) <= MAX_BYTES || cur.length <= 1) {
25
+ out.push(cur);
26
+ continue;
27
+ }
28
+ const mid = Math.floor(cur.length / 2);
29
+ stack.push(cur.slice(0, mid));
30
+ stack.push(cur.slice(mid));
31
+ }
32
+ return out;
33
+ }
34
+ // ---- small helpers ----
35
+ const now = () => Date.now();
36
+ const newAID = () => `A_${now()}_${Math.random().toString(36).slice(2, 6)}`;
37
+ // server time normalization
38
+ const offsetRef = { current: 0 }; // ms to add to client time
39
+ const nowServer = () => now() + (offsetRef.current || 0);
40
+ async function getJSON(url, headers) {
41
+ const r = await fetch(url, headers ? { headers } : undefined);
42
+ if (!r.ok)
43
+ throw new Error(`${r.status}`);
44
+ return (await r.json());
45
+ }
46
+ const INTERNAL_HEADER = "X-Repro-Internal";
47
+ const REQUEST_START_HEADER = "X-Bug-Request-Start";
48
+ const NGROK_SKIP_HEADER = "ngrok-skip-browser-warning";
49
+ const NGROK_SKIP_VALUE = "true";
50
+ const API_BASE = "https://oozy-loreta-gully.ngrok-free.dev";
51
+ let __reproCtx = null;
52
+ /** Manually attach Repro to any Axios instance (no recursion). */
53
+ export function attachAxios(axiosInstance) {
54
+ if (!axiosInstance || axiosInstance.__reproAttached)
55
+ return;
56
+ axiosInstance.__reproAttached = true;
57
+ axiosInstance.interceptors.request.use((config) => {
58
+ const ctx = __reproCtx;
59
+ if (!ctx)
60
+ return config;
61
+ const sid = ctx.getSid();
62
+ const aid = ctx.getAid();
63
+ const url = `${config.baseURL || ""}${config.url || ""}`;
64
+ const isInternal = url.startsWith(ctx.base);
65
+ const isSdkInternal = !!config.headers?.[INTERNAL_HEADER];
66
+ if (!config.headers)
67
+ config.headers = {};
68
+ const setHeader = (key, value) => {
69
+ if (!config.headers)
70
+ return;
71
+ if (typeof config.headers.set === "function") {
72
+ config.headers.set(key, value);
73
+ }
74
+ else {
75
+ config.headers[key] = value;
76
+ }
77
+ };
78
+ if (!isSdkInternal) {
79
+ const requestStart = nowServer();
80
+ setHeader(REQUEST_START_HEADER, String(requestStart));
81
+ }
82
+ const sdkToken = ctx.getToken();
83
+ const userToken = ctx.getUserToken();
84
+ if (isInternal) {
85
+ setHeader(NGROK_SKIP_HEADER, NGROK_SKIP_VALUE);
86
+ if (sdkToken && !config.headers["x-sdk-token"]) {
87
+ setHeader("x-sdk-token", sdkToken);
88
+ }
89
+ const existingAuth = config.headers?.Authorization ??
90
+ config.headers?.authorization;
91
+ if (userToken && !existingAuth) {
92
+ setHeader("Authorization", `Bearer ${userToken}`);
93
+ }
94
+ }
95
+ if (sid && aid && !isInternal && !isSdkInternal) {
96
+ setHeader("X-Bug-Session-Id", sid);
97
+ setHeader("X-Bug-Action-Id", aid);
98
+ }
99
+ return config;
100
+ });
101
+ axiosInstance.interceptors.response.use(async (resp) => {
102
+ const ctx = __reproCtx;
103
+ if (!ctx)
104
+ return resp;
105
+ const url = `${resp.config.baseURL || ""}${resp.config.url || ""}`;
106
+ const isInternal = url.startsWith(ctx.base);
107
+ const isSdkInternal = !!resp.config.headers?.[INTERNAL_HEADER];
108
+ if (isInternal && resp.status === 401) {
109
+ ctx.onUnauthorized?.();
110
+ }
111
+ const sid = ctx.getSid();
112
+ const aid = ctx.getAid();
113
+ if (!isInternal && !isSdkInternal && sid && aid && !ctx.hasReqMarked.has(aid)) {
114
+ ctx.hasReqMarked.add(aid);
115
+ }
116
+ return resp;
117
+ }, (err) => {
118
+ const ctx = __reproCtx;
119
+ if (ctx) {
120
+ const status = err?.response?.status;
121
+ const url = `${err?.config?.baseURL || ""}${err?.config?.url || ""}`;
122
+ if (status === 401 && url.startsWith(ctx.base)) {
123
+ ctx.onUnauthorized?.();
124
+ }
125
+ }
126
+ return Promise.reject(err);
127
+ });
128
+ }
129
+ export function ReproProvider({ appId, children, button, masking }) {
130
+ const base = API_BASE;
131
+ const storageKey = `repro-auth-${appId}`;
132
+ const initialAuth = (() => {
133
+ if (typeof window === "undefined")
134
+ return null;
135
+ try {
136
+ const raw = window.localStorage.getItem(storageKey);
137
+ if (!raw)
138
+ return null;
139
+ const parsed = JSON.parse(raw);
140
+ if (!parsed || typeof parsed !== "object")
141
+ return null;
142
+ const email = typeof parsed.email === "string" ? parsed.email : null;
143
+ const password = typeof parsed.password === "string" ? parsed.password : null;
144
+ const token = typeof parsed.token === "string" && parsed.token.trim().length
145
+ ? parsed.token.trim()
146
+ : null;
147
+ if (!email || !token)
148
+ return null;
149
+ return {
150
+ email,
151
+ password,
152
+ token,
153
+ data: parsed.data,
154
+ };
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ })();
160
+ const initialAuthRef = useRef(initialAuth);
161
+ // ---- refs & state (hooks MUST be inside component) ----
162
+ const sdkTokenRef = useRef(null);
163
+ const sessionIdRef = useRef(null);
164
+ const stopRecRef = useRef();
165
+ const rrwebEventsRef = useRef([]);
166
+ const currentAidRef = useRef(null);
167
+ const aidExpiryTimerRef = useRef(null);
168
+ const sessionExpiryTimerRef = useRef(null);
169
+ const origFetchRef = useRef();
170
+ const hasReqMarkedRef = useRef(new Set());
171
+ const lastActionLabelRef = useRef(null);
172
+ const actionMeta = useRef(new Map());
173
+ const nextSeqRef = useRef(1); // rrweb chunk counter
174
+ const isFlushingRef = useRef(false);
175
+ const backoffRef = useRef(0); // ms
176
+ const isStoppingRef = useRef(false);
177
+ // NEW: track installed click handler + dedupe recent clicks
178
+ const clickHandlerRef = useRef(null);
179
+ const lastClickRef = useRef(null);
180
+ const [ready, setReady] = useState(false);
181
+ const [recording, setRecording] = useState(false);
182
+ const recordingRef = useRef(false);
183
+ const [auth, setAuth] = useState(initialAuth);
184
+ const userPasswordRef = useRef(initialAuth?.password ?? null);
185
+ const userTokenRef = useRef(initialAuth?.token ?? null);
186
+ const [showLogin, setShowLogin] = useState(false);
187
+ const [loginEmail, setLoginEmail] = useState(initialAuth?.email ?? "");
188
+ const [loginPassword, setLoginPassword] = useState(initialAuth?.password ?? "");
189
+ const [loginError, setLoginError] = useState(null);
190
+ const [isLoggingIn, setIsLoggingIn] = useState(false);
191
+ const disableLogin = isLoggingIn || !loginEmail.trim() || !loginPassword.trim();
192
+ const [shareUrl, setShareUrl] = useState(null);
193
+ const [copyStatus, setCopyStatus] = useState("idle");
194
+ const copyStatusTimerRef = useRef(null);
195
+ const logoutInFlightRef = useRef(false);
196
+ const authCheckInFlightRef = useRef(false);
197
+ const lastAuthCheckTokenRef = useRef(null);
198
+ const [controlsHidden, setControlsHidden] = useState(false);
199
+ const [showHiddenNotice, setShowHiddenNotice] = useState(true);
200
+ const shortcutKeysRef = useRef(new Set());
201
+ const clearCopyStatusTimer = () => {
202
+ if (copyStatusTimerRef.current == null)
203
+ return;
204
+ if (typeof window !== "undefined") {
205
+ window.clearTimeout(copyStatusTimerRef.current);
206
+ }
207
+ else {
208
+ clearTimeout(copyStatusTimerRef.current);
209
+ }
210
+ copyStatusTimerRef.current = null;
211
+ };
212
+ const resetCopyFeedback = () => {
213
+ clearCopyStatusTimer();
214
+ setCopyStatus("idle");
215
+ };
216
+ const clearShareInfo = () => {
217
+ setShareUrl(null);
218
+ resetCopyFeedback();
219
+ };
220
+ const setCopyStatusWithTimeout = (status) => {
221
+ clearCopyStatusTimer();
222
+ setCopyStatus(status);
223
+ if (status === "idle")
224
+ return;
225
+ if (typeof window !== "undefined") {
226
+ copyStatusTimerRef.current = window.setTimeout(() => {
227
+ setCopyStatus("idle");
228
+ copyStatusTimerRef.current = null;
229
+ }, 2200);
230
+ }
231
+ };
232
+ const clearSessionExpiryTimer = () => {
233
+ if (sessionExpiryTimerRef.current == null)
234
+ return;
235
+ if (typeof window !== "undefined") {
236
+ window.clearTimeout(sessionExpiryTimerRef.current);
237
+ }
238
+ else {
239
+ clearTimeout(sessionExpiryTimerRef.current);
240
+ }
241
+ sessionExpiryTimerRef.current = null;
242
+ };
243
+ const addTenantHeader = (headers) => ({
244
+ ...headers,
245
+ [NGROK_SKIP_HEADER]: NGROK_SKIP_VALUE,
246
+ });
247
+ const setTenantOnHeaders = (headers, isInternal) => {
248
+ if (isInternal) {
249
+ headers.set(NGROK_SKIP_HEADER, NGROK_SKIP_VALUE);
250
+ }
251
+ };
252
+ useEffect(() => {
253
+ __reproCtx = {
254
+ base,
255
+ getSid: () => sessionIdRef.current,
256
+ getAid: () => currentAidRef.current,
257
+ getToken: () => sdkTokenRef.current,
258
+ getUserToken: () => userTokenRef.current,
259
+ getUserPassword: () => userPasswordRef.current,
260
+ getFetch: () => (origFetchRef.current ?? window.fetch.bind(window)),
261
+ hasReqMarked: hasReqMarkedRef.current,
262
+ onUnauthorized: () => handleUnauthorized(),
263
+ };
264
+ return () => {
265
+ __reproCtx = null;
266
+ };
267
+ }, [base]);
268
+ useEffect(() => {
269
+ const storedToken = initialAuthRef.current?.token ?? null;
270
+ if (!storedToken) {
271
+ lastAuthCheckTokenRef.current = null;
272
+ return;
273
+ }
274
+ if (!ready)
275
+ return;
276
+ const sdkToken = sdkTokenRef.current;
277
+ if (!sdkToken)
278
+ return;
279
+ if (auth?.token !== storedToken) {
280
+ return;
281
+ }
282
+ if (authCheckInFlightRef.current ||
283
+ lastAuthCheckTokenRef.current === storedToken) {
284
+ return;
285
+ }
286
+ let cancelled = false;
287
+ authCheckInFlightRef.current = true;
288
+ lastAuthCheckTokenRef.current = storedToken;
289
+ (async () => {
290
+ try {
291
+ const fetcher = origFetchRef.current ?? window.fetch.bind(window);
292
+ const resp = await fetcher(`${base}/v1/apps/${appId}/users/me`, {
293
+ method: "GET",
294
+ headers: addTenantHeader({
295
+ Accept: "application/json",
296
+ Authorization: `Bearer ${storedToken}`,
297
+ "x-sdk-token": sdkToken,
298
+ [INTERNAL_HEADER]: "1",
299
+ }),
300
+ });
301
+ if (cancelled)
302
+ return;
303
+ if (resp.ok)
304
+ return;
305
+ handleUnauthorizedStatus(resp.status);
306
+ }
307
+ catch {
308
+ if (!cancelled) {
309
+ // Avoid logging out on transient validation failures.
310
+ }
311
+ }
312
+ finally {
313
+ authCheckInFlightRef.current = false;
314
+ }
315
+ })();
316
+ return () => {
317
+ cancelled = true;
318
+ };
319
+ }, [auth, appId, base, ready]);
320
+ useEffect(() => {
321
+ userPasswordRef.current = auth?.password ?? null;
322
+ userTokenRef.current = auth?.token ?? null;
323
+ if (typeof window !== "undefined") {
324
+ try {
325
+ if (auth) {
326
+ window.localStorage.setItem(storageKey, JSON.stringify(auth));
327
+ }
328
+ else {
329
+ window.localStorage.removeItem(storageKey);
330
+ }
331
+ }
332
+ catch {
333
+ /* ignore storage failures */
334
+ }
335
+ }
336
+ }, [auth, storageKey]);
337
+ const requireAuth = () => {
338
+ if (!auth) {
339
+ setShowLogin(true);
340
+ return false;
341
+ }
342
+ return true;
343
+ };
344
+ async function login(email, password) {
345
+ setIsLoggingIn(true);
346
+ setLoginError(null);
347
+ try {
348
+ const resp = await fetch(`${base}/v1/apps/${appId}/users/login`, {
349
+ method: "POST",
350
+ headers: addTenantHeader({
351
+ Accept: "application/json",
352
+ "Content-Type": "application/json",
353
+ [INTERNAL_HEADER]: "1",
354
+ }),
355
+ body: JSON.stringify({ email, password }),
356
+ });
357
+ if (!resp.ok) {
358
+ throw new Error(`Login failed (${resp.status})`);
359
+ }
360
+ const data = await resp.json();
361
+ const accessTokenFromUser = typeof data?.user?.accessToken === "string"
362
+ ? data.user.accessToken.trim()
363
+ : null;
364
+ const accessTokenFromData = typeof data?.accessToken === "string"
365
+ ? data.accessToken.trim()
366
+ : null;
367
+ const accessToken = accessTokenFromUser || accessTokenFromData;
368
+ if (!accessToken) {
369
+ throw new Error("Login response did not include an access token.");
370
+ }
371
+ setAuth({ email, password, token: accessToken, data });
372
+ setLoginPassword("");
373
+ setShowLogin(false);
374
+ }
375
+ catch (err) {
376
+ setLoginError(err?.message || "Unable to login");
377
+ }
378
+ finally {
379
+ setIsLoggingIn(false);
380
+ }
381
+ }
382
+ function handleLoginSubmit(evt) {
383
+ evt.preventDefault();
384
+ if (disableLogin)
385
+ return;
386
+ login(loginEmail.trim(), loginPassword.trim());
387
+ }
388
+ async function logout(options) {
389
+ if (logoutInFlightRef.current)
390
+ return;
391
+ logoutInFlightRef.current = true;
392
+ try {
393
+ if (recording && !isStoppingRef.current) {
394
+ try {
395
+ await stop();
396
+ }
397
+ catch {
398
+ /* ignore */
399
+ }
400
+ }
401
+ setAuth(null);
402
+ setLoginPassword("");
403
+ clearShareInfo();
404
+ setShowLogin(options?.showLogin ?? false);
405
+ }
406
+ finally {
407
+ logoutInFlightRef.current = false;
408
+ }
409
+ }
410
+ function handleUnauthorized() {
411
+ void logout({ showLogin: true });
412
+ }
413
+ const handleUnauthorizedStatus = (status) => {
414
+ if (status === 401) {
415
+ handleUnauthorized();
416
+ return true;
417
+ }
418
+ return false;
419
+ };
420
+ useEffect(() => {
421
+ if (!showLogin) {
422
+ setLoginError(null);
423
+ }
424
+ }, [showLogin]);
425
+ useEffect(() => {
426
+ return () => {
427
+ clearCopyStatusTimer();
428
+ clearSessionExpiryTimer();
429
+ };
430
+ }, []);
431
+ useEffect(() => {
432
+ const pressed = shortcutKeysRef.current;
433
+ if (typeof window === "undefined")
434
+ return;
435
+ const handleKeyDown = (evt) => {
436
+ const hasMod = evt.ctrlKey || evt.metaKey;
437
+ if (!hasMod) {
438
+ pressed.clear();
439
+ return;
440
+ }
441
+ const key = (evt.key || "").toLowerCase();
442
+ if (key === "r" || key === "o") {
443
+ pressed.add(key);
444
+ const combo = pressed.has("r") && pressed.has("o");
445
+ if (combo) {
446
+ if (controlsHidden) {
447
+ evt.preventDefault();
448
+ }
449
+ setControlsHidden(false);
450
+ setShowHiddenNotice(false);
451
+ pressed.clear();
452
+ return;
453
+ }
454
+ if (controlsHidden) {
455
+ evt.preventDefault();
456
+ }
457
+ return;
458
+ }
459
+ if (key === "control" || key === "meta") {
460
+ pressed.clear();
461
+ return;
462
+ }
463
+ pressed.clear();
464
+ };
465
+ const handleKeyUp = (evt) => {
466
+ const key = (evt.key || "").toLowerCase();
467
+ if (key === "r" || key === "o") {
468
+ pressed.delete(key);
469
+ }
470
+ else if (key === "control" || key === "meta") {
471
+ pressed.clear();
472
+ }
473
+ };
474
+ window.addEventListener("keydown", handleKeyDown, true);
475
+ window.addEventListener("keyup", handleKeyUp, true);
476
+ return () => {
477
+ window.removeEventListener("keydown", handleKeyDown, true);
478
+ window.removeEventListener("keyup", handleKeyUp, true);
479
+ };
480
+ }, [controlsHidden]);
481
+ // ---- bootstrap once ----
482
+ useEffect(() => {
483
+ let mounted = true;
484
+ (async () => {
485
+ try {
486
+ const resp = await getJSON(`${base}/v1/sdk/bootstrap?appId=${encodeURIComponent(appId)}`, addTenantHeader({ [INTERNAL_HEADER]: "1" }));
487
+ if (mounted && resp.enabled && resp.sdkToken) {
488
+ sdkTokenRef.current = resp.sdkToken;
489
+ setReady(true);
490
+ }
491
+ else {
492
+ setReady(false);
493
+ }
494
+ }
495
+ catch {
496
+ setReady(false);
497
+ }
498
+ })();
499
+ return () => {
500
+ mounted = false;
501
+ };
502
+ }, [appId, base]);
503
+ const rrBufferRef = useRef([]);
504
+ const rrFlushTimerRef = useRef(null);
505
+ const CHUNK_SIZE = 80; // send when buffer hits 200 events
506
+ const FLUSH_MS = 1500; // or every 2s, whichever first
507
+ async function sendChunkGzip({ baseUrl, sid, token, envelope, seq }) {
508
+ try {
509
+ const json = JSON.stringify({ ...envelope, seq });
510
+ const gz = gzip(json); // Uint8Array
511
+ const r = await (origFetchRef.current ?? window.fetch)(`${baseUrl}/v1/sessions/${sid}/events`, {
512
+ method: "POST",
513
+ headers: addTenantHeader({
514
+ "Content-Type": "application/json",
515
+ "Content-Encoding": "gzip",
516
+ "x-sdk-token": token,
517
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
518
+ [INTERNAL_HEADER]: "1",
519
+ }),
520
+ body: gz,
521
+ });
522
+ if (handleUnauthorizedStatus(r.status))
523
+ return 'fail';
524
+ if (r.status === 413)
525
+ return 'too_large';
526
+ if (!r.ok)
527
+ return 'fail';
528
+ return 'ok';
529
+ }
530
+ catch {
531
+ return 'fail';
532
+ }
533
+ }
534
+ async function flushRrwebBuffer(reason) {
535
+ if (isFlushingRef.current)
536
+ return;
537
+ const sid = sessionIdRef.current;
538
+ const token = sdkTokenRef.current;
539
+ const baseUrl = API_BASE;
540
+ if (!sid || !token)
541
+ return;
542
+ if (!rrBufferRef.current.length)
543
+ return;
544
+ isFlushingRef.current = true;
545
+ try {
546
+ if (backoffRef.current > 0) {
547
+ await new Promise(r => setTimeout(r, backoffRef.current));
548
+ }
549
+ // COPY buffer; do not mutate until we know what succeeded
550
+ const fullSlice = rrBufferRef.current.slice(0);
551
+ const seq = nextSeqRef.current; // do NOT increment yet
552
+ const mkEnvelope = (slice) => {
553
+ const tFirst = slice[0]?.timestamp ?? nowServer();
554
+ const tLast = slice[slice.length - 1]?.timestamp ?? tFirst;
555
+ return { type: 'rrweb', seq, tFirst, tLast, events: slice };
556
+ };
557
+ // Pre-split by size into <= MAX_BYTES pieces (same seq for first, then seq+1, …)
558
+ const pieces = splitEventsBySize(fullSlice, mkEnvelope);
559
+ // Send each piece in order, incrementing seq only on success per piece
560
+ let sentCount = 0;
561
+ for (let i = 0; i < pieces.length; i++) {
562
+ const piece = pieces[i];
563
+ const pieceSeq = seq + i; // stable seq per piece
564
+ const env = mkEnvelope(piece);
565
+ const jsonSize = jsonBytes(env);
566
+ let res;
567
+ if (jsonSize > 64 * 1024) {
568
+ res = await sendChunkGzip({ baseUrl, sid, token, envelope: env, seq: pieceSeq });
569
+ }
570
+ else {
571
+ res = await sendChunk({ baseUrl, sid, token, envelope: env, seq: pieceSeq });
572
+ }
573
+ if (res === 'ok') {
574
+ sentCount += piece.length;
575
+ nextSeqRef.current = pieceSeq + 1; // advance to next expected seq
576
+ backoffRef.current = 0;
577
+ continue;
578
+ }
579
+ if (res === 'too_large' && piece.length > 1) {
580
+ // If a piece is STILL too large after pre-split, split it again and retry
581
+ const more = splitEventsBySize(piece, mkEnvelope);
582
+ // splice into 'pieces' at current position
583
+ pieces.splice(i, 1, ...more);
584
+ i -= 1; // reprocess at same index
585
+ continue;
586
+ }
587
+ // failure: stop; keep remaining events in buffer for retry
588
+ backoffRef.current = Math.min(4000, (backoffRef.current || 250) * 2);
589
+ break;
590
+ }
591
+ // success for `sentCount` events → drop them from buffer
592
+ if (sentCount > 0) {
593
+ rrBufferRef.current.splice(0, sentCount);
594
+ }
595
+ }
596
+ finally {
597
+ isFlushingRef.current = false;
598
+ }
599
+ }
600
+ async function sendChunk({ baseUrl, sid, token, envelope, seq }) {
601
+ try {
602
+ const r = await (origFetchRef.current ?? window.fetch)(`${baseUrl}/v1/sessions/${sid}/events`, {
603
+ method: "POST",
604
+ headers: addTenantHeader({
605
+ "Content-Type": "application/json",
606
+ "x-sdk-token": token,
607
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
608
+ [INTERNAL_HEADER]: "1",
609
+ }),
610
+ body: JSON.stringify({ ...envelope, seq }), // ensure seq matches piece
611
+ });
612
+ if (handleUnauthorizedStatus(r.status))
613
+ return 'fail';
614
+ if (r.status === 413)
615
+ return 'too_large';
616
+ if (!r.ok)
617
+ return 'fail';
618
+ return 'ok';
619
+ }
620
+ catch {
621
+ return 'fail';
622
+ }
623
+ }
624
+ /**
625
+ * Auto-attach to window.axios if present.
626
+ * If a team uses a custom Axios instance, they can call exported attachAxios(instance).
627
+ */
628
+ function attachAxiosIfPresent() {
629
+ const ax = window.axios;
630
+ if (!ax || ax.__reproAttached)
631
+ return;
632
+ ax.__reproAttached = true;
633
+ ax.interceptors.request.use((config) => {
634
+ const sid = sessionIdRef.current;
635
+ const aid = currentAidRef.current;
636
+ const url = `${config.baseURL || ""}${config.url || ""}`;
637
+ const isInternal = url.startsWith(base);
638
+ const isSdkInternal = config.headers?.[INTERNAL_HEADER] != null;
639
+ if (isInternal) {
640
+ config.headers = config.headers || {};
641
+ config.headers[NGROK_SKIP_HEADER] = NGROK_SKIP_VALUE;
642
+ }
643
+ if (sid && aid && !isInternal && !isSdkInternal) {
644
+ config.headers = config.headers || {};
645
+ config.headers["X-Bug-Session-Id"] = sid;
646
+ config.headers["X-Bug-Action-Id"] = aid;
647
+ }
648
+ return config;
649
+ });
650
+ ax.interceptors.response.use(async (resp) => {
651
+ const url = `${resp.config.baseURL || ""}${resp.config.url || ""}`;
652
+ const hdrs = resp.config.headers || {};
653
+ const isInternal = url.startsWith(base);
654
+ const isSdkInternal = hdrs[INTERNAL_HEADER] != null;
655
+ if (isInternal) {
656
+ handleUnauthorizedStatus(resp.status);
657
+ }
658
+ if (!isInternal &&
659
+ !isSdkInternal &&
660
+ sessionIdRef.current &&
661
+ currentAidRef.current &&
662
+ sdkTokenRef.current &&
663
+ !hasReqMarkedRef.current.has(currentAidRef.current)) {
664
+ hasReqMarkedRef.current.add(currentAidRef.current);
665
+ // use ORIGINAL fetch to avoid recursion
666
+ await origFetchRef.current(`${base}/v1/sessions/${sessionIdRef.current}/events`, {
667
+ method: "POST",
668
+ headers: addTenantHeader({
669
+ "Content-Type": "application/json",
670
+ "x-sdk-token": sdkTokenRef.current,
671
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
672
+ [INTERNAL_HEADER]: "1",
673
+ }),
674
+ body: JSON.stringify({
675
+ seq: nowServer(),
676
+ events: [
677
+ {
678
+ type: "action",
679
+ aid: currentAidRef.current,
680
+ label: lastActionLabelRef.current,
681
+ tStart: nowServer(),
682
+ tEnd: nowServer(),
683
+ hasReq: true,
684
+ ui: {},
685
+ },
686
+ ],
687
+ }),
688
+ }).catch(() => { });
689
+ }
690
+ return resp;
691
+ }, (err) => Promise.reject(err));
692
+ }
693
+ // ---- derive a label from a click target (best-effort, minimal) ----
694
+ function labelFromClickTarget(target) {
695
+ const el = target;
696
+ if (!el)
697
+ return "Click";
698
+ const txt = (el.innerText || el.getAttribute?.("aria-label") || "").trim().slice(0, 40) ||
699
+ el.getAttribute?.("title") ||
700
+ "";
701
+ const id = el.id ? `#${el.id}` : "";
702
+ const cls = el.className && typeof el.className === "string"
703
+ ? `.${el.className.split(" ").slice(0, 2).join(".")}`
704
+ : "";
705
+ return txt ? `Click • ${txt}` : `Click • ${(el.tagName || "el").toLowerCase()}${id || cls}`;
706
+ }
707
+ // ---- START recording ----
708
+ async function start() {
709
+ if (!sdkTokenRef.current || recordingRef.current)
710
+ return;
711
+ // 1) start session
712
+ if (!requireAuth())
713
+ return;
714
+ clearShareInfo();
715
+ const r = await fetch(`${base}/v1/sessions`, {
716
+ method: "POST",
717
+ headers: addTenantHeader({
718
+ "Content-Type": "application/json",
719
+ "x-sdk-token": sdkTokenRef.current,
720
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
721
+ }),
722
+ body: JSON.stringify({ clientTime: nowServer() }),
723
+ });
724
+ if (!r.ok) {
725
+ handleUnauthorizedStatus(r.status);
726
+ return;
727
+ }
728
+ const sess = (await r.json());
729
+ sessionIdRef.current = sess.sessionId; // <-- MISSING, add this
730
+ offsetRef.current = Number(sess.clockOffsetMs || 0);
731
+ // 2) hold original fetch & install interceptor
732
+ origFetchRef.current = window.fetch.bind(window);
733
+ nextSeqRef.current = 1; // reset for new session
734
+ rrBufferRef.current = [];
735
+ if (rrFlushTimerRef.current)
736
+ window.clearInterval(rrFlushTimerRef.current);
737
+ rrFlushTimerRef.current = window.setInterval(() => {
738
+ flushRrwebBuffer('timer');
739
+ }, FLUSH_MS);
740
+ window.fetch = async (input, init = {}) => {
741
+ // figure request url
742
+ const urlStr = typeof input === "string" || input instanceof URL
743
+ ? String(input)
744
+ : input.url;
745
+ // existing incoming headers (if any)
746
+ const hdrsIn = new Headers(init.headers || input?.headers || {});
747
+ const isInternal = urlStr.startsWith(base);
748
+ const isSdkInternal = hdrsIn.has(INTERNAL_HEADER) || hdrsIn.has(INTERNAL_HEADER.toLowerCase());
749
+ // inject bug headers for app requests only (not our API or SDK-internal posts)
750
+ const headers = new Headers(init.headers || {});
751
+ setTenantOnHeaders(headers, isInternal);
752
+ if (!isSdkInternal) {
753
+ const requestStart = nowServer();
754
+ headers.set(REQUEST_START_HEADER, String(requestStart));
755
+ }
756
+ if (sessionIdRef.current && currentAidRef.current && !isInternal && !isSdkInternal) {
757
+ headers.set("X-Bug-Session-Id", sessionIdRef.current);
758
+ headers.set("X-Bug-Action-Id", currentAidRef.current);
759
+ }
760
+ init.headers = headers;
761
+ // always call ORIGINAL fetch to avoid recursion
762
+ const res = await origFetchRef.current(input, init);
763
+ if (isInternal) {
764
+ handleUnauthorizedStatus(res.status);
765
+ }
766
+ // mark hasReq ONCE per action (skip internal/API calls)
767
+ if (!isInternal &&
768
+ !isSdkInternal &&
769
+ sessionIdRef.current &&
770
+ currentAidRef.current &&
771
+ !hasReqMarkedRef.current.has(currentAidRef.current)) {
772
+ hasReqMarkedRef.current.add(currentAidRef.current);
773
+ }
774
+ return res;
775
+ };
776
+ attachAxiosIfPresent();
777
+ // 3) click -> new ActionId + minimal action event (via ORIGINAL fetch)
778
+ // Remove any previous handler to avoid duplicates
779
+ if (clickHandlerRef.current) {
780
+ document.removeEventListener("click", clickHandlerRef.current, { capture: true });
781
+ clickHandlerRef.current = null;
782
+ }
783
+ const clickHandler = (evt) => {
784
+ if (!sessionIdRef.current || !sdkTokenRef.current)
785
+ return;
786
+ // Skip clicks on the SDK's own UI
787
+ const targetEl = evt.target;
788
+ if (targetEl && targetEl.closest('[data-repro-internal="1"]'))
789
+ return;
790
+ // Dedupe: ignore same-label clicks within 250ms (dev/StrictMode safety)
791
+ const label = labelFromClickTarget(targetEl);
792
+ const t = nowServer();
793
+ if (lastClickRef.current && t - lastClickRef.current.t < 250 && lastClickRef.current.label === label) {
794
+ return;
795
+ }
796
+ lastClickRef.current = { t, label };
797
+ const aid = newAID();
798
+ currentAidRef.current = aid;
799
+ lastActionLabelRef.current = label;
800
+ if (aidExpiryTimerRef.current)
801
+ window.clearTimeout(aidExpiryTimerRef.current);
802
+ aidExpiryTimerRef.current = window.setTimeout(() => {
803
+ currentAidRef.current = null;
804
+ }, 5000);
805
+ // post action row (internal; avoid recursion)
806
+ origFetchRef.current(`${base}/v1/sessions/${sessionIdRef.current}/events`, {
807
+ method: "POST",
808
+ headers: addTenantHeader({
809
+ "Content-Type": "application/json",
810
+ "x-sdk-token": sdkTokenRef.current,
811
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
812
+ [INTERNAL_HEADER]: "1",
813
+ }),
814
+ body: JSON.stringify({
815
+ seq: t,
816
+ events: [
817
+ {
818
+ type: "action",
819
+ aid,
820
+ label,
821
+ tStart: t,
822
+ tEnd: t,
823
+ hasReq: false,
824
+ hasDb: false,
825
+ error: false,
826
+ ui: { kind: "click" },
827
+ },
828
+ ],
829
+ }),
830
+ })
831
+ .then((resp) => {
832
+ handleUnauthorizedStatus(resp.status);
833
+ })
834
+ .catch(() => { });
835
+ };
836
+ clickHandlerRef.current = clickHandler;
837
+ document.addEventListener("click", clickHandler, { capture: true });
838
+ // 4) rrweb: keep events in memory; upload on stop()
839
+ stopRecRef.current = record({
840
+ emit: (ev) => {
841
+ rrBufferRef.current.push(ev);
842
+ // stream out if we reach CHUNK_SIZE
843
+ if (rrBufferRef.current.length >= CHUNK_SIZE) {
844
+ flushRrwebBuffer('size');
845
+ }
846
+ },
847
+ ...(masking ?? {}),
848
+ });
849
+ // 5) cleanup registration
850
+ stop._cleanup = () => {
851
+ if (clickHandlerRef.current) {
852
+ document.removeEventListener("click", clickHandlerRef.current, { capture: true });
853
+ clickHandlerRef.current = null;
854
+ }
855
+ };
856
+ clearSessionExpiryTimer();
857
+ sessionExpiryTimerRef.current = window.setTimeout(() => {
858
+ void stop();
859
+ }, SESSION_MAX_MS);
860
+ recordingRef.current = true;
861
+ setRecording(true);
862
+ }
863
+ // ---- STOP recording ----
864
+ async function stop() {
865
+ clearSessionExpiryTimer();
866
+ if (!recordingRef.current || isStoppingRef.current)
867
+ return;
868
+ isStoppingRef.current = true;
869
+ try {
870
+ // stop rrweb + listeners
871
+ stopRecRef.current?.();
872
+ stop._cleanup?.();
873
+ // clear periodic timer
874
+ if (rrFlushTimerRef.current) {
875
+ window.clearInterval(rrFlushTimerRef.current);
876
+ rrFlushTimerRef.current = null;
877
+ }
878
+ // final flush of whatever is left
879
+ await flushRrwebBuffer('stop');
880
+ const origFetch = origFetchRef.current ?? window.fetch.bind(window);
881
+ const sid = sessionIdRef.current;
882
+ const token = sdkTokenRef.current;
883
+ // 1) upload rrweb chunks (internal; use original fetch)
884
+ try {
885
+ const events = rrwebEventsRef.current;
886
+ const CHUNK = 500;
887
+ for (let i = 0; i < events.length; i += CHUNK) {
888
+ const slice = events.slice(i, i + CHUNK);
889
+ const seq = nextSeqRef.current++;
890
+ const tFirst = slice[0]?.timestamp ?? nowServer();
891
+ const tLast = slice[slice.length - 1]?.timestamp ?? tFirst;
892
+ const chunkRes = await origFetch(`${base}/v1/sessions/${sid}/events`, {
893
+ method: "POST",
894
+ headers: addTenantHeader({
895
+ "Content-Type": "application/json",
896
+ "x-sdk-token": token,
897
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
898
+ [INTERNAL_HEADER]: "1",
899
+ }),
900
+ body: JSON.stringify({
901
+ type: "rrweb",
902
+ seq,
903
+ tFirst,
904
+ tLast,
905
+ events: slice, // send raw array
906
+ }),
907
+ });
908
+ if (handleUnauthorizedStatus(chunkRes.status)) {
909
+ break;
910
+ }
911
+ }
912
+ }
913
+ catch {
914
+ /* ignore in MVP */
915
+ }
916
+ finally {
917
+ rrwebEventsRef.current = [];
918
+ nextSeqRef.current = 1;
919
+ actionMeta.current.clear();
920
+ }
921
+ // 2) finish (internal; use original fetch)
922
+ try {
923
+ const res = await origFetch(`${base}/v1/sessions/${sid}/finish`, {
924
+ method: "POST",
925
+ headers: addTenantHeader({
926
+ "Content-Type": "application/json",
927
+ "x-sdk-token": token,
928
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
929
+ [INTERNAL_HEADER]: "1",
930
+ }),
931
+ body: JSON.stringify({ notes: "" }),
932
+ });
933
+ if (handleUnauthorizedStatus(res.status)) {
934
+ clearShareInfo();
935
+ }
936
+ else if (res.ok) {
937
+ let fin = null;
938
+ try {
939
+ fin = await res.json();
940
+ }
941
+ catch {
942
+ fin = null;
943
+ }
944
+ if (fin?.viewerUrl) {
945
+ resetCopyFeedback();
946
+ setShareUrl(fin.viewerUrl);
947
+ }
948
+ else {
949
+ clearShareInfo();
950
+ }
951
+ }
952
+ else {
953
+ clearShareInfo();
954
+ }
955
+ }
956
+ catch {
957
+ clearShareInfo();
958
+ }
959
+ }
960
+ finally {
961
+ // 3) restore fetch & reset
962
+ if (origFetchRef.current)
963
+ window.fetch = origFetchRef.current;
964
+ sessionIdRef.current = null;
965
+ currentAidRef.current = null;
966
+ lastActionLabelRef.current = null;
967
+ hasReqMarkedRef.current.clear();
968
+ if (aidExpiryTimerRef.current)
969
+ window.clearTimeout(aidExpiryTimerRef.current);
970
+ // also reset dedupe
971
+ lastClickRef.current = null;
972
+ setRecording(false);
973
+ recordingRef.current = false;
974
+ isStoppingRef.current = false;
975
+ }
976
+ }
977
+ // ---- UI (floating button) ----
978
+ const btnLabel = recording ? (button?.text ?? "Stop & Report") : (button?.text ?? "Record");
979
+ const loginLabel = "Authenticate to Record";
980
+ const floatingContainerBaseStyle = {
981
+ position: "fixed",
982
+ zIndex: 2147483647,
983
+ display: "flex",
984
+ flexDirection: "column",
985
+ alignItems: "flex-end",
986
+ gap: 12,
987
+ width: "100%",
988
+ maxWidth: 360,
989
+ };
990
+ const floatingContainerStyle = {
991
+ ...floatingContainerBaseStyle,
992
+ right: 16,
993
+ bottom: 16,
994
+ };
995
+ const buttonRowStyle = {
996
+ display: "flex",
997
+ gap: 10,
998
+ justifyContent: "flex-end",
999
+ width: "100%",
1000
+ };
1001
+ const buttonBaseStyle = {
1002
+ display: "inline-flex",
1003
+ alignItems: "center",
1004
+ justifyContent: "center",
1005
+ gap: 8,
1006
+ padding: "12px 24px",
1007
+ borderRadius: 9999,
1008
+ border: "none",
1009
+ background: "#f4f5f7",
1010
+ cursor: "pointer",
1011
+ fontFamily: "ui-sans-serif, system-ui, -apple-system",
1012
+ fontSize: "14px",
1013
+ fontWeight: 600,
1014
+ color: "#ffffff",
1015
+ boxShadow: "0 14px 24px rgba(15, 23, 42, 0.18)",
1016
+ transition: "transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease",
1017
+ minWidth: 150,
1018
+ };
1019
+ const recordButtonStyle = {
1020
+ ...buttonBaseStyle,
1021
+ background: recording
1022
+ ? "linear-gradient(120deg, #ef4444, #b91c1c)"
1023
+ : "linear-gradient(120deg, #2563eb, #1d4ed8)",
1024
+ boxShadow: recording
1025
+ ? "0 18px 32px rgba(239, 68, 68, 0.35)"
1026
+ : "0 18px 32px rgba(37, 99, 235, 0.35)",
1027
+ };
1028
+ const loginButtonStyle = {
1029
+ ...buttonBaseStyle,
1030
+ background: "linear-gradient(120deg, #14b8a6, #0d9488)",
1031
+ };
1032
+ const logoutButtonStyle = {
1033
+ ...buttonBaseStyle,
1034
+ background: "#ffffff",
1035
+ border: "1px solid #e5e7eb",
1036
+ color: "#1f2933",
1037
+ boxShadow: "0 10px 18px rgba(15, 23, 42, 0.12)",
1038
+ };
1039
+ const shareCardStyle = {
1040
+ width: "100%",
1041
+ background: "#ffffff",
1042
+ borderRadius: 16,
1043
+ padding: "14px 16px",
1044
+ border: "1px solid #e5e7eb",
1045
+ boxShadow: "0 20px 36px rgba(15, 23, 42, 0.2)",
1046
+ fontFamily: "ui-sans-serif, system-ui, -apple-system",
1047
+ };
1048
+ const shareLinkStyle = {
1049
+ marginTop: 6,
1050
+ padding: "8px 10px",
1051
+ borderRadius: 10,
1052
+ background: "#f3f4f6",
1053
+ fontSize: 12,
1054
+ color: "#374151",
1055
+ wordBreak: "break-all",
1056
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1057
+ };
1058
+ const copyButtonStyle = {
1059
+ ...buttonBaseStyle,
1060
+ padding: "8px 16px",
1061
+ fontSize: 13,
1062
+ background: "#111827",
1063
+ boxShadow: "none",
1064
+ minWidth: 110,
1065
+ };
1066
+ const hideButtonStyle = {
1067
+ border: "none",
1068
+ background: "transparent",
1069
+ color: "#6b7280",
1070
+ cursor: "pointer",
1071
+ fontWeight: 700,
1072
+ padding: "6px 8px",
1073
+ borderRadius: 8,
1074
+ fontSize: 12,
1075
+ };
1076
+ const modalOverlayStyle = {
1077
+ position: "fixed",
1078
+ inset: 0,
1079
+ background: "rgba(15, 23, 42, 0.38)",
1080
+ display: "flex",
1081
+ alignItems: "center",
1082
+ justifyContent: "center",
1083
+ zIndex: 2147483648,
1084
+ padding: 16,
1085
+ };
1086
+ const modalStyle = {
1087
+ width: "100%",
1088
+ maxWidth: 360,
1089
+ background: "#ffffff",
1090
+ borderRadius: 12,
1091
+ boxShadow: "0 20px 40px rgba(15, 23, 42, 0.25)",
1092
+ padding: "24px 24px 20px",
1093
+ fontFamily: "ui-sans-serif, system-ui, -apple-system",
1094
+ };
1095
+ const hiddenNoticeStyle = {
1096
+ position: "fixed",
1097
+ right: 16,
1098
+ bottom: 16,
1099
+ zIndex: 2147483647,
1100
+ maxWidth: 320,
1101
+ background: "#111827",
1102
+ color: "#e5e7eb",
1103
+ borderRadius: 12,
1104
+ padding: "12px 14px",
1105
+ boxShadow: "0 16px 30px rgba(0, 0, 0, 0.3)",
1106
+ fontFamily: "ui-sans-serif, system-ui, -apple-system",
1107
+ fontSize: 13,
1108
+ lineHeight: 1.4,
1109
+ };
1110
+ const hiddenNoticeCloseStyle = {
1111
+ border: "none",
1112
+ background: "transparent",
1113
+ color: "#e5e7eb",
1114
+ cursor: "pointer",
1115
+ fontWeight: 700,
1116
+ padding: 0,
1117
+ lineHeight: 1,
1118
+ fontSize: 14,
1119
+ };
1120
+ const labelStyle = {
1121
+ display: "block",
1122
+ fontSize: 13,
1123
+ fontWeight: 600,
1124
+ color: "#111827",
1125
+ marginBottom: 6,
1126
+ };
1127
+ const inputStyle = {
1128
+ width: "100%",
1129
+ padding: "10px 12px",
1130
+ borderRadius: 6,
1131
+ border: "1px solid #d1d5db",
1132
+ fontSize: 14,
1133
+ marginBottom: 14,
1134
+ boxSizing: "border-box",
1135
+ fontFamily: "inherit",
1136
+ };
1137
+ const hideControls = () => {
1138
+ setControlsHidden(true);
1139
+ setShowHiddenNotice(true);
1140
+ };
1141
+ async function copyShareLink() {
1142
+ if (!shareUrl)
1143
+ return;
1144
+ const text = shareUrl;
1145
+ try {
1146
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
1147
+ await navigator.clipboard.writeText(text);
1148
+ }
1149
+ else if (typeof document !== "undefined") {
1150
+ const helper = document.createElement("textarea");
1151
+ helper.value = text;
1152
+ helper.style.position = "fixed";
1153
+ helper.style.opacity = "0";
1154
+ helper.style.pointerEvents = "none";
1155
+ document.body.appendChild(helper);
1156
+ helper.focus();
1157
+ helper.select();
1158
+ document.execCommand("copy");
1159
+ document.body.removeChild(helper);
1160
+ }
1161
+ else {
1162
+ throw new Error("clipboard unavailable");
1163
+ }
1164
+ setCopyStatusWithTimeout("copied");
1165
+ }
1166
+ catch {
1167
+ setCopyStatusWithTimeout("error");
1168
+ }
1169
+ }
1170
+ return (_jsxs(_Fragment, { children: [children, !controlsHidden && (shareUrl || ready) && (_jsxs("div", { "data-repro-internal": "1", style: floatingContainerStyle, children: [_jsx("div", { "data-repro-internal": "1", style: { display: "flex", alignItems: "center", justifyContent: "flex-end", width: "100%", gap: 8 }, "aria-label": "Recording controls header", children: _jsx("button", { type: "button", "data-repro-internal": "1", onClick: hideControls, style: hideButtonStyle, "aria-label": "Hide recording controls", children: "Hide" }) }), shareUrl && (_jsxs("div", { "data-repro-internal": "1", style: shareCardStyle, children: [_jsxs("div", { style: {
1171
+ display: "flex",
1172
+ alignItems: "center",
1173
+ justifyContent: "space-between",
1174
+ gap: 12,
1175
+ width: "100%",
1176
+ }, children: [_jsx("div", { style: { fontSize: 12, fontWeight: 600, color: "#0f172a" }, children: "Latest capture ready to share" }), _jsx("button", { type: "button", "data-repro-internal": "1", onClick: () => clearShareInfo(), style: {
1177
+ border: "none",
1178
+ background: "transparent",
1179
+ color: "#9ca3af",
1180
+ cursor: "pointer",
1181
+ padding: 4,
1182
+ lineHeight: 1,
1183
+ fontWeight: 700,
1184
+ borderRadius: 999,
1185
+ width: 24,
1186
+ height: 24,
1187
+ display: "flex",
1188
+ alignItems: "center",
1189
+ justifyContent: "center",
1190
+ }, "aria-label": "Close share link", children: "\u00D7" })] }), _jsx("div", { style: shareLinkStyle, title: shareUrl, children: shareUrl }), _jsxs("div", { style: {
1191
+ marginTop: 10,
1192
+ display: "flex",
1193
+ alignItems: "center",
1194
+ justifyContent: "space-between",
1195
+ gap: 12,
1196
+ width: "100%",
1197
+ }, children: [_jsx("span", { style: {
1198
+ fontSize: 12,
1199
+ minWidth: 100,
1200
+ color: copyStatus === "copied"
1201
+ ? "#059669"
1202
+ : copyStatus === "error"
1203
+ ? "#b91c1c"
1204
+ : "#6b7280",
1205
+ visibility: copyStatus === "idle" ? "hidden" : "visible",
1206
+ transition: "color 0.2s ease",
1207
+ }, children: copyStatus === "copied"
1208
+ ? "Link copied!"
1209
+ : copyStatus === "error"
1210
+ ? "Unable to copy"
1211
+ : "placeholder" }), _jsx("button", { type: "button", "data-repro-internal": "1", onClick: () => void copyShareLink(), style: copyButtonStyle, children: copyStatus === "copied" ? "Copied" : "Copy link" })] })] })), ready && auth && (_jsxs("div", { "data-repro-internal": "1", style: buttonRowStyle, children: [_jsxs("button", { "data-repro-internal": "1", onClick: () => (recording ? stop() : start()), style: recordButtonStyle, type: "button", children: [_jsx("span", { "aria-hidden": "true", style: {
1212
+ width: 10,
1213
+ height: 10,
1214
+ borderRadius: "999px",
1215
+ background: recording ? "#fecaca" : "#bbf7d0",
1216
+ boxShadow: recording
1217
+ ? "0 0 12px rgba(239, 68, 68, 0.7)"
1218
+ : "0 0 10px rgba(16, 185, 129, 0.6)",
1219
+ } }), btnLabel] }), _jsx("button", { "data-repro-internal": "1", onClick: () => void logout(), style: logoutButtonStyle, type: "button", children: "Log out" })] })), ready && !auth && (_jsx("div", { "data-repro-internal": "1", style: buttonRowStyle, children: _jsx("button", { "data-repro-internal": "1", onClick: () => {
1220
+ setLoginError(null);
1221
+ setShowLogin(true);
1222
+ }, style: loginButtonStyle, type: "button", children: loginLabel }) }))] })), controlsHidden && showHiddenNotice && (_jsxs("div", { "data-repro-internal": "1", style: hiddenNoticeStyle, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }, children: [_jsx("div", { style: { fontWeight: 700 }, children: "Recording controls hidden" }), _jsx("button", { type: "button", "data-repro-internal": "1", style: hiddenNoticeCloseStyle, "aria-label": "Close hidden controls message", onClick: () => setShowHiddenNotice(false), children: "\u00D7" })] }), _jsx("div", { children: "Press Ctrl/Cmd + R + O to show them again." })] })), showLogin && (_jsx("div", { "data-repro-internal": "1", style: modalOverlayStyle, onClick: (evt) => {
1223
+ if (evt.target === evt.currentTarget && !isLoggingIn) {
1224
+ setShowLogin(false);
1225
+ }
1226
+ }, children: _jsxs("div", { "data-repro-internal": "1", style: modalStyle, children: [_jsx("h3", { style: { margin: 0, marginBottom: 12, fontSize: 18, color: "#0f172a" }, children: "Authenticate to start recording" }), _jsxs("form", { onSubmit: handleLoginSubmit, children: [_jsx("label", { style: labelStyle, htmlFor: "repro-login-email", children: "Email" }), _jsx("input", { id: "repro-login-email", "data-repro-internal": "1", style: inputStyle, type: "email", value: loginEmail, placeholder: "user@example.com", onChange: (evt) => {
1227
+ setLoginEmail(evt.target.value);
1228
+ setLoginError(null);
1229
+ }, autoComplete: "email", required: true }), _jsx("label", { style: labelStyle, htmlFor: "repro-login-password", children: "Password" }), _jsx("input", { id: "repro-login-password", "data-repro-internal": "1", style: inputStyle, type: "password", value: loginPassword, placeholder: "Enter your workspace password", onChange: (evt) => {
1230
+ setLoginPassword(evt.target.value);
1231
+ setLoginError(null);
1232
+ }, autoComplete: "current-password", required: true }), loginError && (_jsx("div", { style: {
1233
+ color: "#b91c1c",
1234
+ background: "#fee2e2",
1235
+ borderRadius: 8,
1236
+ padding: "8px 12px",
1237
+ fontSize: 13,
1238
+ marginBottom: 12,
1239
+ }, children: loginError })), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 12 }, children: [_jsx("button", { type: "button", "data-repro-internal": "1", onClick: () => {
1240
+ if (!isLoggingIn)
1241
+ setShowLogin(false);
1242
+ }, style: {
1243
+ padding: "10px 16px",
1244
+ borderRadius: 6,
1245
+ border: "1px solid transparent",
1246
+ background: "transparent",
1247
+ color: "#4b5563",
1248
+ fontWeight: 600,
1249
+ cursor: isLoggingIn ? "not-allowed" : "pointer",
1250
+ }, disabled: isLoggingIn, children: "Cancel" }), _jsx("button", { type: "submit", "data-repro-internal": "1", style: {
1251
+ padding: "10px 16px",
1252
+ borderRadius: 6,
1253
+ border: "none",
1254
+ background: disableLogin ? "#d1d5db" : "#2563eb",
1255
+ color: disableLogin ? "#6b7280" : "#ffffff",
1256
+ fontWeight: 700,
1257
+ cursor: disableLogin ? "not-allowed" : "pointer",
1258
+ transition: "background 0.2s ease",
1259
+ }, disabled: disableLogin, children: isLoggingIn ? "Signing in..." : "Sign in" })] })] })] }) }))] }));
1260
+ }
1261
+ export default ReproProvider;