@guidelytech/react-native 0.12.0 → 0.14.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.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4
4
 
5
5
  // src/index.tsx
6
6
  import { useContext as useContext3, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
7
- import { AppState as AppState2, Dimensions, Linking, Platform as Platform6, StyleSheet as StyleSheet10, View as View12 } from "react-native";
7
+ import { AppState as AppState2, Dimensions, Linking, NativeModules, Platform as Platform6, StyleSheet as StyleSheet10, View as View12 } from "react-native";
8
8
  import { createStaticFlags as createStaticFlags2, stableStringify as stableStringify3 } from "@guidelytech/flags";
9
9
 
10
10
  // src/flags.tsx
@@ -1378,14 +1378,14 @@ function quoteValue(value) {
1378
1378
  function attrSelector(name, value) {
1379
1379
  return `[${name}=${quoteValue(value)}]`.slice(0, 300);
1380
1380
  }
1381
- function cleanText(text) {
1382
- const t = (text ?? "").replace(/\s+/g, " ").trim();
1381
+ function cleanText(text2) {
1382
+ const t = (text2 ?? "").replace(/\s+/g, " ").trim();
1383
1383
  return t ? t.slice(0, 100) : void 0;
1384
1384
  }
1385
1385
  function tapProperties(el, x, y, platform, maskAll) {
1386
1386
  const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
1387
- const text = maskAll ? void 0 : cleanText(el.text);
1388
- if (text) props.text = text;
1387
+ const text2 = maskAll ? void 0 : cleanText(el.text);
1388
+ if (text2) props.text = text2;
1389
1389
  return props;
1390
1390
  }
1391
1391
  var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
@@ -2006,10 +2006,10 @@ function xhrStreamPost() {
2006
2006
  const type = xhr.getResponseHeader("content-type") || "";
2007
2007
  if (!type.includes("text/event-stream")) return;
2008
2008
  streamed = true;
2009
- const text = xhr.responseText || "";
2010
- if (text.length > seen) {
2011
- onChunk(text.slice(seen));
2012
- seen = text.length;
2009
+ const text2 = xhr.responseText || "";
2010
+ if (text2.length > seen) {
2011
+ onChunk(text2.slice(seen));
2012
+ seen = text2.length;
2013
2013
  }
2014
2014
  };
2015
2015
  xhr.open("POST", url);
@@ -2084,14 +2084,14 @@ function fetchStreamPost(fetchImpl) {
2084
2084
  if (rest) onChunk(rest);
2085
2085
  return { status: res.status, json: null, streamed: true };
2086
2086
  }
2087
- const text = await race(res.text());
2087
+ const text2 = await race(res.text());
2088
2088
  if (sse) {
2089
- onChunk(text);
2089
+ onChunk(text2);
2090
2090
  return { status: res.status, json: null, streamed: true };
2091
2091
  }
2092
2092
  let json = null;
2093
2093
  try {
2094
- json = JSON.parse(text || "null");
2094
+ json = JSON.parse(text2 || "null");
2095
2095
  } catch {
2096
2096
  json = null;
2097
2097
  }
@@ -2204,10 +2204,10 @@ var Messenger = class {
2204
2204
  if (v.kind === "tab" && v.tab === "messages") void this.refreshConversations();
2205
2205
  }
2206
2206
  /** Opens a new thread (optionally pre-filling the composer). */
2207
- showNewMessage(text = "") {
2207
+ showNewMessage(text2 = "") {
2208
2208
  this.stopThreadPolling();
2209
2209
  if (!this.snapshot.open) this.deps.onOpen();
2210
- this.set({ open: true, view: { kind: "thread", conversationId: null }, thread: { conversation: null, messages: [], streaming: {}, sending: false, error: null, draft: text } });
2210
+ this.set({ open: true, view: { kind: "thread", conversationId: null }, thread: { conversation: null, messages: [], streaming: {}, sending: false, error: null, draft: text2 } });
2211
2211
  }
2212
2212
  async openConversation(id) {
2213
2213
  const known = this.snapshot.conversations.find((c) => c.id === id) ?? null;
@@ -2251,8 +2251,8 @@ var Messenger = class {
2251
2251
  }
2252
2252
  }
2253
2253
  /** Sends the composer text: starts a conversation or posts to the open one. Streams Scout's reply. */
2254
- async send(text) {
2255
- const body = text.trim().slice(0, 4e3);
2254
+ async send(text2) {
2255
+ const body = text2.trim().slice(0, 4e3);
2256
2256
  const thread = this.snapshot.thread;
2257
2257
  if (!body || !thread || thread.sending) return;
2258
2258
  this.setThread({ sending: true, error: null, draft: "" });
@@ -2428,6 +2428,425 @@ var Messenger = class {
2428
2428
  }
2429
2429
  };
2430
2430
 
2431
+ // src/core/replay.ts
2432
+ var RrwebEvents = {
2433
+ IMG_ID: 8,
2434
+ TOUCH_START: 7,
2435
+ TOUCH_END: 9,
2436
+ CLICK: 2,
2437
+ CSS: "html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#000}img{display:block;width:100%;height:100%;object-fit:contain}",
2438
+ dataUrl(base64) {
2439
+ return `data:image/jpeg;base64,${base64}`;
2440
+ },
2441
+ meta(timestamp, href, width, height) {
2442
+ return JSON.stringify({ type: 4, data: { href, width, height }, timestamp });
2443
+ },
2444
+ fullSnapshot(timestamp, dataUrl, width, height) {
2445
+ const style = { type: 2, id: 5, tagName: "style", attributes: {}, childNodes: [{ type: 3, id: 6, textContent: RrwebEvents.CSS, isStyle: true }] };
2446
+ const img = { type: 2, id: 8, tagName: "img", attributes: { src: dataUrl, width, height, alt: "" }, childNodes: [] };
2447
+ const html = { type: 2, id: 3, tagName: "html", attributes: {}, childNodes: [{ type: 2, id: 4, tagName: "head", attributes: {}, childNodes: [style] }, { type: 2, id: 7, tagName: "body", attributes: {}, childNodes: [img] }] };
2448
+ const node = { type: 0, id: 1, childNodes: [{ type: 1, id: 2, name: "html", publicId: "", systemId: "" }, html] };
2449
+ return JSON.stringify({ type: 2, data: { node, initialOffset: { left: 0, top: 0 } }, timestamp });
2450
+ },
2451
+ frame(timestamp, dataUrl) {
2452
+ return JSON.stringify({ type: 3, data: { source: 0, texts: [], attributes: [{ id: 8, attributes: { src: dataUrl } }], removes: [], adds: [] }, timestamp });
2453
+ },
2454
+ /** MouseInteraction (source 2): 7 TouchStart, 9 TouchEnd, 2 Click. */
2455
+ interaction(timestamp, type, x, y) {
2456
+ return JSON.stringify({ type: 3, data: { source: 2, type, id: 8, x: round1(x), y: round1(y) }, timestamp });
2457
+ },
2458
+ touchMove(timestamp, x, y) {
2459
+ return JSON.stringify({ type: 3, data: { source: 6, positions: [{ x: round1(x), y: round1(y), id: 8, timeOffset: 0 }] }, timestamp });
2460
+ },
2461
+ custom(timestamp, tag, payload) {
2462
+ return JSON.stringify({ type: 5, data: { tag, payload }, timestamp });
2463
+ }
2464
+ };
2465
+ var round1 = (v) => Math.round(v * 10) / 10;
2466
+ var UPLOAD_TIMEOUT_MS = 3e4;
2467
+ var ReplayUploader = class {
2468
+ constructor(opts) {
2469
+ __publicField(this, "opts", opts);
2470
+ __publicField(this, "windowId", randomId(16));
2471
+ __publicField(this, "buffer", null);
2472
+ __publicField(this, "queue", []);
2473
+ __publicField(this, "queuedBytes", 0);
2474
+ __publicField(this, "seqs", /* @__PURE__ */ new Map());
2475
+ __publicField(this, "inflight", null);
2476
+ __publicField(this, "flushTimer", null);
2477
+ __publicField(this, "retryTimer", null);
2478
+ __publicField(this, "attempt", 0);
2479
+ __publicField(this, "destroyed", false);
2480
+ __publicField(this, "offRecover");
2481
+ this.offRecover = opts.health?.onRecover?.(() => {
2482
+ this.clearRetry();
2483
+ this.attempt = 0;
2484
+ this.flush();
2485
+ }) ?? null;
2486
+ }
2487
+ get pendingChunks() {
2488
+ return this.queue.length;
2489
+ }
2490
+ get bufferedEvents() {
2491
+ return this.buffer?.events.length ?? 0;
2492
+ }
2493
+ push(sessionId, json) {
2494
+ if (this.destroyed) return;
2495
+ if (this.buffer && this.buffer.sessionId !== sessionId) this.seal();
2496
+ const chunk = this.buffer ?? (this.buffer = { sessionId, meta: this.opts.meta(), events: [], bytes: 0 });
2497
+ chunk.events.push(json);
2498
+ chunk.bytes += json.length;
2499
+ if (chunk.bytes > (this.opts.maxBufferBytes ?? 1e6)) this.flush();
2500
+ else if (!this.flushTimer) {
2501
+ this.flushTimer = setTimeout(() => {
2502
+ this.flushTimer = null;
2503
+ this.flush();
2504
+ }, this.opts.flushIntervalMs ?? 1e4);
2505
+ }
2506
+ }
2507
+ /** Seals the buffer and uploads the backlog (one chunk at a time). */
2508
+ flush() {
2509
+ if (this.destroyed) return;
2510
+ this.seal();
2511
+ if (this.retryTimer || this.inflight || !this.queue.length) return;
2512
+ void this.sendNext();
2513
+ }
2514
+ /** Drops everything buffered or queued (consent withdrawn, server disabled replay). */
2515
+ discard() {
2516
+ this.buffer = null;
2517
+ this.queue = [];
2518
+ this.queuedBytes = 0;
2519
+ this.clearFlushTimer();
2520
+ this.clearRetry();
2521
+ this.attempt = 0;
2522
+ }
2523
+ /** Uploads what is left once; no retries afterwards. */
2524
+ destroy() {
2525
+ this.flush();
2526
+ this.destroyed = true;
2527
+ this.offRecover?.();
2528
+ this.clearFlushTimer();
2529
+ this.clearRetry();
2530
+ }
2531
+ clearFlushTimer() {
2532
+ if (this.flushTimer) clearTimeout(this.flushTimer);
2533
+ this.flushTimer = null;
2534
+ }
2535
+ clearRetry() {
2536
+ if (this.retryTimer) clearTimeout(this.retryTimer);
2537
+ this.retryTimer = null;
2538
+ }
2539
+ scheduleRetry(ms) {
2540
+ this.retryTimer = setTimeout(() => {
2541
+ this.retryTimer = null;
2542
+ this.flush();
2543
+ }, ms);
2544
+ }
2545
+ seal() {
2546
+ this.clearFlushTimer();
2547
+ const chunk = this.buffer;
2548
+ this.buffer = null;
2549
+ if (!chunk || !chunk.events.length) return;
2550
+ this.queue.push(chunk);
2551
+ this.queuedBytes += chunk.bytes;
2552
+ const max = this.opts.maxQueuedBytes ?? 5e6;
2553
+ while (this.queuedBytes > max && this.queue.length > 1) {
2554
+ const i = this.inflight && this.queue[0] === this.inflight ? 1 : 0;
2555
+ const [dropped] = this.queue.splice(i, 1);
2556
+ if (!dropped) break;
2557
+ this.queuedBytes -= dropped.bytes;
2558
+ this.opts.log?.(`replay backlog full: dropped a chunk of ${dropped.events.length} events`);
2559
+ }
2560
+ }
2561
+ nextSeq(chunk) {
2562
+ if (chunk.seq === void 0) {
2563
+ const seq = this.seqs.get(chunk.sessionId) ?? 0;
2564
+ this.seqs.set(chunk.sessionId, seq + 1);
2565
+ chunk.seq = seq;
2566
+ }
2567
+ return chunk.seq;
2568
+ }
2569
+ body(chunk) {
2570
+ const head = JSON.stringify({ sessionId: chunk.sessionId, windowId: this.windowId, seq: this.nextSeq(chunk), ...chunk.meta });
2571
+ return `${head.slice(0, -1)},"events":[${chunk.events.join(",")}]}`;
2572
+ }
2573
+ async sendNext() {
2574
+ const chunk = this.queue[0];
2575
+ if (!chunk) return;
2576
+ const wait = this.destroyed ? 0 : this.opts.health?.waitMs() ?? 0;
2577
+ if (wait > 0) return this.scheduleRetry(wait);
2578
+ this.inflight = chunk;
2579
+ const result = await this.upload(this.body(chunk));
2580
+ this.onResult(chunk, result);
2581
+ }
2582
+ async upload(json) {
2583
+ const f = this.opts.fetch();
2584
+ if (!f) return "drop";
2585
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
2586
+ const timer = ac ? setTimeout(() => ac.abort(), UPLOAD_TIMEOUT_MS) : null;
2587
+ try {
2588
+ let res;
2589
+ try {
2590
+ res = await f(this.opts.url(), { method: "POST", body: json, headers: { "Content-Type": "application/json" }, signal: ac?.signal });
2591
+ } catch {
2592
+ this.opts.health?.failure();
2593
+ return "retry";
2594
+ }
2595
+ if (res.status === 429 || res.status >= 500) this.opts.health?.failure();
2596
+ else this.opts.health?.success();
2597
+ if (res.ok) {
2598
+ const data = await res.json().catch(() => null);
2599
+ return data?.disabled ? "disabled" : "ok";
2600
+ }
2601
+ return res.status >= 400 && res.status < 500 && res.status !== 429 ? "drop" : "retry";
2602
+ } finally {
2603
+ if (timer) clearTimeout(timer);
2604
+ }
2605
+ }
2606
+ onResult(chunk, result) {
2607
+ if (this.inflight === chunk) this.inflight = null;
2608
+ if (result === "retry" && this.queue.includes(chunk)) {
2609
+ if (!this.destroyed) {
2610
+ const max = this.opts.maxAttempts ?? 5;
2611
+ this.attempt = Math.min(this.attempt + 1, max);
2612
+ const delay = this.attempt >= max ? this.opts.slowRetryMs ?? 6e4 : backoffMs(this.attempt - 1, 1e3, 8e3);
2613
+ return this.scheduleRetry(Math.max(delay, this.opts.health?.waitMs() ?? 0));
2614
+ }
2615
+ this.opts.log?.("replay upload failed after the recorder stopped; dropping the chunk");
2616
+ }
2617
+ this.attempt = 0;
2618
+ this.remove(chunk);
2619
+ if (result === "disabled") {
2620
+ for (const c of this.queue.slice()) if (c.sessionId === chunk.sessionId) this.remove(c);
2621
+ if (this.buffer?.sessionId === chunk.sessionId) this.buffer = null;
2622
+ this.opts.onDisabled(chunk.sessionId);
2623
+ }
2624
+ if (this.queue.length && !this.retryTimer && !this.inflight) void this.sendNext();
2625
+ }
2626
+ remove(chunk) {
2627
+ const i = this.queue.indexOf(chunk);
2628
+ if (i >= 0) {
2629
+ this.queue.splice(i, 1);
2630
+ this.queuedBytes -= chunk.bytes;
2631
+ }
2632
+ }
2633
+ };
2634
+ var TOUCH_SETTLE_MS = 200;
2635
+ var IDLE_FRAMES = 3;
2636
+ var MAX_IDLE_INTERVAL_MS = 5e3;
2637
+ var SessionRecorder = class {
2638
+ constructor(opts) {
2639
+ __publicField(this, "opts", opts);
2640
+ __publicField(this, "intervalMs");
2641
+ __publicField(this, "now");
2642
+ __publicField(this, "sid", null);
2643
+ __publicField(this, "foreground", true);
2644
+ __publicField(this, "generation", 0);
2645
+ __publicField(this, "timer", null);
2646
+ __publicField(this, "timerDue", 0);
2647
+ __publicField(this, "capturing", false);
2648
+ __publicField(this, "needKeyframe", true);
2649
+ __publicField(this, "hasKeyframe", false);
2650
+ __publicField(this, "lastFrame", null);
2651
+ __publicField(this, "unchanged", 0);
2652
+ __publicField(this, "captureSoon", false);
2653
+ __publicField(this, "pendingCustom", []);
2654
+ __publicField(this, "touchDown", false);
2655
+ __publicField(this, "downX", 0);
2656
+ __publicField(this, "downY", 0);
2657
+ __publicField(this, "downAt", 0);
2658
+ __publicField(this, "maxDistance", 0);
2659
+ __publicField(this, "lastMoveAt", 0);
2660
+ __publicField(this, "originX", 0);
2661
+ __publicField(this, "originY", 0);
2662
+ /** Viewport of the last frame (points), for chunk metadata. */
2663
+ __publicField(this, "viewportWidth", 0);
2664
+ __publicField(this, "viewportHeight", 0);
2665
+ this.intervalMs = Math.max(500, opts.intervalMs);
2666
+ this.now = opts.now ?? Date.now;
2667
+ }
2668
+ get sessionId() {
2669
+ return this.sid;
2670
+ }
2671
+ get isRecording() {
2672
+ return this.sid !== null;
2673
+ }
2674
+ get currentIntervalMs() {
2675
+ return this.unchanged >= IDLE_FRAMES ? Math.max(this.intervalMs, Math.min(4 * this.intervalMs, MAX_IDLE_INTERVAL_MS)) : this.intervalMs;
2676
+ }
2677
+ /** Starts (or keeps) recording `sessionId`; another session stops the current one first (uploading it). */
2678
+ start(sessionId) {
2679
+ if (this.sid === sessionId) return;
2680
+ if (this.sid !== null) this.stop(false);
2681
+ this.sid = sessionId;
2682
+ this.generation++;
2683
+ this.needKeyframe = true;
2684
+ this.hasKeyframe = false;
2685
+ this.lastFrame = null;
2686
+ this.unchanged = 0;
2687
+ this.captureSoon = false;
2688
+ this.pendingCustom = [];
2689
+ this.touchDown = false;
2690
+ if (this.foreground) this.schedule(0);
2691
+ }
2692
+ /** Stops recording; buffered events are uploaded unless `discard`. */
2693
+ stop(discard) {
2694
+ if (this.sid === null) return;
2695
+ this.sid = null;
2696
+ this.generation++;
2697
+ this.clearTimer();
2698
+ this.capturing = false;
2699
+ this.pendingCustom = [];
2700
+ this.touchDown = false;
2701
+ if (discard) this.opts.uploader.discard();
2702
+ else this.opts.uploader.flush();
2703
+ }
2704
+ /** App backgrounded: no captures; uploads what is buffered. */
2705
+ pause() {
2706
+ this.foreground = false;
2707
+ this.clearTimer();
2708
+ this.generation++;
2709
+ this.capturing = false;
2710
+ this.touchDown = false;
2711
+ this.opts.uploader.flush();
2712
+ }
2713
+ /** App foregrounded: resume with a keyframe. */
2714
+ resume() {
2715
+ if (this.foreground) return;
2716
+ this.foreground = true;
2717
+ this.needKeyframe = true;
2718
+ this.unchanged = 0;
2719
+ if (this.sid !== null) this.schedule(0);
2720
+ }
2721
+ /** A new screen: the next frame is a keyframe with the new href. */
2722
+ onScreen() {
2723
+ if (this.sid === null) return;
2724
+ this.needKeyframe = true;
2725
+ this.activity();
2726
+ }
2727
+ activity() {
2728
+ const wasIdle = this.unchanged >= IDLE_FRAMES;
2729
+ this.unchanged = 0;
2730
+ if (wasIdle && this.sid !== null && this.foreground && !this.capturing && !this.touchDown) this.schedule(this.intervalMs, true);
2731
+ }
2732
+ /** Adds the `guidely` marker for an analytics event. */
2733
+ addGuidelyEvent(name, customName, guideId) {
2734
+ const id = this.sid;
2735
+ if (id === null) return;
2736
+ const payload = { name };
2737
+ if (customName) payload.customName = customName;
2738
+ if (guideId) payload.guideId = guideId;
2739
+ this.activity();
2740
+ const now = this.now();
2741
+ if (!this.hasKeyframe) {
2742
+ if (this.pendingCustom.length < 50) this.pendingCustom.push([now, payload]);
2743
+ return;
2744
+ }
2745
+ this.opts.uploader.push(id, RrwebEvents.custom(now, "guidely", payload));
2746
+ }
2747
+ /** A primary-pointer touch at (x, y) points from the React root view. */
2748
+ touch(phase, rootX, rootY) {
2749
+ const id = this.sid;
2750
+ if (id === null || !this.foreground) return;
2751
+ const x = rootX + this.originX;
2752
+ const y = rootY + this.originY;
2753
+ const now = this.now();
2754
+ const emit = this.hasKeyframe;
2755
+ const push = (json) => emit && this.opts.uploader.push(id, json);
2756
+ switch (phase) {
2757
+ case "down":
2758
+ this.touchDown = true;
2759
+ this.unchanged = 0;
2760
+ this.downX = x;
2761
+ this.downY = y;
2762
+ this.downAt = now;
2763
+ this.maxDistance = 0;
2764
+ this.lastMoveAt = now;
2765
+ push(RrwebEvents.interaction(now, RrwebEvents.TOUCH_START, x, y));
2766
+ return;
2767
+ case "move":
2768
+ if (!this.touchDown) return;
2769
+ this.maxDistance = Math.max(this.maxDistance, Math.hypot(x - this.downX, y - this.downY));
2770
+ if (now - this.lastMoveAt < 50) return;
2771
+ this.lastMoveAt = now;
2772
+ push(RrwebEvents.touchMove(now, x, y));
2773
+ return;
2774
+ default:
2775
+ if (!this.touchDown) return;
2776
+ this.touchDown = false;
2777
+ this.unchanged = 0;
2778
+ this.maxDistance = Math.max(this.maxDistance, Math.hypot(x - this.downX, y - this.downY));
2779
+ push(RrwebEvents.interaction(now, RrwebEvents.TOUCH_END, x, y));
2780
+ if (phase === "up" && this.maxDistance <= 10 && now - this.downAt < 1e3) push(RrwebEvents.interaction(now, RrwebEvents.CLICK, x, y));
2781
+ if (this.capturing) this.captureSoon = true;
2782
+ else this.schedule(TOUCH_SETTLE_MS, true);
2783
+ }
2784
+ }
2785
+ clearTimer() {
2786
+ if (this.timer) clearTimeout(this.timer);
2787
+ this.timer = null;
2788
+ }
2789
+ /** Schedules the next capture `delay` ms from now, or keeps an earlier one when `ifSooner`. */
2790
+ schedule(delay, ifSooner = false) {
2791
+ const due = this.now() + delay;
2792
+ if (ifSooner && this.timer && this.timerDue <= due) return;
2793
+ this.clearTimer();
2794
+ this.timerDue = due;
2795
+ const gen = this.generation;
2796
+ this.timer = setTimeout(() => {
2797
+ this.timer = null;
2798
+ if (gen === this.generation) void this.tick();
2799
+ }, delay);
2800
+ }
2801
+ async tick() {
2802
+ if (this.sid === null || !this.foreground || this.capturing) return;
2803
+ if (this.touchDown) {
2804
+ this.schedule(this.intervalMs);
2805
+ return;
2806
+ }
2807
+ this.capturing = true;
2808
+ this.captureSoon = false;
2809
+ const gen = this.generation;
2810
+ let frame = null;
2811
+ try {
2812
+ frame = await this.opts.capturer(await this.opts.captureOptions());
2813
+ } catch (err) {
2814
+ this.opts.onError?.(err);
2815
+ }
2816
+ if (gen !== this.generation) return;
2817
+ this.capturing = false;
2818
+ if (frame) this.onFrame(frame, this.now());
2819
+ if (this.sid !== null && this.foreground) this.schedule(this.captureSoon ? TOUCH_SETTLE_MS : this.currentIntervalMs);
2820
+ }
2821
+ onFrame(frame, at) {
2822
+ const id = this.sid;
2823
+ if (id === null || !(frame.width > 0) || !(frame.height > 0) || !frame.base64) return;
2824
+ const sizeChanged = frame.width !== this.viewportWidth || frame.height !== this.viewportHeight;
2825
+ const keyframe = this.needKeyframe || !this.hasKeyframe || sizeChanged;
2826
+ if (!keyframe && this.lastFrame === frame.base64) {
2827
+ this.unchanged++;
2828
+ return;
2829
+ }
2830
+ this.unchanged = 0;
2831
+ this.lastFrame = frame.base64;
2832
+ this.originX = frame.originX ?? 0;
2833
+ this.originY = frame.originY ?? 0;
2834
+ this.viewportWidth = frame.width;
2835
+ this.viewportHeight = frame.height;
2836
+ const url = RrwebEvents.dataUrl(frame.base64);
2837
+ const up = this.opts.uploader;
2838
+ if (!keyframe) return up.push(id, RrwebEvents.frame(at, url));
2839
+ this.needKeyframe = false;
2840
+ up.push(id, RrwebEvents.meta(at, this.opts.href(), frame.width, frame.height));
2841
+ up.push(id, RrwebEvents.fullSnapshot(at, url, frame.width, frame.height));
2842
+ if (!this.hasKeyframe) {
2843
+ this.hasKeyframe = true;
2844
+ for (const [ts, payload] of this.pendingCustom) up.push(id, RrwebEvents.custom(Math.max(ts, at), "guidely", payload));
2845
+ this.pendingCustom = [];
2846
+ }
2847
+ }
2848
+ };
2849
+
2431
2850
  // src/core/storage.ts
2432
2851
  var PersistentStore = class {
2433
2852
  constructor(backing, onError = () => {
@@ -2481,7 +2900,7 @@ var PersistentStore = class {
2481
2900
 
2482
2901
  // src/core/client.ts
2483
2902
  var SDK_NAME = "@guidelytech/react-native";
2484
- var SDK_VERSION = "0.12.0";
2903
+ var SDK_VERSION = "0.14.0";
2485
2904
  var DEFAULT_API_URL = "https://guidelytech.com";
2486
2905
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2487
2906
  var CACHED_START_MS = 1500;
@@ -2542,6 +2961,10 @@ var GuidelyClient = class {
2542
2961
  __publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
2543
2962
  __publicField(this, "backgroundedAt", null);
2544
2963
  __publicField(this, "messenger", null);
2964
+ __publicField(this, "replayUploader", null);
2965
+ __publicField(this, "recorder", null);
2966
+ /** Replay decision for the current session (sampled once; forced by start/stopSessionRecording). */
2967
+ __publicField(this, "replayDecision", null);
2545
2968
  __publicField(this, "ui", { steps: [], checklists: [], survey: null, resourceCenterOpen: false, theme: {}, messenger: false });
2546
2969
  __publicField(this, "uiListeners", /* @__PURE__ */ new Set());
2547
2970
  /** Feature flags for this environment: `guidely.flags.isEnabled("new-checkout")`. */
@@ -2693,6 +3116,7 @@ var GuidelyClient = class {
2693
3116
  if (this.destroyed) return;
2694
3117
  this.isReady = true;
2695
3118
  if (this.config) this.emitter.emit("ready", this.config);
3119
+ this.syncReplay();
2696
3120
  this.notify();
2697
3121
  this.evaluate("ready");
2698
3122
  } catch (err) {
@@ -2719,6 +3143,7 @@ var GuidelyClient = class {
2719
3143
  this.syncMessenger();
2720
3144
  if (this.hydrated) this.checkLaunch();
2721
3145
  this.emitter.emit("config", this.config);
3146
+ this.syncReplay();
2722
3147
  this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
2723
3148
  return true;
2724
3149
  }
@@ -2771,6 +3196,7 @@ var GuidelyClient = class {
2771
3196
  void this.outbox?.flush();
2772
3197
  void this.store.flush();
2773
3198
  void this.flags.flush();
3199
+ this.recorder?.pause();
2774
3200
  this.messenger?.setForeground(false);
2775
3201
  } else if (state === "active" && this.backgroundedAt !== null) {
2776
3202
  this.messenger?.setForeground(true);
@@ -2780,7 +3206,10 @@ var GuidelyClient = class {
2780
3206
  this.session = newSession();
2781
3207
  this.persistSession();
2782
3208
  this.firedDelays.clear();
3209
+ this.recorder?.stop(false);
2783
3210
  }
3211
+ this.recorder?.resume();
3212
+ this.syncReplay();
2784
3213
  this.cameToForeground();
2785
3214
  void this.flags.refresh();
2786
3215
  if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
@@ -2803,6 +3232,9 @@ var GuidelyClient = class {
2803
3232
  this.queued = [];
2804
3233
  this.messenger?.destroy();
2805
3234
  this.messenger = null;
3235
+ this.recorder?.stop(!this.analyticsEnabled());
3236
+ this.recorder = null;
3237
+ this.replayUploader?.destroy();
2806
3238
  this.visibilityWatchers.forEach((stop) => stop());
2807
3239
  this.visibilityWatchers.clear();
2808
3240
  this.timers.forEach((t) => clearTimeout(t));
@@ -2917,6 +3349,7 @@ var GuidelyClient = class {
2917
3349
  this.store.set(KEYS.anon, this.anonymousId);
2918
3350
  this.state = emptyState();
2919
3351
  this.persistState();
3352
+ this.recorder?.stop(false);
2920
3353
  this.session = newSession();
2921
3354
  this.persistSession();
2922
3355
  this.lastAudiences = /* @__PURE__ */ new Set();
@@ -2924,6 +3357,7 @@ var GuidelyClient = class {
2924
3357
  this.firstVisit = true;
2925
3358
  this.exposures.clear();
2926
3359
  this.syncFlagAttributes();
3360
+ this.syncReplay();
2927
3361
  this.emitter.emit("identify", { userId: null, anonymousId: this.anonymousId });
2928
3362
  this.notify();
2929
3363
  if (this.isReady) this.evaluate("identify");
@@ -2938,6 +3372,7 @@ var GuidelyClient = class {
2938
3372
  this.whenHydrated(() => {
2939
3373
  if (consent === "denied") this.queue?.clear();
2940
3374
  else if (consent === "granted") void this.queue?.flush();
3375
+ this.syncReplay();
2941
3376
  });
2942
3377
  }
2943
3378
  analyticsEnabled() {
@@ -2966,6 +3401,7 @@ var GuidelyClient = class {
2966
3401
  const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2967
3402
  if (locale) event.properties = { ...event.properties ?? {}, locale };
2968
3403
  this.queue.enqueue(event);
3404
+ this.recorder?.addGuidelyEvent(name, event.customName, event.guideId);
2969
3405
  this.emitter.emit("track", event);
2970
3406
  return event;
2971
3407
  }
@@ -3003,6 +3439,7 @@ var GuidelyClient = class {
3003
3439
  }
3004
3440
  this.screenName = name;
3005
3441
  this.targets.setScreen(name);
3442
+ if (changed) this.recorder?.onScreen();
3006
3443
  if (changed) this.syncFlagAttributes();
3007
3444
  const url = this.currentUrl();
3008
3445
  if (changed) {
@@ -3037,6 +3474,112 @@ var GuidelyClient = class {
3037
3474
  if (this.isReady) this.evaluate("click", { targetId });
3038
3475
  }
3039
3476
  // ---------------------------------------------------------------------------
3477
+ // Session replay
3478
+ // ---------------------------------------------------------------------------
3479
+ replaySettings() {
3480
+ const r = this.config?.settings?.replay;
3481
+ if (!r?.enabled || !this.options.sessionReplay?.enabled || !this.bridge.capturer) return null;
3482
+ return r;
3483
+ }
3484
+ /** Reconciles recording with config, consent, readiness, the session and its sample decision. Idempotent. */
3485
+ syncReplay() {
3486
+ if (this.destroyed || !this.hydrated) return;
3487
+ const r = this.isReady && this.analyticsEnabled() ? this.replaySettings() : null;
3488
+ const id = this.session.id;
3489
+ if (r && this.replayDecision?.sessionId !== id) {
3490
+ this.replayDecision = { sessionId: id, record: rolloutBucket(id, "replay") < (r.sampleRate ?? 100) };
3491
+ this.log(`replay ${this.replayDecision.record ? "sampled" : "not sampled"} for session ${id}`);
3492
+ }
3493
+ if (!r || !this.replayDecision?.record) {
3494
+ if (this.recorder?.isRecording) this.recorder.stop(!this.analyticsEnabled());
3495
+ return;
3496
+ }
3497
+ const rec = this.recorder ?? this.createRecorder();
3498
+ if (rec && rec.sessionId !== id) {
3499
+ this.log(`recording session ${id}`);
3500
+ rec.start(id);
3501
+ }
3502
+ }
3503
+ createRecorder() {
3504
+ const capturer = this.bridge.capturer;
3505
+ if (!capturer) return null;
3506
+ const uploader = this.replayUploader ?? (this.replayUploader = new ReplayUploader({
3507
+ url: () => this.api.url("/replay"),
3508
+ fetch: () => this.options.fetch ?? (typeof fetch === "function" ? fetch : null),
3509
+ health: this.api.health,
3510
+ meta: () => {
3511
+ const rec2 = this.recorder;
3512
+ return {
3513
+ anonymousId: this.anonymousId,
3514
+ userId: this.user?.id || null,
3515
+ ...this.collectUrls() && this.screenName ? { url: this.currentUrl() } : {},
3516
+ device: { type: this.bridge.device.deviceType, os: this.bridge.device.platform },
3517
+ ...rec2 && rec2.viewportWidth > 0 && rec2.viewportHeight > 0 ? { viewport: { width: rec2.viewportWidth, height: rec2.viewportHeight } } : {},
3518
+ sdk: { name: SDK_NAME, version: SDK_VERSION }
3519
+ };
3520
+ },
3521
+ onDisabled: (sid) => {
3522
+ if (sid !== this.session.id) return;
3523
+ this.log(`replay disabled by the server for session ${sid}`);
3524
+ this.replayDecision = { sessionId: sid, record: false };
3525
+ this.recorder?.stop(true);
3526
+ },
3527
+ log: (m) => this.log(m)
3528
+ }));
3529
+ const opts = this.options.sessionReplay ?? {};
3530
+ const rec = new SessionRecorder({
3531
+ capturer,
3532
+ uploader,
3533
+ intervalMs: opts.captureIntervalMs ?? 1e3,
3534
+ captureOptions: () => this.replayCaptureOptions(),
3535
+ href: () => this.currentUrl(),
3536
+ onError: (err) => this.reportError(err)
3537
+ });
3538
+ if (this.backgroundedAt !== null) rec.pause();
3539
+ this.recorder = rec;
3540
+ return rec;
3541
+ }
3542
+ async replayCaptureOptions() {
3543
+ const r = this.config?.settings?.replay;
3544
+ const opts = this.options.sessionReplay ?? {};
3545
+ const ids = new Set((r?.blockSelectors ?? []).map((sel) => guidelyIdFromSelector(sel)).filter((id) => !!id));
3546
+ const rects = await Promise.all(Array.from(ids, (id) => this.targets.measure(id)));
3547
+ return { maskAllText: !!opts.maskAllText || !!r?.maskAllText, maskAllImages: !!opts.maskAllImages, blockRects: rects.filter((rect) => !!rect) };
3548
+ }
3549
+ /**
3550
+ * Records this session even when it was not sampled (still needs `sessionReplay.enabled`, replay
3551
+ * enabled for the project, analytics consent and the native replay module). Sticks until the session ends.
3552
+ */
3553
+ startSessionRecording() {
3554
+ this.whenHydrated(() => {
3555
+ this.replayDecision = { sessionId: this.session.id, record: true };
3556
+ this.syncReplay();
3557
+ });
3558
+ }
3559
+ /** Stops recording for the rest of this session; what was recorded is uploaded. */
3560
+ stopSessionRecording() {
3561
+ this.whenHydrated(() => {
3562
+ this.replayDecision = { sessionId: this.session.id, record: false };
3563
+ this.recorder?.stop(false);
3564
+ });
3565
+ }
3566
+ /** True while this session is being recorded. */
3567
+ isRecording() {
3568
+ return !!this.recorder?.isRecording;
3569
+ }
3570
+ /** The current analytics session id (also the replay id). */
3571
+ getSessionId() {
3572
+ return this.session.id;
3573
+ }
3574
+ /** True when touches should be passed to {@link recordTouch}. */
3575
+ isRecordingTouches() {
3576
+ return !!this.recorder?.isRecording;
3577
+ }
3578
+ /** A primary-pointer touch on the app, in window points (the provider's touch observer calls this). */
3579
+ recordTouch(phase, x, y) {
3580
+ this.recorder?.touch(phase, x, y);
3581
+ }
3582
+ // ---------------------------------------------------------------------------
3040
3583
  // Product analytics (autocapture)
3041
3584
  // ---------------------------------------------------------------------------
3042
3585
  captureAllowed() {
@@ -3083,7 +3626,7 @@ var GuidelyClient = class {
3083
3626
  }
3084
3627
  if (this.destroyed) return;
3085
3628
  const platform = this.bridge.device.platform;
3086
- const maskAll = !!this.config?.settings?.replay?.maskAllText;
3629
+ const maskAll = !!this.config?.settings?.replay?.maskAllText || !!this.options.sessionReplay?.maskAllText;
3087
3630
  const eventUrl = this.collectUrls() && url ? url : void 0;
3088
3631
  if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
3089
3632
  const count = this.rage.add(tap.x, tap.y, t);
@@ -3129,12 +3672,12 @@ var GuidelyClient = class {
3129
3672
  /** `{ appVersion, build }` of this launch, without unknown keys. */
3130
3673
  appInfo() {
3131
3674
  const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
3132
- const build = this.options.appBuild;
3675
+ const build = this.options.appBuild ?? (this.options.appVersion ? void 0 : this.bridge.device.appBuild);
3133
3676
  return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
3134
3677
  }
3135
3678
  /**
3136
3679
  * Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
3137
- * version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
3680
+ * version (when the app version is known and the app passes `storage`), then `app_open` with `coldStart: true`.
3138
3681
  */
3139
3682
  checkLaunch() {
3140
3683
  if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
@@ -3951,8 +4494,8 @@ var GuidelyClient = class {
3951
4494
  this.messenger?.close();
3952
4495
  }
3953
4496
  /** Opens a new messenger conversation, optionally pre-filling the message. */
3954
- showNewMessage(text) {
3955
- this.whenHydrated(() => this.messenger?.showNewMessage(text));
4497
+ showNewMessage(text2) {
4498
+ this.whenHydrated(() => this.messenger?.showNewMessage(text2));
3956
4499
  }
3957
4500
  // ---------------------------------------------------------------------------
3958
4501
  // Feature flags
@@ -3976,7 +4519,7 @@ var GuidelyClient = class {
3976
4519
  /** Device facts on every screen view, read by the Pages devices / browsers breakdowns. */
3977
4520
  deviceProperties() {
3978
4521
  const d = this.bridge.device;
3979
- return { platform: d.platform, deviceType: d.deviceType, os: d.platform };
4522
+ return { platform: d.platform, deviceType: d.deviceType, os: d.platform, ...d.osVersion ? { osVersion: d.osVersion } : {} };
3980
4523
  }
3981
4524
  /** Attributes flags are evaluated with: identity, traits, account, audiences, screen and device. */
3982
4525
  flagContext() {
@@ -3987,6 +4530,7 @@ var GuidelyClient = class {
3987
4530
  deviceType: d.deviceType,
3988
4531
  locale: d.locale,
3989
4532
  appVersion: this.options.appVersion ?? d.appVersion,
4533
+ osVersion: d.osVersion,
3990
4534
  environment: this.options.environment,
3991
4535
  ...this.screenName ? { screen: this.screenName, path: this.currentUrl() } : {},
3992
4536
  ...this.user?.traits ?? {},
@@ -4077,6 +4621,51 @@ function withoutCode(guide) {
4077
4621
  return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
4078
4622
  }
4079
4623
 
4624
+ // src/core/device.ts
4625
+ function text(value) {
4626
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
4627
+ if (typeof value !== "string") return void 0;
4628
+ const v = value.trim();
4629
+ return v ? v.slice(0, 64) : void 0;
4630
+ }
4631
+ function constant(mod, key) {
4632
+ if (!mod || typeof mod !== "object" && typeof mod !== "function") return void 0;
4633
+ const m = mod;
4634
+ try {
4635
+ const fromGetter = typeof m.getConstants === "function" ? m.getConstants()?.[key] : void 0;
4636
+ return fromGetter ?? m[key];
4637
+ } catch {
4638
+ return void 0;
4639
+ }
4640
+ }
4641
+ function read(mod, versionKey, buildKey) {
4642
+ const appVersion = text(constant(mod, versionKey));
4643
+ if (!appVersion) return null;
4644
+ const appBuild = text(constant(mod, buildKey));
4645
+ return { appVersion, ...appBuild ? { appBuild } : {} };
4646
+ }
4647
+ function inExpoGo(src) {
4648
+ const c = src.expoModules?.ExponentConstants ?? src.nativeModules.ExponentConstants;
4649
+ return constant(c, "executionEnvironment") === "storeClient" || constant(c, "appOwnership") === "expo";
4650
+ }
4651
+ function detectAppVersion(src) {
4652
+ try {
4653
+ const expo = inExpoGo(src) ? null : src.expoModules?.ExpoApplication ?? src.nativeModules.ExpoApplication;
4654
+ return read(src.nativeModules.GuidelyReplay, "appVersion", "appBuild") ?? read(expo, "nativeApplicationVersion", "nativeBuildVersion") ?? read(src.nativeModules.RNDeviceInfo, "appVersion", "buildNumber") ?? {};
4655
+ } catch {
4656
+ return {};
4657
+ }
4658
+ }
4659
+ function detectOsVersion(src) {
4660
+ try {
4661
+ const c = src.platform.constants ?? {};
4662
+ if (src.platform.OS === "android") return text(c.Release) ?? text(src.platform.Version);
4663
+ return text(c.osVersion) ?? text(src.platform.Version);
4664
+ } catch {
4665
+ return void 0;
4666
+ }
4667
+ }
4668
+
4080
4669
  // src/ui/Autocapture.tsx
4081
4670
  import { useMemo as useMemo5, useRef as useRef5 } from "react";
4082
4671
  import { View as View10 } from "react-native";
@@ -4208,8 +4797,8 @@ function describe(f, masked, opts, withText) {
4208
4797
  if (!sub.masked) {
4209
4798
  const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
4210
4799
  const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
4211
- const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4212
- if (text) el.text = text;
4800
+ const text2 = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4801
+ if (text2) el.text = text2;
4213
4802
  }
4214
4803
  }
4215
4804
  if (host) el.measure = () => measureHost(host);
@@ -4365,17 +4954,17 @@ function parseHtml(html) {
4365
4954
  flush();
4366
4955
  current = { kind, level, marker, runs: [] };
4367
4956
  };
4368
- const push = (text) => {
4369
- if (!text) return;
4957
+ const push = (text2) => {
4958
+ if (!text2) return;
4370
4959
  if (!current) current = { kind: "paragraph", level: 1, runs: [] };
4371
- const run = { text };
4960
+ const run = { text: text2 };
4372
4961
  if (style.bold) run.bold = true;
4373
4962
  if (style.italic) run.italic = true;
4374
4963
  if (style.underline) run.underline = true;
4375
4964
  if (style.code) run.code = true;
4376
4965
  if (hrefs.length) run.href = hrefs[hrefs.length - 1];
4377
4966
  const last = current.runs[current.runs.length - 1];
4378
- if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text;
4967
+ if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text2;
4379
4968
  else current.runs.push(run);
4380
4969
  };
4381
4970
  const size = (attrs, name) => {
@@ -4391,18 +4980,18 @@ function parseHtml(html) {
4391
4980
  const re = /<!--[\s\S]*?-->|<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>|([^<]+|<)/g;
4392
4981
  let m;
4393
4982
  while (m = re.exec(html || "")) {
4394
- const [token, rawTag, attrs = "", text] = m;
4395
- if (text !== void 0) {
4983
+ const [token, rawTag, attrs = "", text2] = m;
4984
+ if (text2 !== void 0) {
4396
4985
  if (skipping) continue;
4397
4986
  if (pdf) {
4398
- if (pdf.inLink) pdf.name += decodeEntities(text);
4987
+ if (pdf.inLink) pdf.name += decodeEntities(text2);
4399
4988
  continue;
4400
4989
  }
4401
4990
  if (fileLink) {
4402
- fileLink.name += decodeEntities(text);
4991
+ fileLink.name += decodeEntities(text2);
4403
4992
  continue;
4404
4993
  }
4405
- push(decodeEntities(text.replace(/\s+/g, " ")));
4994
+ push(decodeEntities(text2.replace(/\s+/g, " ")));
4406
4995
  continue;
4407
4996
  }
4408
4997
  if (!rawTag) continue;
@@ -4976,13 +5565,13 @@ function Conversations({ store, palette: p }) {
4976
5565
  /* @__PURE__ */ jsx5(GButton, { label: "New conversation", palette: p, onPress: () => store.showNewMessage(), style: styles4.fullWidth })
4977
5566
  ] });
4978
5567
  }
4979
- function Bubble({ m, text, name, palette: p, onArticle, onGuide }) {
5568
+ function Bubble({ m, text: text2, name, palette: p, onArticle, onGuide }) {
4980
5569
  if (m.author === "system")
4981
- return /* @__PURE__ */ jsx5(Text4, { accessibilityRole: "text", style: [styles4.system, { color: p.muted }], children: text });
5570
+ return /* @__PURE__ */ jsx5(Text4, { accessibilityRole: "text", style: [styles4.system, { color: p.muted }], children: text2 });
4982
5571
  const mine = m.author === "user";
4983
5572
  return /* @__PURE__ */ jsxs3(View4, { style: [styles4.bubbleWrap, mine ? styles4.mine : styles4.theirs], children: [
4984
5573
  !mine ? /* @__PURE__ */ jsx5(Text4, { style: [styles4.author, { color: p.muted }], children: m.author === "agent" ? m.authorName || "Team" : name }) : null,
4985
- /* @__PURE__ */ jsx5(View4, { style: [styles4.bubble, { borderRadius: Math.max(12, p.radius) }, mine ? { backgroundColor: p.primary } : { backgroundColor: p.surface }], children: m.status === "pending" && !text ? /* @__PURE__ */ jsx5(TypingDots, { color: p.muted }) : /* @__PURE__ */ jsx5(Text4, { style: [styles4.bubbleText, { color: mine ? p.onPrimary : p.text }], children: text }) }),
5574
+ /* @__PURE__ */ jsx5(View4, { style: [styles4.bubble, { borderRadius: Math.max(12, p.radius) }, mine ? { backgroundColor: p.primary } : { backgroundColor: p.surface }], children: m.status === "pending" && !text2 ? /* @__PURE__ */ jsx5(TypingDots, { color: p.muted }) : /* @__PURE__ */ jsx5(Text4, { style: [styles4.bubbleText, { color: mine ? p.onPrimary : p.text }], children: text2 }) }),
4986
5575
  m.citations.length || m.suggestedGuides.length ? /* @__PURE__ */ jsxs3(View4, { style: styles4.chips, children: [
4987
5576
  m.citations.map((c) => /* @__PURE__ */ jsx5(Pressable4, { accessibilityRole: "link", accessibilityLabel: `Article: ${c.title}`, onPress: () => onArticle(c.articleId), style: [styles4.chip, { borderColor: p.border }], children: /* @__PURE__ */ jsxs3(Text4, { style: [styles4.chipText, { color: p.text }], children: [
4988
5577
  "\u{1F4C4} ",
@@ -5001,7 +5590,7 @@ function TypingDots({ color }) {
5001
5590
  function Thread({ store, palette: p, bottomInset }) {
5002
5591
  const snap = useMessenger(store);
5003
5592
  const t = snap.thread;
5004
- const [text, setText] = useState3(t?.draft ?? "");
5593
+ const [text2, setText] = useState3(t?.draft ?? "");
5005
5594
  const scroll = useRef2(null);
5006
5595
  const convo = t?.conversation ?? null;
5007
5596
  useEffect3(() => {
@@ -5010,7 +5599,7 @@ function Thread({ store, palette: p, bottomInset }) {
5010
5599
  const streamingIds = useMemo3(() => Object.keys(t?.streaming ?? {}).filter((id) => !t?.messages.some((m) => m.id === id)), [t]);
5011
5600
  if (!t) return null;
5012
5601
  const send = () => {
5013
- const value = text;
5602
+ const value = text2;
5014
5603
  setText("");
5015
5604
  void store.send(value);
5016
5605
  };
@@ -5032,7 +5621,7 @@ function Thread({ store, palette: p, bottomInset }) {
5032
5621
  /* @__PURE__ */ jsx5(
5033
5622
  TextInput,
5034
5623
  {
5035
- value: text,
5624
+ value: text2,
5036
5625
  onChangeText: (v) => setText(v.slice(0, 4e3)),
5037
5626
  placeholder: convo ? "Reply\u2026" : "Write a message\u2026",
5038
5627
  placeholderTextColor: p.muted,
@@ -5041,7 +5630,7 @@ function Thread({ store, palette: p, bottomInset }) {
5041
5630
  style: [styles4.input, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(18, p.radius + 6) }]
5042
5631
  }
5043
5632
  ),
5044
- /* @__PURE__ */ jsx5(GButton, { label: "Send", palette: p, onPress: send, disabled: !text.trim() || t.sending })
5633
+ /* @__PURE__ */ jsx5(GButton, { label: "Send", palette: p, onPress: send, disabled: !text2.trim() || t.sending })
5045
5634
  ] })
5046
5635
  ] });
5047
5636
  }
@@ -5955,15 +6544,15 @@ function palette(theme, systemScheme, explicit) {
5955
6544
  const wantsDark = theme.dark === "dark" || theme.dark === "auto" && systemScheme === "dark";
5956
6545
  const dark = wantsDark && !explicit?.backgroundColor;
5957
6546
  const background = dark ? DARK.background : theme.backgroundColor ?? "#ffffff";
5958
- const text = dark ? DARK.text : theme.textColor ?? "#111827";
6547
+ const text2 = dark ? DARK.text : theme.textColor ?? "#111827";
5959
6548
  return {
5960
6549
  primary,
5961
6550
  onPrimary: luminance(primary) > 0.45 ? "#111827" : "#ffffff",
5962
6551
  background,
5963
- text,
5964
- muted: dark ? DARK.muted : mix(text, 0.65),
5965
- border: dark ? DARK.border : mix(text, 0.12),
5966
- surface: dark ? DARK.surface : mix(text, 0.05),
6552
+ text: text2,
6553
+ muted: dark ? DARK.muted : mix(text2, 0.65),
6554
+ border: dark ? DARK.border : mix(text2, 0.12),
6555
+ surface: dark ? DARK.surface : mix(text2, 0.05),
5967
6556
  radius: theme.borderRadius ?? 12,
5968
6557
  overlay: `rgba(15, 23, 42, ${theme.overlayOpacity ?? 0.55})`,
5969
6558
  fontFamily: nativeFont(theme.fontFamily)
@@ -6052,8 +6641,9 @@ function rcLauncherLabel(client) {
6052
6641
 
6053
6642
  // src/ui/Autocapture.tsx
6054
6643
  import { jsx as jsx11 } from "react/jsx-runtime";
6644
+ var GUIDELY_MASK_NATIVE_ID = "guidely-mask";
6055
6645
  function GuidelyMask({ children, ...rest }) {
6056
- return /* @__PURE__ */ jsx11(View10, { ...rest, children });
6646
+ return /* @__PURE__ */ jsx11(View10, { collapsable: false, ...rest, nativeID: GUIDELY_MASK_NATIVE_ID, children });
6057
6647
  }
6058
6648
  var RESOLVE = {
6059
6649
  isIgnored: (type) => type === GuidelyOverlay,
@@ -6064,6 +6654,18 @@ function useTapObserver(client) {
6064
6654
  clientRef.current = client;
6065
6655
  return useMemo5(() => {
6066
6656
  const tracker = new TapTracker();
6657
+ let replayPointer = null;
6658
+ const replay = (phase, e) => {
6659
+ const c = clientRef.current;
6660
+ const n = e.nativeEvent;
6661
+ if (!c?.isRecordingTouches()) return void (replayPointer = null);
6662
+ if (phase === "down") {
6663
+ if (replayPointer !== null) return;
6664
+ replayPointer = n.identifier;
6665
+ } else if (n.identifier !== replayPointer) return;
6666
+ if (phase === "up" || phase === "cancel") replayPointer = null;
6667
+ c.recordTouch(phase, n.pageX, n.pageY);
6668
+ };
6067
6669
  const guard = (fn) => (e) => {
6068
6670
  try {
6069
6671
  fn(e);
@@ -6073,12 +6675,17 @@ function useTapObserver(client) {
6073
6675
  };
6074
6676
  return {
6075
6677
  onTouchStartCapture: guard((e) => {
6678
+ replay("down", e);
6076
6679
  const n = e.nativeEvent;
6077
6680
  if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
6078
6681
  tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
6079
6682
  }),
6080
- onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
6683
+ onTouchMoveCapture: guard((e) => {
6684
+ replay("move", e);
6685
+ tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier);
6686
+ }),
6081
6687
  onTouchEndCapture: guard((e) => {
6688
+ replay("up", e);
6082
6689
  const n = e.nativeEvent;
6083
6690
  const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
6084
6691
  const c = clientRef.current;
@@ -6086,7 +6693,10 @@ function useTapObserver(client) {
6086
6693
  const r = resolveTapTarget(e, RESOLVE);
6087
6694
  if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
6088
6695
  }),
6089
- onTouchCancelCapture: guard(() => tracker.cancel())
6696
+ onTouchCancelCapture: guard((e) => {
6697
+ replay("cancel", e);
6698
+ tracker.cancel();
6699
+ })
6090
6700
  };
6091
6701
  }, []);
6092
6702
  }
@@ -6169,20 +6779,37 @@ function deviceLocale() {
6169
6779
  return void 0;
6170
6780
  }
6171
6781
  }
6172
- function defaultBridge(appVersion) {
6782
+ function nativeSources() {
6783
+ return {
6784
+ platform: Platform6,
6785
+ nativeModules: NativeModules,
6786
+ expoModules: globalThis.expo?.modules
6787
+ };
6788
+ }
6789
+ function defaultBridge(appVersion, appBuild) {
6173
6790
  const { width, height } = Dimensions.get("window");
6791
+ const sources = nativeSources();
6792
+ const detected = appVersion ? {} : detectAppVersion(sources);
6174
6793
  return {
6175
6794
  device: {
6176
6795
  platform: Platform6.OS === "android" ? "android" : "ios",
6177
6796
  deviceType: Platform6.OS === "ios" && Platform6.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
6178
6797
  locale: deviceLocale(),
6179
- appVersion
6798
+ appVersion: appVersion ?? detected.appVersion,
6799
+ appBuild: appBuild ?? detected.appBuild,
6800
+ osVersion: detectOsVersion(sources)
6180
6801
  },
6181
6802
  openExternal: (url) => void Linking.openURL(url).catch(() => {
6182
6803
  }),
6183
- windowSize: () => Dimensions.get("window")
6804
+ windowSize: () => Dimensions.get("window"),
6805
+ capturer: nativeCapturer()
6184
6806
  };
6185
6807
  }
6808
+ function nativeCapturer() {
6809
+ const mod = NativeModules.GuidelyReplay;
6810
+ if (!mod || typeof mod.capture !== "function") return void 0;
6811
+ return (options) => mod.capture(options);
6812
+ }
6186
6813
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
6187
6814
  const [client, setClient] = useState8(null);
6188
6815
  const optionsRef = useRef7(options);
@@ -6215,7 +6842,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6215
6842
  onNavigate: (url) => optionsRef.current.onNavigate?.(url),
6216
6843
  onError: (err) => optionsRef.current.onError?.(err)
6217
6844
  },
6218
- defaultBridge(latest.appVersion)
6845
+ defaultBridge(latest.appVersion, latest.appBuild)
6219
6846
  );
6220
6847
  } catch (err) {
6221
6848
  optionsRef.current.onError?.(err instanceof Error ? err : new Error(String(err)));
@@ -6252,7 +6879,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6252
6879
  const flags = useMemo6(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
6253
6880
  const tapObserver = useTapObserver(client);
6254
6881
  return /* @__PURE__ */ jsx13(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ jsxs9(FlagsContext.Provider, { value: flags, children: [
6255
- options.autocapture === false ? children : /* @__PURE__ */ jsx13(View12, { style: styles10.root, ...tapObserver, children }),
6882
+ options.autocapture === false && !options.sessionReplay?.enabled ? children : /* @__PURE__ */ jsx13(View12, { style: styles10.root, ...tapObserver, children }),
6256
6883
  client ? /* @__PURE__ */ jsx13(GuidelyOverlay, { client, insets, launcherOffset }) : null
6257
6884
  ] }) });
6258
6885
  }
@@ -6272,6 +6899,9 @@ var NOOP_RESULTS = {
6272
6899
  getMessenger: null,
6273
6900
  isGuideCompleted: false,
6274
6901
  isClientReady: false,
6902
+ isRecording: false,
6903
+ isRecordingTouches: false,
6904
+ getSessionId: "",
6275
6905
  getScreen: "",
6276
6906
  on: () => {
6277
6907
  },
@@ -6405,12 +7035,17 @@ export {
6405
7035
  InMemoryStickyBucketStore,
6406
7036
  Messenger,
6407
7037
  RageClickDetector,
7038
+ ReplayUploader,
7039
+ RrwebEvents,
6408
7040
  SDK_NAME,
6409
7041
  SDK_VERSION,
7042
+ SessionRecorder,
6410
7043
  TapTracker,
6411
7044
  TargetRegistry,
6412
7045
  createFlagsClient3 as createFlagsClient,
6413
7046
  createSseParser,
7047
+ detectAppVersion,
7048
+ detectOsVersion,
6414
7049
  guidelyScreenLayout,
6415
7050
  htmlToText,
6416
7051
  noopClient,