@guidelytech/react-native 0.11.0 → 0.13.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.cjs CHANGED
@@ -37,8 +37,11 @@ __export(index_exports, {
37
37
  InMemoryStickyBucketStore: () => import_flags6.InMemoryStickyBucketStore,
38
38
  Messenger: () => Messenger,
39
39
  RageClickDetector: () => RageClickDetector,
40
+ ReplayUploader: () => ReplayUploader,
41
+ RrwebEvents: () => RrwebEvents,
40
42
  SDK_NAME: () => SDK_NAME,
41
43
  SDK_VERSION: () => SDK_VERSION,
44
+ SessionRecorder: () => SessionRecorder,
42
45
  TapTracker: () => TapTracker,
43
46
  TargetRegistry: () => TargetRegistry,
44
47
  createFlagsClient: () => import_flags6.createFlagsClient,
@@ -160,7 +163,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
160
163
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagsContext.Provider, { value, children });
161
164
  }
162
165
 
163
- // ../shared/dist/chunk-TGIUEYXM.js
166
+ // ../shared/dist/chunk-XJXXVV3C.js
164
167
  function fnv1a(input) {
165
168
  let h = 2166136261;
166
169
  for (let i = 0; i < input.length; i++) {
@@ -470,7 +473,7 @@ function sanitizeProperties(props) {
470
473
  }
471
474
  return out;
472
475
  }
473
- var SOURCEMAP_MAX_FILE_BYTES = 25 * 1024 * 1024;
476
+ var SOURCEMAP_MAX_FILE_BYTES = 100 * 1024 * 1024;
474
477
  var MAX_COMPLAINT_ATTACHMENT_BYTES = 2 * 1024 * 1024;
475
478
  var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
476
479
  var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
@@ -765,7 +768,7 @@ function localizeResourceCenter(rc, requested, defaultLocale = DEFAULT_LOCALE) {
765
768
  };
766
769
  }
767
770
 
768
- // ../sdk-core/dist/chunk-JUSM7J5N.js
771
+ // ../sdk-core/dist/chunk-XML74K2P.js
769
772
  var __defProp2 = Object.defineProperty;
770
773
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
771
774
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -1074,10 +1077,12 @@ var KEYS = {
1074
1077
  /** Messenger conversations this browser can read: [{ id, token }]. */
1075
1078
  conversations: (key) => `guidely:conversations:${key}`,
1076
1079
  /** Announcement ids opened in the messenger's News tab. */
1077
- newsSeen: (key) => `guidely:news:${key}`
1080
+ newsSeen: (key) => `guidely:news:${key}`,
1081
+ /** Where the visitor dragged the messenger launcher ({ side, y }). */
1082
+ launcher: (key) => `guidely:launcher:${key}`
1078
1083
  };
1079
1084
 
1080
- // ../sdk-core/dist/chunk-ZAF2KRJA.js
1085
+ // ../sdk-core/dist/chunk-2E2YUQAG.js
1081
1086
  var EventQueue = class {
1082
1087
  constructor(opts) {
1083
1088
  __publicField2(this, "opts", opts);
@@ -1387,6 +1392,49 @@ function dismissedAfterMerge(local, server) {
1387
1392
  // src/core/client.ts
1388
1393
  var import_flags2 = require("@guidelytech/flags");
1389
1394
 
1395
+ // src/core/launcherPlacement.ts
1396
+ var LAUNCHER_PLACEMENT_KEY = "guidely:messenger.launcher";
1397
+ var LAUNCHER_MARGIN = 16;
1398
+ var LAUNCHER_DRAG_THRESHOLD = 8;
1399
+ var clamp2 = (v, min, max) => Math.min(max, Math.max(min, v));
1400
+ function sideX(f, side) {
1401
+ return side === "left" ? f.insets.left + LAUNCHER_MARGIN : f.width - f.insets.right - LAUNCHER_MARGIN - f.size;
1402
+ }
1403
+ function verticalRange(f) {
1404
+ const max = f.height - f.defaultBottom - f.size;
1405
+ return { min: Math.min(max, f.insets.top + LAUNCHER_MARGIN), max };
1406
+ }
1407
+ function clampToSafeArea(f, p) {
1408
+ const left = f.insets.left + LAUNCHER_MARGIN;
1409
+ const top = f.insets.top + LAUNCHER_MARGIN;
1410
+ return {
1411
+ x: clamp2(p.x, left, Math.max(left, f.width - f.insets.right - LAUNCHER_MARGIN - f.size)),
1412
+ y: clamp2(p.y, top, Math.max(top, f.height - f.insets.bottom - LAUNCHER_MARGIN - f.size))
1413
+ };
1414
+ }
1415
+ function snapPlacement(f, p) {
1416
+ const { min, max } = verticalRange(f);
1417
+ const side = p.x + f.size / 2 < f.width / 2 ? "left" : "right";
1418
+ return { side, fraction: max > min ? (max - clamp2(p.y, min, max)) / (max - min) : 0 };
1419
+ }
1420
+ function placementPoint(f, placement) {
1421
+ const { min, max } = verticalRange(f);
1422
+ return { x: sideX(f, placement.side), y: max - clamp2(placement.fraction, 0, 1) * (max - min) };
1423
+ }
1424
+ function parsePlacement(raw) {
1425
+ if (!raw) return null;
1426
+ try {
1427
+ const v = JSON.parse(raw);
1428
+ if (!v || typeof v !== "object" || v.side !== "left" && v.side !== "right" || typeof v.fraction !== "number" || !Number.isFinite(v.fraction)) return null;
1429
+ return { side: v.side, fraction: clamp2(v.fraction, 0, 1) };
1430
+ } catch {
1431
+ return null;
1432
+ }
1433
+ }
1434
+ function serializePlacement(p) {
1435
+ return JSON.stringify({ side: p.side, fraction: Math.round(clamp2(p.fraction, 0, 1) * 1e4) / 1e4 });
1436
+ }
1437
+
1390
1438
  // src/core/autocapture.ts
1391
1439
  var TAP_SLOP = 10;
1392
1440
  var TAP_MAX_MS = 1e3;
@@ -2447,6 +2495,425 @@ var Messenger = class {
2447
2495
  }
2448
2496
  };
2449
2497
 
2498
+ // src/core/replay.ts
2499
+ var RrwebEvents = {
2500
+ IMG_ID: 8,
2501
+ TOUCH_START: 7,
2502
+ TOUCH_END: 9,
2503
+ CLICK: 2,
2504
+ 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}",
2505
+ dataUrl(base64) {
2506
+ return `data:image/jpeg;base64,${base64}`;
2507
+ },
2508
+ meta(timestamp, href, width, height) {
2509
+ return JSON.stringify({ type: 4, data: { href, width, height }, timestamp });
2510
+ },
2511
+ fullSnapshot(timestamp, dataUrl, width, height) {
2512
+ const style = { type: 2, id: 5, tagName: "style", attributes: {}, childNodes: [{ type: 3, id: 6, textContent: RrwebEvents.CSS, isStyle: true }] };
2513
+ const img = { type: 2, id: 8, tagName: "img", attributes: { src: dataUrl, width, height, alt: "" }, childNodes: [] };
2514
+ 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] }] };
2515
+ const node = { type: 0, id: 1, childNodes: [{ type: 1, id: 2, name: "html", publicId: "", systemId: "" }, html] };
2516
+ return JSON.stringify({ type: 2, data: { node, initialOffset: { left: 0, top: 0 } }, timestamp });
2517
+ },
2518
+ frame(timestamp, dataUrl) {
2519
+ return JSON.stringify({ type: 3, data: { source: 0, texts: [], attributes: [{ id: 8, attributes: { src: dataUrl } }], removes: [], adds: [] }, timestamp });
2520
+ },
2521
+ /** MouseInteraction (source 2): 7 TouchStart, 9 TouchEnd, 2 Click. */
2522
+ interaction(timestamp, type, x, y) {
2523
+ return JSON.stringify({ type: 3, data: { source: 2, type, id: 8, x: round1(x), y: round1(y) }, timestamp });
2524
+ },
2525
+ touchMove(timestamp, x, y) {
2526
+ return JSON.stringify({ type: 3, data: { source: 6, positions: [{ x: round1(x), y: round1(y), id: 8, timeOffset: 0 }] }, timestamp });
2527
+ },
2528
+ custom(timestamp, tag, payload) {
2529
+ return JSON.stringify({ type: 5, data: { tag, payload }, timestamp });
2530
+ }
2531
+ };
2532
+ var round1 = (v) => Math.round(v * 10) / 10;
2533
+ var UPLOAD_TIMEOUT_MS = 3e4;
2534
+ var ReplayUploader = class {
2535
+ constructor(opts) {
2536
+ __publicField(this, "opts", opts);
2537
+ __publicField(this, "windowId", randomId(16));
2538
+ __publicField(this, "buffer", null);
2539
+ __publicField(this, "queue", []);
2540
+ __publicField(this, "queuedBytes", 0);
2541
+ __publicField(this, "seqs", /* @__PURE__ */ new Map());
2542
+ __publicField(this, "inflight", null);
2543
+ __publicField(this, "flushTimer", null);
2544
+ __publicField(this, "retryTimer", null);
2545
+ __publicField(this, "attempt", 0);
2546
+ __publicField(this, "destroyed", false);
2547
+ __publicField(this, "offRecover");
2548
+ this.offRecover = opts.health?.onRecover?.(() => {
2549
+ this.clearRetry();
2550
+ this.attempt = 0;
2551
+ this.flush();
2552
+ }) ?? null;
2553
+ }
2554
+ get pendingChunks() {
2555
+ return this.queue.length;
2556
+ }
2557
+ get bufferedEvents() {
2558
+ return this.buffer?.events.length ?? 0;
2559
+ }
2560
+ push(sessionId, json) {
2561
+ if (this.destroyed) return;
2562
+ if (this.buffer && this.buffer.sessionId !== sessionId) this.seal();
2563
+ const chunk = this.buffer ?? (this.buffer = { sessionId, meta: this.opts.meta(), events: [], bytes: 0 });
2564
+ chunk.events.push(json);
2565
+ chunk.bytes += json.length;
2566
+ if (chunk.bytes > (this.opts.maxBufferBytes ?? 1e6)) this.flush();
2567
+ else if (!this.flushTimer) {
2568
+ this.flushTimer = setTimeout(() => {
2569
+ this.flushTimer = null;
2570
+ this.flush();
2571
+ }, this.opts.flushIntervalMs ?? 1e4);
2572
+ }
2573
+ }
2574
+ /** Seals the buffer and uploads the backlog (one chunk at a time). */
2575
+ flush() {
2576
+ if (this.destroyed) return;
2577
+ this.seal();
2578
+ if (this.retryTimer || this.inflight || !this.queue.length) return;
2579
+ void this.sendNext();
2580
+ }
2581
+ /** Drops everything buffered or queued (consent withdrawn, server disabled replay). */
2582
+ discard() {
2583
+ this.buffer = null;
2584
+ this.queue = [];
2585
+ this.queuedBytes = 0;
2586
+ this.clearFlushTimer();
2587
+ this.clearRetry();
2588
+ this.attempt = 0;
2589
+ }
2590
+ /** Uploads what is left once; no retries afterwards. */
2591
+ destroy() {
2592
+ this.flush();
2593
+ this.destroyed = true;
2594
+ this.offRecover?.();
2595
+ this.clearFlushTimer();
2596
+ this.clearRetry();
2597
+ }
2598
+ clearFlushTimer() {
2599
+ if (this.flushTimer) clearTimeout(this.flushTimer);
2600
+ this.flushTimer = null;
2601
+ }
2602
+ clearRetry() {
2603
+ if (this.retryTimer) clearTimeout(this.retryTimer);
2604
+ this.retryTimer = null;
2605
+ }
2606
+ scheduleRetry(ms) {
2607
+ this.retryTimer = setTimeout(() => {
2608
+ this.retryTimer = null;
2609
+ this.flush();
2610
+ }, ms);
2611
+ }
2612
+ seal() {
2613
+ this.clearFlushTimer();
2614
+ const chunk = this.buffer;
2615
+ this.buffer = null;
2616
+ if (!chunk || !chunk.events.length) return;
2617
+ this.queue.push(chunk);
2618
+ this.queuedBytes += chunk.bytes;
2619
+ const max = this.opts.maxQueuedBytes ?? 5e6;
2620
+ while (this.queuedBytes > max && this.queue.length > 1) {
2621
+ const i = this.inflight && this.queue[0] === this.inflight ? 1 : 0;
2622
+ const [dropped] = this.queue.splice(i, 1);
2623
+ if (!dropped) break;
2624
+ this.queuedBytes -= dropped.bytes;
2625
+ this.opts.log?.(`replay backlog full: dropped a chunk of ${dropped.events.length} events`);
2626
+ }
2627
+ }
2628
+ nextSeq(chunk) {
2629
+ if (chunk.seq === void 0) {
2630
+ const seq = this.seqs.get(chunk.sessionId) ?? 0;
2631
+ this.seqs.set(chunk.sessionId, seq + 1);
2632
+ chunk.seq = seq;
2633
+ }
2634
+ return chunk.seq;
2635
+ }
2636
+ body(chunk) {
2637
+ const head = JSON.stringify({ sessionId: chunk.sessionId, windowId: this.windowId, seq: this.nextSeq(chunk), ...chunk.meta });
2638
+ return `${head.slice(0, -1)},"events":[${chunk.events.join(",")}]}`;
2639
+ }
2640
+ async sendNext() {
2641
+ const chunk = this.queue[0];
2642
+ if (!chunk) return;
2643
+ const wait = this.destroyed ? 0 : this.opts.health?.waitMs() ?? 0;
2644
+ if (wait > 0) return this.scheduleRetry(wait);
2645
+ this.inflight = chunk;
2646
+ const result = await this.upload(this.body(chunk));
2647
+ this.onResult(chunk, result);
2648
+ }
2649
+ async upload(json) {
2650
+ const f = this.opts.fetch();
2651
+ if (!f) return "drop";
2652
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
2653
+ const timer = ac ? setTimeout(() => ac.abort(), UPLOAD_TIMEOUT_MS) : null;
2654
+ try {
2655
+ let res;
2656
+ try {
2657
+ res = await f(this.opts.url(), { method: "POST", body: json, headers: { "Content-Type": "application/json" }, signal: ac?.signal });
2658
+ } catch {
2659
+ this.opts.health?.failure();
2660
+ return "retry";
2661
+ }
2662
+ if (res.status === 429 || res.status >= 500) this.opts.health?.failure();
2663
+ else this.opts.health?.success();
2664
+ if (res.ok) {
2665
+ const data = await res.json().catch(() => null);
2666
+ return data?.disabled ? "disabled" : "ok";
2667
+ }
2668
+ return res.status >= 400 && res.status < 500 && res.status !== 429 ? "drop" : "retry";
2669
+ } finally {
2670
+ if (timer) clearTimeout(timer);
2671
+ }
2672
+ }
2673
+ onResult(chunk, result) {
2674
+ if (this.inflight === chunk) this.inflight = null;
2675
+ if (result === "retry" && this.queue.includes(chunk)) {
2676
+ if (!this.destroyed) {
2677
+ const max = this.opts.maxAttempts ?? 5;
2678
+ this.attempt = Math.min(this.attempt + 1, max);
2679
+ const delay = this.attempt >= max ? this.opts.slowRetryMs ?? 6e4 : backoffMs(this.attempt - 1, 1e3, 8e3);
2680
+ return this.scheduleRetry(Math.max(delay, this.opts.health?.waitMs() ?? 0));
2681
+ }
2682
+ this.opts.log?.("replay upload failed after the recorder stopped; dropping the chunk");
2683
+ }
2684
+ this.attempt = 0;
2685
+ this.remove(chunk);
2686
+ if (result === "disabled") {
2687
+ for (const c of this.queue.slice()) if (c.sessionId === chunk.sessionId) this.remove(c);
2688
+ if (this.buffer?.sessionId === chunk.sessionId) this.buffer = null;
2689
+ this.opts.onDisabled(chunk.sessionId);
2690
+ }
2691
+ if (this.queue.length && !this.retryTimer && !this.inflight) void this.sendNext();
2692
+ }
2693
+ remove(chunk) {
2694
+ const i = this.queue.indexOf(chunk);
2695
+ if (i >= 0) {
2696
+ this.queue.splice(i, 1);
2697
+ this.queuedBytes -= chunk.bytes;
2698
+ }
2699
+ }
2700
+ };
2701
+ var TOUCH_SETTLE_MS = 200;
2702
+ var IDLE_FRAMES = 3;
2703
+ var MAX_IDLE_INTERVAL_MS = 5e3;
2704
+ var SessionRecorder = class {
2705
+ constructor(opts) {
2706
+ __publicField(this, "opts", opts);
2707
+ __publicField(this, "intervalMs");
2708
+ __publicField(this, "now");
2709
+ __publicField(this, "sid", null);
2710
+ __publicField(this, "foreground", true);
2711
+ __publicField(this, "generation", 0);
2712
+ __publicField(this, "timer", null);
2713
+ __publicField(this, "timerDue", 0);
2714
+ __publicField(this, "capturing", false);
2715
+ __publicField(this, "needKeyframe", true);
2716
+ __publicField(this, "hasKeyframe", false);
2717
+ __publicField(this, "lastFrame", null);
2718
+ __publicField(this, "unchanged", 0);
2719
+ __publicField(this, "captureSoon", false);
2720
+ __publicField(this, "pendingCustom", []);
2721
+ __publicField(this, "touchDown", false);
2722
+ __publicField(this, "downX", 0);
2723
+ __publicField(this, "downY", 0);
2724
+ __publicField(this, "downAt", 0);
2725
+ __publicField(this, "maxDistance", 0);
2726
+ __publicField(this, "lastMoveAt", 0);
2727
+ __publicField(this, "originX", 0);
2728
+ __publicField(this, "originY", 0);
2729
+ /** Viewport of the last frame (points), for chunk metadata. */
2730
+ __publicField(this, "viewportWidth", 0);
2731
+ __publicField(this, "viewportHeight", 0);
2732
+ this.intervalMs = Math.max(500, opts.intervalMs);
2733
+ this.now = opts.now ?? Date.now;
2734
+ }
2735
+ get sessionId() {
2736
+ return this.sid;
2737
+ }
2738
+ get isRecording() {
2739
+ return this.sid !== null;
2740
+ }
2741
+ get currentIntervalMs() {
2742
+ return this.unchanged >= IDLE_FRAMES ? Math.max(this.intervalMs, Math.min(4 * this.intervalMs, MAX_IDLE_INTERVAL_MS)) : this.intervalMs;
2743
+ }
2744
+ /** Starts (or keeps) recording `sessionId`; another session stops the current one first (uploading it). */
2745
+ start(sessionId) {
2746
+ if (this.sid === sessionId) return;
2747
+ if (this.sid !== null) this.stop(false);
2748
+ this.sid = sessionId;
2749
+ this.generation++;
2750
+ this.needKeyframe = true;
2751
+ this.hasKeyframe = false;
2752
+ this.lastFrame = null;
2753
+ this.unchanged = 0;
2754
+ this.captureSoon = false;
2755
+ this.pendingCustom = [];
2756
+ this.touchDown = false;
2757
+ if (this.foreground) this.schedule(0);
2758
+ }
2759
+ /** Stops recording; buffered events are uploaded unless `discard`. */
2760
+ stop(discard) {
2761
+ if (this.sid === null) return;
2762
+ this.sid = null;
2763
+ this.generation++;
2764
+ this.clearTimer();
2765
+ this.capturing = false;
2766
+ this.pendingCustom = [];
2767
+ this.touchDown = false;
2768
+ if (discard) this.opts.uploader.discard();
2769
+ else this.opts.uploader.flush();
2770
+ }
2771
+ /** App backgrounded: no captures; uploads what is buffered. */
2772
+ pause() {
2773
+ this.foreground = false;
2774
+ this.clearTimer();
2775
+ this.generation++;
2776
+ this.capturing = false;
2777
+ this.touchDown = false;
2778
+ this.opts.uploader.flush();
2779
+ }
2780
+ /** App foregrounded: resume with a keyframe. */
2781
+ resume() {
2782
+ if (this.foreground) return;
2783
+ this.foreground = true;
2784
+ this.needKeyframe = true;
2785
+ this.unchanged = 0;
2786
+ if (this.sid !== null) this.schedule(0);
2787
+ }
2788
+ /** A new screen: the next frame is a keyframe with the new href. */
2789
+ onScreen() {
2790
+ if (this.sid === null) return;
2791
+ this.needKeyframe = true;
2792
+ this.activity();
2793
+ }
2794
+ activity() {
2795
+ const wasIdle = this.unchanged >= IDLE_FRAMES;
2796
+ this.unchanged = 0;
2797
+ if (wasIdle && this.sid !== null && this.foreground && !this.capturing && !this.touchDown) this.schedule(this.intervalMs, true);
2798
+ }
2799
+ /** Adds the `guidely` marker for an analytics event. */
2800
+ addGuidelyEvent(name, customName, guideId) {
2801
+ const id = this.sid;
2802
+ if (id === null) return;
2803
+ const payload = { name };
2804
+ if (customName) payload.customName = customName;
2805
+ if (guideId) payload.guideId = guideId;
2806
+ this.activity();
2807
+ const now = this.now();
2808
+ if (!this.hasKeyframe) {
2809
+ if (this.pendingCustom.length < 50) this.pendingCustom.push([now, payload]);
2810
+ return;
2811
+ }
2812
+ this.opts.uploader.push(id, RrwebEvents.custom(now, "guidely", payload));
2813
+ }
2814
+ /** A primary-pointer touch at (x, y) points from the React root view. */
2815
+ touch(phase, rootX, rootY) {
2816
+ const id = this.sid;
2817
+ if (id === null || !this.foreground) return;
2818
+ const x = rootX + this.originX;
2819
+ const y = rootY + this.originY;
2820
+ const now = this.now();
2821
+ const emit = this.hasKeyframe;
2822
+ const push = (json) => emit && this.opts.uploader.push(id, json);
2823
+ switch (phase) {
2824
+ case "down":
2825
+ this.touchDown = true;
2826
+ this.unchanged = 0;
2827
+ this.downX = x;
2828
+ this.downY = y;
2829
+ this.downAt = now;
2830
+ this.maxDistance = 0;
2831
+ this.lastMoveAt = now;
2832
+ push(RrwebEvents.interaction(now, RrwebEvents.TOUCH_START, x, y));
2833
+ return;
2834
+ case "move":
2835
+ if (!this.touchDown) return;
2836
+ this.maxDistance = Math.max(this.maxDistance, Math.hypot(x - this.downX, y - this.downY));
2837
+ if (now - this.lastMoveAt < 50) return;
2838
+ this.lastMoveAt = now;
2839
+ push(RrwebEvents.touchMove(now, x, y));
2840
+ return;
2841
+ default:
2842
+ if (!this.touchDown) return;
2843
+ this.touchDown = false;
2844
+ this.unchanged = 0;
2845
+ this.maxDistance = Math.max(this.maxDistance, Math.hypot(x - this.downX, y - this.downY));
2846
+ push(RrwebEvents.interaction(now, RrwebEvents.TOUCH_END, x, y));
2847
+ if (phase === "up" && this.maxDistance <= 10 && now - this.downAt < 1e3) push(RrwebEvents.interaction(now, RrwebEvents.CLICK, x, y));
2848
+ if (this.capturing) this.captureSoon = true;
2849
+ else this.schedule(TOUCH_SETTLE_MS, true);
2850
+ }
2851
+ }
2852
+ clearTimer() {
2853
+ if (this.timer) clearTimeout(this.timer);
2854
+ this.timer = null;
2855
+ }
2856
+ /** Schedules the next capture `delay` ms from now, or keeps an earlier one when `ifSooner`. */
2857
+ schedule(delay, ifSooner = false) {
2858
+ const due = this.now() + delay;
2859
+ if (ifSooner && this.timer && this.timerDue <= due) return;
2860
+ this.clearTimer();
2861
+ this.timerDue = due;
2862
+ const gen = this.generation;
2863
+ this.timer = setTimeout(() => {
2864
+ this.timer = null;
2865
+ if (gen === this.generation) void this.tick();
2866
+ }, delay);
2867
+ }
2868
+ async tick() {
2869
+ if (this.sid === null || !this.foreground || this.capturing) return;
2870
+ if (this.touchDown) {
2871
+ this.schedule(this.intervalMs);
2872
+ return;
2873
+ }
2874
+ this.capturing = true;
2875
+ this.captureSoon = false;
2876
+ const gen = this.generation;
2877
+ let frame = null;
2878
+ try {
2879
+ frame = await this.opts.capturer(await this.opts.captureOptions());
2880
+ } catch (err) {
2881
+ this.opts.onError?.(err);
2882
+ }
2883
+ if (gen !== this.generation) return;
2884
+ this.capturing = false;
2885
+ if (frame) this.onFrame(frame, this.now());
2886
+ if (this.sid !== null && this.foreground) this.schedule(this.captureSoon ? TOUCH_SETTLE_MS : this.currentIntervalMs);
2887
+ }
2888
+ onFrame(frame, at) {
2889
+ const id = this.sid;
2890
+ if (id === null || !(frame.width > 0) || !(frame.height > 0) || !frame.base64) return;
2891
+ const sizeChanged = frame.width !== this.viewportWidth || frame.height !== this.viewportHeight;
2892
+ const keyframe = this.needKeyframe || !this.hasKeyframe || sizeChanged;
2893
+ if (!keyframe && this.lastFrame === frame.base64) {
2894
+ this.unchanged++;
2895
+ return;
2896
+ }
2897
+ this.unchanged = 0;
2898
+ this.lastFrame = frame.base64;
2899
+ this.originX = frame.originX ?? 0;
2900
+ this.originY = frame.originY ?? 0;
2901
+ this.viewportWidth = frame.width;
2902
+ this.viewportHeight = frame.height;
2903
+ const url = RrwebEvents.dataUrl(frame.base64);
2904
+ const up = this.opts.uploader;
2905
+ if (!keyframe) return up.push(id, RrwebEvents.frame(at, url));
2906
+ this.needKeyframe = false;
2907
+ up.push(id, RrwebEvents.meta(at, this.opts.href(), frame.width, frame.height));
2908
+ up.push(id, RrwebEvents.fullSnapshot(at, url, frame.width, frame.height));
2909
+ if (!this.hasKeyframe) {
2910
+ this.hasKeyframe = true;
2911
+ for (const [ts, payload] of this.pendingCustom) up.push(id, RrwebEvents.custom(Math.max(ts, at), "guidely", payload));
2912
+ this.pendingCustom = [];
2913
+ }
2914
+ }
2915
+ };
2916
+
2450
2917
  // src/core/storage.ts
2451
2918
  var PersistentStore = class {
2452
2919
  constructor(backing, onError = () => {
@@ -2500,7 +2967,7 @@ var PersistentStore = class {
2500
2967
 
2501
2968
  // src/core/client.ts
2502
2969
  var SDK_NAME = "@guidelytech/react-native";
2503
- var SDK_VERSION = "0.11.0";
2970
+ var SDK_VERSION = "0.13.0";
2504
2971
  var DEFAULT_API_URL = "https://guidelytech.com";
2505
2972
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2506
2973
  var CACHED_START_MS = 1500;
@@ -2561,6 +3028,10 @@ var GuidelyClient = class {
2561
3028
  __publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
2562
3029
  __publicField(this, "backgroundedAt", null);
2563
3030
  __publicField(this, "messenger", null);
3031
+ __publicField(this, "replayUploader", null);
3032
+ __publicField(this, "recorder", null);
3033
+ /** Replay decision for the current session (sampled once; forced by start/stopSessionRecording). */
3034
+ __publicField(this, "replayDecision", null);
2564
3035
  __publicField(this, "ui", { steps: [], checklists: [], survey: null, resourceCenterOpen: false, theme: {}, messenger: false });
2565
3036
  __publicField(this, "uiListeners", /* @__PURE__ */ new Set());
2566
3037
  /** Feature flags for this environment: `guidely.flags.isEnabled("new-checkout")`. */
@@ -2666,7 +3137,7 @@ var GuidelyClient = class {
2666
3137
  async bootstrap() {
2667
3138
  const key = this.options.projectId;
2668
3139
  try {
2669
- await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key)]);
3140
+ await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key), LAUNCHER_PLACEMENT_KEY]);
2670
3141
  if (this.destroyed) return;
2671
3142
  this.anonymousId = this.store.get(KEYS.anon) || "";
2672
3143
  this.hadIdentity = !!this.anonymousId;
@@ -2712,6 +3183,7 @@ var GuidelyClient = class {
2712
3183
  if (this.destroyed) return;
2713
3184
  this.isReady = true;
2714
3185
  if (this.config) this.emitter.emit("ready", this.config);
3186
+ this.syncReplay();
2715
3187
  this.notify();
2716
3188
  this.evaluate("ready");
2717
3189
  } catch (err) {
@@ -2738,6 +3210,7 @@ var GuidelyClient = class {
2738
3210
  this.syncMessenger();
2739
3211
  if (this.hydrated) this.checkLaunch();
2740
3212
  this.emitter.emit("config", this.config);
3213
+ this.syncReplay();
2741
3214
  this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
2742
3215
  return true;
2743
3216
  }
@@ -2790,6 +3263,7 @@ var GuidelyClient = class {
2790
3263
  void this.outbox?.flush();
2791
3264
  void this.store.flush();
2792
3265
  void this.flags.flush();
3266
+ this.recorder?.pause();
2793
3267
  this.messenger?.setForeground(false);
2794
3268
  } else if (state === "active" && this.backgroundedAt !== null) {
2795
3269
  this.messenger?.setForeground(true);
@@ -2799,7 +3273,10 @@ var GuidelyClient = class {
2799
3273
  this.session = newSession();
2800
3274
  this.persistSession();
2801
3275
  this.firedDelays.clear();
3276
+ this.recorder?.stop(false);
2802
3277
  }
3278
+ this.recorder?.resume();
3279
+ this.syncReplay();
2803
3280
  this.cameToForeground();
2804
3281
  void this.flags.refresh();
2805
3282
  if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
@@ -2822,6 +3299,9 @@ var GuidelyClient = class {
2822
3299
  this.queued = [];
2823
3300
  this.messenger?.destroy();
2824
3301
  this.messenger = null;
3302
+ this.recorder?.stop(!this.analyticsEnabled());
3303
+ this.recorder = null;
3304
+ this.replayUploader?.destroy();
2825
3305
  this.visibilityWatchers.forEach((stop) => stop());
2826
3306
  this.visibilityWatchers.clear();
2827
3307
  this.timers.forEach((t) => clearTimeout(t));
@@ -2936,6 +3416,7 @@ var GuidelyClient = class {
2936
3416
  this.store.set(KEYS.anon, this.anonymousId);
2937
3417
  this.state = emptyState();
2938
3418
  this.persistState();
3419
+ this.recorder?.stop(false);
2939
3420
  this.session = newSession();
2940
3421
  this.persistSession();
2941
3422
  this.lastAudiences = /* @__PURE__ */ new Set();
@@ -2943,6 +3424,7 @@ var GuidelyClient = class {
2943
3424
  this.firstVisit = true;
2944
3425
  this.exposures.clear();
2945
3426
  this.syncFlagAttributes();
3427
+ this.syncReplay();
2946
3428
  this.emitter.emit("identify", { userId: null, anonymousId: this.anonymousId });
2947
3429
  this.notify();
2948
3430
  if (this.isReady) this.evaluate("identify");
@@ -2957,6 +3439,7 @@ var GuidelyClient = class {
2957
3439
  this.whenHydrated(() => {
2958
3440
  if (consent === "denied") this.queue?.clear();
2959
3441
  else if (consent === "granted") void this.queue?.flush();
3442
+ this.syncReplay();
2960
3443
  });
2961
3444
  }
2962
3445
  analyticsEnabled() {
@@ -2985,6 +3468,7 @@ var GuidelyClient = class {
2985
3468
  const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2986
3469
  if (locale) event.properties = { ...event.properties ?? {}, locale };
2987
3470
  this.queue.enqueue(event);
3471
+ this.recorder?.addGuidelyEvent(name, event.customName, event.guideId);
2988
3472
  this.emitter.emit("track", event);
2989
3473
  return event;
2990
3474
  }
@@ -3022,12 +3506,13 @@ var GuidelyClient = class {
3022
3506
  }
3023
3507
  this.screenName = name;
3024
3508
  this.targets.setScreen(name);
3509
+ if (changed) this.recorder?.onScreen();
3025
3510
  if (changed) this.syncFlagAttributes();
3026
3511
  const url = this.currentUrl();
3027
3512
  if (changed) {
3028
3513
  this.screenStartedAt = Date.now();
3029
3514
  this.screenLeft = this.backgroundedAt !== null;
3030
- if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, platform: this.bridge.device.platform } });
3515
+ if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, ...this.deviceProperties() } });
3031
3516
  }
3032
3517
  this.completeChecklistTasksBy("url_visited", url);
3033
3518
  this.active?.controller.resume();
@@ -3056,6 +3541,112 @@ var GuidelyClient = class {
3056
3541
  if (this.isReady) this.evaluate("click", { targetId });
3057
3542
  }
3058
3543
  // ---------------------------------------------------------------------------
3544
+ // Session replay
3545
+ // ---------------------------------------------------------------------------
3546
+ replaySettings() {
3547
+ const r = this.config?.settings?.replay;
3548
+ if (!r?.enabled || !this.options.sessionReplay?.enabled || !this.bridge.capturer) return null;
3549
+ return r;
3550
+ }
3551
+ /** Reconciles recording with config, consent, readiness, the session and its sample decision. Idempotent. */
3552
+ syncReplay() {
3553
+ if (this.destroyed || !this.hydrated) return;
3554
+ const r = this.isReady && this.analyticsEnabled() ? this.replaySettings() : null;
3555
+ const id = this.session.id;
3556
+ if (r && this.replayDecision?.sessionId !== id) {
3557
+ this.replayDecision = { sessionId: id, record: rolloutBucket(id, "replay") < (r.sampleRate ?? 100) };
3558
+ this.log(`replay ${this.replayDecision.record ? "sampled" : "not sampled"} for session ${id}`);
3559
+ }
3560
+ if (!r || !this.replayDecision?.record) {
3561
+ if (this.recorder?.isRecording) this.recorder.stop(!this.analyticsEnabled());
3562
+ return;
3563
+ }
3564
+ const rec = this.recorder ?? this.createRecorder();
3565
+ if (rec && rec.sessionId !== id) {
3566
+ this.log(`recording session ${id}`);
3567
+ rec.start(id);
3568
+ }
3569
+ }
3570
+ createRecorder() {
3571
+ const capturer = this.bridge.capturer;
3572
+ if (!capturer) return null;
3573
+ const uploader = this.replayUploader ?? (this.replayUploader = new ReplayUploader({
3574
+ url: () => this.api.url("/replay"),
3575
+ fetch: () => this.options.fetch ?? (typeof fetch === "function" ? fetch : null),
3576
+ health: this.api.health,
3577
+ meta: () => {
3578
+ const rec2 = this.recorder;
3579
+ return {
3580
+ anonymousId: this.anonymousId,
3581
+ userId: this.user?.id || null,
3582
+ ...this.collectUrls() && this.screenName ? { url: this.currentUrl() } : {},
3583
+ device: { type: this.bridge.device.deviceType, os: this.bridge.device.platform },
3584
+ ...rec2 && rec2.viewportWidth > 0 && rec2.viewportHeight > 0 ? { viewport: { width: rec2.viewportWidth, height: rec2.viewportHeight } } : {},
3585
+ sdk: { name: SDK_NAME, version: SDK_VERSION }
3586
+ };
3587
+ },
3588
+ onDisabled: (sid) => {
3589
+ if (sid !== this.session.id) return;
3590
+ this.log(`replay disabled by the server for session ${sid}`);
3591
+ this.replayDecision = { sessionId: sid, record: false };
3592
+ this.recorder?.stop(true);
3593
+ },
3594
+ log: (m) => this.log(m)
3595
+ }));
3596
+ const opts = this.options.sessionReplay ?? {};
3597
+ const rec = new SessionRecorder({
3598
+ capturer,
3599
+ uploader,
3600
+ intervalMs: opts.captureIntervalMs ?? 1e3,
3601
+ captureOptions: () => this.replayCaptureOptions(),
3602
+ href: () => this.currentUrl(),
3603
+ onError: (err) => this.reportError(err)
3604
+ });
3605
+ if (this.backgroundedAt !== null) rec.pause();
3606
+ this.recorder = rec;
3607
+ return rec;
3608
+ }
3609
+ async replayCaptureOptions() {
3610
+ const r = this.config?.settings?.replay;
3611
+ const opts = this.options.sessionReplay ?? {};
3612
+ const ids = new Set((r?.blockSelectors ?? []).map((sel) => guidelyIdFromSelector(sel)).filter((id) => !!id));
3613
+ const rects = await Promise.all(Array.from(ids, (id) => this.targets.measure(id)));
3614
+ return { maskAllText: !!opts.maskAllText || !!r?.maskAllText, maskAllImages: !!opts.maskAllImages, blockRects: rects.filter((rect) => !!rect) };
3615
+ }
3616
+ /**
3617
+ * Records this session even when it was not sampled (still needs `sessionReplay.enabled`, replay
3618
+ * enabled for the project, analytics consent and the native replay module). Sticks until the session ends.
3619
+ */
3620
+ startSessionRecording() {
3621
+ this.whenHydrated(() => {
3622
+ this.replayDecision = { sessionId: this.session.id, record: true };
3623
+ this.syncReplay();
3624
+ });
3625
+ }
3626
+ /** Stops recording for the rest of this session; what was recorded is uploaded. */
3627
+ stopSessionRecording() {
3628
+ this.whenHydrated(() => {
3629
+ this.replayDecision = { sessionId: this.session.id, record: false };
3630
+ this.recorder?.stop(false);
3631
+ });
3632
+ }
3633
+ /** True while this session is being recorded. */
3634
+ isRecording() {
3635
+ return !!this.recorder?.isRecording;
3636
+ }
3637
+ /** The current analytics session id (also the replay id). */
3638
+ getSessionId() {
3639
+ return this.session.id;
3640
+ }
3641
+ /** True when touches should be passed to {@link recordTouch}. */
3642
+ isRecordingTouches() {
3643
+ return !!this.recorder?.isRecording;
3644
+ }
3645
+ /** A primary-pointer touch on the app, in window points (the provider's touch observer calls this). */
3646
+ recordTouch(phase, x, y) {
3647
+ this.recorder?.touch(phase, x, y);
3648
+ }
3649
+ // ---------------------------------------------------------------------------
3059
3650
  // Product analytics (autocapture)
3060
3651
  // ---------------------------------------------------------------------------
3061
3652
  captureAllowed() {
@@ -3102,7 +3693,7 @@ var GuidelyClient = class {
3102
3693
  }
3103
3694
  if (this.destroyed) return;
3104
3695
  const platform = this.bridge.device.platform;
3105
- const maskAll = !!this.config?.settings?.replay?.maskAllText;
3696
+ const maskAll = !!this.config?.settings?.replay?.maskAllText || !!this.options.sessionReplay?.maskAllText;
3106
3697
  const eventUrl = this.collectUrls() && url ? url : void 0;
3107
3698
  if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
3108
3699
  const count = this.rage.add(tap.x, tap.y, t);
@@ -3192,7 +3783,7 @@ var GuidelyClient = class {
3192
3783
  if (this.screenName && this.screenLeft) {
3193
3784
  this.screenStartedAt = Date.now();
3194
3785
  this.screenLeft = false;
3195
- if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, platform: this.bridge.device.platform, resumed: true } });
3786
+ if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, ...this.deviceProperties(), resumed: true } });
3196
3787
  }
3197
3788
  } catch (err) {
3198
3789
  this.reportError(err);
@@ -3954,6 +4545,14 @@ var GuidelyClient = class {
3954
4545
  getMessenger() {
3955
4546
  return this.messenger;
3956
4547
  }
4548
+ /** Where the visitor dragged the messenger bubble on this device, or null to use the configured spot. */
4549
+ getMessengerLauncherPlacement() {
4550
+ return parsePlacement(this.store.get(LAUNCHER_PLACEMENT_KEY));
4551
+ }
4552
+ /** Remembers the bubble's spot on this device (in memory only when no `storage` was passed). */
4553
+ setMessengerLauncherPlacement(placement) {
4554
+ this.store.set(LAUNCHER_PLACEMENT_KEY, serializePlacement(placement));
4555
+ }
3957
4556
  /** Opens the Scout messenger, optionally on a tab. No-op when Scout is off for this platform. */
3958
4557
  openMessenger(tab) {
3959
4558
  this.whenHydrated(() => this.messenger?.open(tab));
@@ -3984,6 +4583,11 @@ var GuidelyClient = class {
3984
4583
  onFlagsChange(cb) {
3985
4584
  return this.flags.subscribe(cb);
3986
4585
  }
4586
+ /** Device facts on every screen view, read by the Pages devices / browsers breakdowns. */
4587
+ deviceProperties() {
4588
+ const d = this.bridge.device;
4589
+ return { platform: d.platform, deviceType: d.deviceType, os: d.platform };
4590
+ }
3987
4591
  /** Attributes flags are evaluated with: identity, traits, account, audiences, screen and device. */
3988
4592
  flagContext() {
3989
4593
  const d = this.bridge.device;
@@ -4812,14 +5416,89 @@ function ago(iso) {
4812
5416
  if (s < 86400) return `${Math.floor(s / 3600)}h`;
4813
5417
  return `${Math.floor(s / 86400)}d`;
4814
5418
  }
4815
- function MessengerLauncher({ store, palette: p, style }) {
5419
+ var LAUNCHER_SIZE = 54;
5420
+ function useReduceMotion() {
5421
+ const [on, setOn] = (0, import_react3.useState)(false);
5422
+ (0, import_react3.useEffect)(() => {
5423
+ let live = true;
5424
+ import_react_native5.AccessibilityInfo.isReduceMotionEnabled?.().then((v) => live && setOn(v)).catch(() => {
5425
+ });
5426
+ const sub = import_react_native5.AccessibilityInfo.addEventListener?.("reduceMotionChanged", setOn);
5427
+ return () => {
5428
+ live = false;
5429
+ sub?.remove();
5430
+ };
5431
+ }, []);
5432
+ return on;
5433
+ }
5434
+ function MessengerLauncher({ store, client, palette: p, style, frame, defaultSide }) {
4816
5435
  const snap = useMessenger(store);
5436
+ const reduceMotion = useReduceMotion();
5437
+ const [placement, setPlacement] = (0, import_react3.useState)(() => client.getMessengerLauncherPlacement());
5438
+ const pan = (0, import_react3.useRef)(new import_react_native5.Animated.ValueXY()).current;
5439
+ const offset = (0, import_react3.useRef)({ x: 0, y: 0 });
5440
+ const dragging = (0, import_react3.useRef)(false);
5441
+ const live = (0, import_react3.useRef)({ frame, defaultSide, reduceMotion });
5442
+ live.current = { frame, defaultSide, reduceMotion };
5443
+ const { width, height, size, defaultBottom } = frame;
5444
+ const { top, bottom, left, right } = frame.insets;
5445
+ (0, import_react3.useLayoutEffect)(() => {
5446
+ if (dragging.current) return;
5447
+ const f = live.current.frame;
5448
+ const anchor = placementPoint(f, { side: defaultSide, fraction: 0 });
5449
+ const at = placement ? placementPoint(f, placement) : anchor;
5450
+ offset.current = { x: at.x - anchor.x, y: at.y - anchor.y };
5451
+ pan.setValue(offset.current);
5452
+ }, [pan, placement, defaultSide, width, height, size, defaultBottom, top, bottom, left, right]);
5453
+ const responder = (0, import_react3.useMemo)(() => {
5454
+ const moved = (g) => Math.hypot(g.dx, g.dy) > LAUNCHER_DRAG_THRESHOLD;
5455
+ const anchor = () => placementPoint(live.current.frame, { side: live.current.defaultSide, fraction: 0 });
5456
+ let start = { x: 0, y: 0 };
5457
+ const follow = (g) => {
5458
+ const a = anchor();
5459
+ const at = clampToSafeArea(live.current.frame, { x: a.x + start.x + g.dx, y: a.y + start.y + g.dy });
5460
+ return { x: at.x - a.x, y: at.y - a.y };
5461
+ };
5462
+ const drop = (g) => {
5463
+ const a = anchor();
5464
+ const d = follow(g);
5465
+ const next = snapPlacement(live.current.frame, { x: a.x + d.x, y: a.y + d.y });
5466
+ const at = placementPoint(live.current.frame, next);
5467
+ const to = { x: at.x - a.x, y: at.y - a.y };
5468
+ offset.current = to;
5469
+ client.setMessengerLauncherPlacement(next);
5470
+ const done = ({ finished }) => {
5471
+ if (finished) dragging.current = false;
5472
+ setPlacement(next);
5473
+ };
5474
+ if (live.current.reduceMotion) {
5475
+ pan.setValue(to);
5476
+ done({ finished: true });
5477
+ } else import_react_native5.Animated.timing(pan, { toValue: to, duration: 300, easing: import_react_native5.Easing.out(import_react_native5.Easing.cubic), useNativeDriver: true }).start(done);
5478
+ };
5479
+ return import_react_native5.PanResponder.create({
5480
+ // Taps stay with the Pressable; only a real drag is taken (which cancels its press).
5481
+ onMoveShouldSetPanResponderCapture: (_, g) => moved(g),
5482
+ onMoveShouldSetPanResponder: (_, g) => moved(g),
5483
+ onPanResponderGrant: () => {
5484
+ dragging.current = true;
5485
+ start = offset.current;
5486
+ pan.stopAnimation((v) => {
5487
+ if (Number.isFinite(v?.x) && Number.isFinite(v?.y)) start = v;
5488
+ });
5489
+ },
5490
+ onPanResponderMove: (_, g) => pan.setValue(follow(g)),
5491
+ onPanResponderRelease: (_, g) => drop(g),
5492
+ onPanResponderTerminate: (_, g) => drop(g),
5493
+ onPanResponderTerminationRequest: () => false
5494
+ });
5495
+ }, [client, pan]);
4817
5496
  if (snap.open) return null;
4818
5497
  const label = snap.unread ? `Open ${snap.config.name}, ${snap.unread} unread` : `Open ${snap.config.name}`;
4819
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: () => store.open(), style: ({ pressed }) => [styles4.launcher, style, { backgroundColor: p.primary, opacity: pressed ? 0.85 : 1 }], children: [
5498
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Animated.View, { ...responder.panHandlers, style: [styles4.launcherWrap, style, { transform: pan.getTranslateTransform() }], children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: () => !dragging.current && store.open(), style: ({ pressed }) => [styles4.launcher, { backgroundColor: p.primary, opacity: pressed ? 0.85 : 1 }], children: [
4820
5499
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.launcherIcon, { color: p.onPrimary }], children: "\u{1F4AC}" }),
4821
5500
  snap.unread ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: styles4.badge, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: styles4.badgeText, children: snap.unread }) }) : null
4822
- ] });
5501
+ ] }) });
4823
5502
  }
4824
5503
  function MessengerSheet({ store, client, palette: p, topInset, bottomInset }) {
4825
5504
  const snap = useMessenger(store);
@@ -5064,7 +5743,8 @@ function Article({ article, palette: p, client }) {
5064
5743
  }
5065
5744
  var styles4 = import_react_native5.StyleSheet.create({
5066
5745
  flex: { flex: 1 },
5067
- launcher: { position: "absolute", width: 54, height: 54, borderRadius: 27, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.22, shadowRadius: 12, shadowOffset: { width: 0, height: 4 } },
5746
+ launcherWrap: { position: "absolute", width: LAUNCHER_SIZE, height: LAUNCHER_SIZE, elevation: 8 },
5747
+ launcher: { width: LAUNCHER_SIZE, height: LAUNCHER_SIZE, borderRadius: 27, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.22, shadowRadius: 12, shadowOffset: { width: 0, height: 4 } },
5068
5748
  launcherIcon: { fontSize: 24 },
5069
5749
  badge: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center", paddingHorizontal: 5, borderWidth: 2, borderColor: "#ffffff" },
5070
5750
  badgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" },
@@ -5297,20 +5977,20 @@ function positionCard(target, size, viewport, insets, placement, offset) {
5297
5977
  const order = rawSide === "auto" ? ["bottom", "top"] : [rawSide, opposite[rawSide], "bottom", "top"];
5298
5978
  const space = { top: target.y - insets.top, bottom: viewport.height - insets.bottom - (target.y + target.height) };
5299
5979
  const side = order.find(fits) ?? (space.bottom >= space.top ? "bottom" : "top");
5300
- const clamp2 = (v, min, max) => Math.max(min, Math.min(max, v));
5980
+ const clamp3 = (v, min, max) => Math.max(min, Math.min(max, v));
5301
5981
  if (side === "top" || side === "bottom") {
5302
5982
  const cx = target.x + target.width / 2;
5303
5983
  let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size.width : cx - size.width / 2;
5304
- x2 = clamp2(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
5984
+ x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
5305
5985
  let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size.height;
5306
- y2 = clamp2(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5307
- return { x: x2, y: y2, side, arrow: clamp2(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
5986
+ y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5987
+ return { x: x2, y: y2, side, arrow: clamp3(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
5308
5988
  }
5309
5989
  const cy = target.y + target.height / 2;
5310
5990
  let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size.height : cy - size.height / 2;
5311
- y = clamp2(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5991
+ y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5312
5992
  const x = side === "right" ? target.x + target.width + offset : target.x - offset - size.width;
5313
- return { x, y, side, arrow: clamp2(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
5993
+ return { x, y, side, arrow: clamp3(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
5314
5994
  }
5315
5995
 
5316
5996
  // src/ui/StepCard.tsx
@@ -5926,6 +6606,7 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5926
6606
  const unread = showRcLauncher ? client.getUnreadAnnouncements() : 0;
5927
6607
  const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
5928
6608
  const checklistOnSameSide = ui.checklists.some((c) => c.guide.config.checklist?.position === "bottom-left" === messengerLeft) && client.options.showChecklistLauncher !== false;
6609
+ const messengerBottom = insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0);
5929
6610
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native10.View, { ref, collapsable: false, pointerEvents: "box-none", style: import_react_native10.StyleSheet.absoluteFill, onLayout: measureOrigin, children: [
5930
6611
  ui.checklists.map((c) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
5931
6612
  showRcLauncher ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
@@ -5941,7 +6622,17 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5941
6622
  ]
5942
6623
  }
5943
6624
  ) : null,
5944
- messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerLauncher, { store: messenger, palette: basePalette, style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0) }] }) : null,
6625
+ messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
6626
+ MessengerLauncher,
6627
+ {
6628
+ store: messenger,
6629
+ client,
6630
+ palette: basePalette,
6631
+ style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: messengerBottom }],
6632
+ frame: { width, height, insets, size: LAUNCHER_SIZE, defaultBottom: messengerBottom },
6633
+ defaultSide: messengerLeft ? "left" : "right"
6634
+ }
6635
+ ) : null,
5945
6636
  ui.survey ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Survey, { view: ui.survey, client, palette: palette(client.themeFor(ui.survey.guide), scheme), geometry }) : null,
5946
6637
  ui.steps.map((s) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StepLayer, { view: s, client, palette: palette(client.themeFor(s.guide), scheme, s.guide.config.appearance), geometry }, s.key)),
5947
6638
  messenger ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, topInset: insets.top, bottomInset: insets.bottom })
@@ -5961,8 +6652,9 @@ function rcLauncherLabel(client) {
5961
6652
 
5962
6653
  // src/ui/Autocapture.tsx
5963
6654
  var import_jsx_runtime11 = require("react/jsx-runtime");
6655
+ var GUIDELY_MASK_NATIVE_ID = "guidely-mask";
5964
6656
  function GuidelyMask({ children, ...rest }) {
5965
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.View, { ...rest, children });
6657
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.View, { collapsable: false, ...rest, nativeID: GUIDELY_MASK_NATIVE_ID, children });
5966
6658
  }
5967
6659
  var RESOLVE = {
5968
6660
  isIgnored: (type) => type === GuidelyOverlay,
@@ -5973,6 +6665,18 @@ function useTapObserver(client) {
5973
6665
  clientRef.current = client;
5974
6666
  return (0, import_react8.useMemo)(() => {
5975
6667
  const tracker = new TapTracker();
6668
+ let replayPointer = null;
6669
+ const replay = (phase, e) => {
6670
+ const c = clientRef.current;
6671
+ const n = e.nativeEvent;
6672
+ if (!c?.isRecordingTouches()) return void (replayPointer = null);
6673
+ if (phase === "down") {
6674
+ if (replayPointer !== null) return;
6675
+ replayPointer = n.identifier;
6676
+ } else if (n.identifier !== replayPointer) return;
6677
+ if (phase === "up" || phase === "cancel") replayPointer = null;
6678
+ c.recordTouch(phase, n.pageX, n.pageY);
6679
+ };
5976
6680
  const guard = (fn) => (e) => {
5977
6681
  try {
5978
6682
  fn(e);
@@ -5982,12 +6686,17 @@ function useTapObserver(client) {
5982
6686
  };
5983
6687
  return {
5984
6688
  onTouchStartCapture: guard((e) => {
6689
+ replay("down", e);
5985
6690
  const n = e.nativeEvent;
5986
6691
  if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
5987
6692
  tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
5988
6693
  }),
5989
- onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
6694
+ onTouchMoveCapture: guard((e) => {
6695
+ replay("move", e);
6696
+ tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier);
6697
+ }),
5990
6698
  onTouchEndCapture: guard((e) => {
6699
+ replay("up", e);
5991
6700
  const n = e.nativeEvent;
5992
6701
  const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
5993
6702
  const c = clientRef.current;
@@ -5995,7 +6704,10 @@ function useTapObserver(client) {
5995
6704
  const r = resolveTapTarget(e, RESOLVE);
5996
6705
  if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
5997
6706
  }),
5998
- onTouchCancelCapture: guard(() => tracker.cancel())
6707
+ onTouchCancelCapture: guard((e) => {
6708
+ replay("cancel", e);
6709
+ tracker.cancel();
6710
+ })
5999
6711
  };
6000
6712
  }, []);
6001
6713
  }
@@ -6089,9 +6801,15 @@ function defaultBridge(appVersion) {
6089
6801
  },
6090
6802
  openExternal: (url) => void import_react_native13.Linking.openURL(url).catch(() => {
6091
6803
  }),
6092
- windowSize: () => import_react_native13.Dimensions.get("window")
6804
+ windowSize: () => import_react_native13.Dimensions.get("window"),
6805
+ capturer: nativeCapturer()
6093
6806
  };
6094
6807
  }
6808
+ function nativeCapturer() {
6809
+ const mod = import_react_native13.NativeModules.GuidelyReplay;
6810
+ if (!mod || typeof mod.capture !== "function") return void 0;
6811
+ return (options) => mod.capture(options);
6812
+ }
6095
6813
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
6096
6814
  const [client, setClient] = (0, import_react10.useState)(null);
6097
6815
  const optionsRef = (0, import_react10.useRef)(options);
@@ -6161,7 +6879,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6161
6879
  const flags = (0, import_react10.useMemo)(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
6162
6880
  const tapObserver = useTapObserver(client);
6163
6881
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(FlagsContext.Provider, { value: flags, children: [
6164
- options.autocapture === false ? children : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native13.View, { style: styles10.root, ...tapObserver, children }),
6882
+ options.autocapture === false && !options.sessionReplay?.enabled ? children : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native13.View, { style: styles10.root, ...tapObserver, children }),
6165
6883
  client ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyOverlay, { client, insets, launcherOffset }) : null
6166
6884
  ] }) });
6167
6885
  }
@@ -6181,6 +6899,9 @@ var NOOP_RESULTS = {
6181
6899
  getMessenger: null,
6182
6900
  isGuideCompleted: false,
6183
6901
  isClientReady: false,
6902
+ isRecording: false,
6903
+ isRecordingTouches: false,
6904
+ getSessionId: "",
6184
6905
  getScreen: "",
6185
6906
  on: () => {
6186
6907
  },
@@ -6315,8 +7036,11 @@ function useActiveGuide() {
6315
7036
  InMemoryStickyBucketStore,
6316
7037
  Messenger,
6317
7038
  RageClickDetector,
7039
+ ReplayUploader,
7040
+ RrwebEvents,
6318
7041
  SDK_NAME,
6319
7042
  SDK_VERSION,
7043
+ SessionRecorder,
6320
7044
  TapTracker,
6321
7045
  TargetRegistry,
6322
7046
  createFlagsClient,