@mrrocket/sdk 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +35 -1
  3. package/dist/api-client.d.ts +213 -0
  4. package/dist/api-client.js +67 -0
  5. package/dist/api-client.js.map +1 -0
  6. package/dist/api.d.ts +2 -122
  7. package/dist/api.js +2 -61
  8. package/dist/api.js.map +1 -1
  9. package/dist/browser/mrrocket-messenger.js +313 -0
  10. package/dist/browser/mrrocket-replay.js +3 -3
  11. package/dist/browser/mrrocket.js +1 -1
  12. package/dist/browser.d.ts +2 -1
  13. package/dist/browser.js +3 -0
  14. package/dist/browser.js.map +1 -1
  15. package/dist/capture.d.ts +1 -13
  16. package/dist/capture.js +1 -22
  17. package/dist/capture.js.map +1 -1
  18. package/dist/core.d.ts +1 -1
  19. package/dist/core.js +1 -1
  20. package/dist/core.js.map +1 -1
  21. package/dist/feedback.js +5 -2
  22. package/dist/feedback.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/messenger-ui.d.ts +47 -0
  27. package/dist/messenger-ui.js +583 -0
  28. package/dist/messenger-ui.js.map +1 -0
  29. package/dist/messenger.d.ts +37 -0
  30. package/dist/messenger.js +69 -0
  31. package/dist/messenger.js.map +1 -0
  32. package/dist/rage.d.ts +14 -0
  33. package/dist/rage.js +27 -0
  34. package/dist/rage.js.map +1 -0
  35. package/dist/react-native/index.d.cts +339 -4
  36. package/dist/react-native/index.d.ts +339 -4
  37. package/dist/react-native/index.js +1191 -31
  38. package/dist/react-native/index.js.map +1 -1
  39. package/dist/styles-css.d.ts +6 -0
  40. package/dist/styles-css.js +327 -0
  41. package/dist/styles-css.js.map +1 -0
  42. package/dist/styles.d.ts +1 -2
  43. package/dist/styles.js +3 -237
  44. package/dist/styles.js.map +1 -1
  45. package/package.json +13 -5
@@ -3,6 +3,7 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
7
  var __export = (target, all) => {
7
8
  for (var name in all)
8
9
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -16,21 +17,28 @@ var __copyProps = (to, from, except, desc) => {
16
17
  return to;
17
18
  };
18
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
19
21
 
20
22
  // src/native/index.tsx
21
23
  var native_exports = {};
22
24
  __export(native_exports, {
25
+ MRRocketFeedback: () => MRRocketFeedback,
23
26
  MRRocketFlows: () => MRRocketFlows,
27
+ MRRocketMask: () => MRRocketMask,
28
+ MRRocketMessenger: () => MRRocketMessenger,
24
29
  MRRocketProvider: () => MRRocketProvider,
30
+ MRRocketSurveys: () => MRRocketSurveys,
25
31
  SDK_VERSION: () => SDK_VERSION,
26
32
  activeRouteName: () => activeRouteName,
27
33
  createMRRocket: () => createMRRocket,
28
34
  getMRRocket: () => getMRRocket,
29
35
  useCancelFlow: () => useCancelFlow,
30
36
  useExpoRouterTracking: () => useExpoRouterTracking,
37
+ useFeedback: () => useFeedback,
31
38
  useFlag: () => useFlag,
32
39
  useFlagVariant: () => useFlagVariant,
33
40
  useMRRocket: () => useMRRocket,
41
+ useMessenger: () => useMessenger,
34
42
  useNavigationTracking: () => useNavigationTracking,
35
43
  useOnboarding: () => useOnboarding,
36
44
  usePush: () => usePush,
@@ -38,10 +46,10 @@ __export(native_exports, {
38
46
  useVariant: () => useVariant
39
47
  });
40
48
  module.exports = __toCommonJS(native_exports);
41
- var import_react2 = require("react");
49
+ var import_react3 = require("react");
42
50
 
43
51
  // src/core.ts
44
- var SDK_VERSION = "0.8.0";
52
+ var SDK_VERSION = "0.9.0";
45
53
  var propOf = (person, name) => {
46
54
  const want = name.trim().toLowerCase();
47
55
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -114,25 +122,25 @@ var parseQuery = (url) => {
114
122
  return out;
115
123
  };
116
124
  var hashUnit = (input) => {
117
- let h2 = 2166136261;
125
+ let h3 = 2166136261;
118
126
  for (let i = 0; i < input.length; i++) {
119
- h2 ^= input.charCodeAt(i);
120
- h2 = Math.imul(h2, 16777619);
127
+ h3 ^= input.charCodeAt(i);
128
+ h3 = Math.imul(h3, 16777619);
121
129
  }
122
- return (h2 >>> 0) / 4294967296;
130
+ return (h3 >>> 0) / 4294967296;
123
131
  };
124
132
  var flagUnit = (input) => {
125
- let h2 = 2166136261;
133
+ let h3 = 2166136261;
126
134
  for (let i = 0; i < input.length; i++) {
127
- h2 ^= input.charCodeAt(i);
128
- h2 = Math.imul(h2, 16777619);
135
+ h3 ^= input.charCodeAt(i);
136
+ h3 = Math.imul(h3, 16777619);
129
137
  }
130
- h2 ^= h2 >>> 16;
131
- h2 = Math.imul(h2, 2246822507);
132
- h2 ^= h2 >>> 13;
133
- h2 = Math.imul(h2, 3266489909);
134
- h2 ^= h2 >>> 16;
135
- return (h2 >>> 0) / 4294967296;
138
+ h3 ^= h3 >>> 16;
139
+ h3 = Math.imul(h3, 2246822507);
140
+ h3 ^= h3 >>> 13;
141
+ h3 = Math.imul(h3, 3266489909);
142
+ h3 ^= h3 >>> 16;
143
+ return (h3 >>> 0) / 4294967296;
136
144
  };
137
145
  var assign = (experiment, distinctId) => {
138
146
  const control = experiment.variants[0]?.id ?? "control";
@@ -163,6 +171,62 @@ var textChanges = (config, distinctId, preview) => {
163
171
  };
164
172
  var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
165
173
 
174
+ // src/api-client.ts
175
+ var ApiError = class extends Error {
176
+ constructor(message, status) {
177
+ super(message);
178
+ __publicField(this, "status");
179
+ this.status = status;
180
+ }
181
+ };
182
+ var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
183
+ var call = async (url, init, signal) => {
184
+ let res;
185
+ try {
186
+ res = await fetch(url, {
187
+ ...init,
188
+ signal,
189
+ headers: init?.body ? { "content-type": "application/json" } : void 0
190
+ });
191
+ } catch (e) {
192
+ if (e.name === "AbortError") throw e;
193
+ throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
194
+ }
195
+ if (!res.ok) {
196
+ let detail = "";
197
+ try {
198
+ detail = (await res.json()).error ?? "";
199
+ } catch {
200
+ }
201
+ throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
202
+ }
203
+ return await res.json();
204
+ };
205
+ var createApi = (c) => {
206
+ const base = `${c.host}/api/sdk`;
207
+ const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
208
+ const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
209
+ return {
210
+ list: (o, signal) => call(`${base}/feedback?${qs({ key: c.apiKey, distinctId: c.distinctId, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
211
+ post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
212
+ roadmap: (signal) => call(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
213
+ changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
214
+ submit: (p) => post("/feedback", p),
215
+ vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
216
+ comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
217
+ comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
218
+ survey: (url, signal) => call(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, void 0, signal),
219
+ seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
220
+ respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
221
+ chat: (signal) => call(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
222
+ sendChat: (m) => post("/messages", m),
223
+ articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
224
+ article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
225
+ messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
226
+ rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
227
+ };
228
+ };
229
+
166
230
  // src/batch.ts
167
231
  var MAX_BATCH = 100;
168
232
  var MAX_BYTES = 6e4;
@@ -612,6 +676,174 @@ var createFlows = (env) => {
612
676
  return controller;
613
677
  };
614
678
 
679
+ // src/rage.ts
680
+ var RageDetector = class {
681
+ constructor(windowMs = 1e3, radius = 30, count = 3) {
682
+ __publicField(this, "windowMs", windowMs);
683
+ __publicField(this, "radius", radius);
684
+ __publicField(this, "count", count);
685
+ __publicField(this, "clicks", []);
686
+ __publicField(this, "fired", false);
687
+ }
688
+ push(x, y, t) {
689
+ const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
690
+ if (near.length !== this.clicks.length) this.fired = false;
691
+ this.clicks = [...near, { x, y, t }];
692
+ if (this.clicks.length >= this.count && !this.fired) {
693
+ this.fired = true;
694
+ return true;
695
+ }
696
+ return false;
697
+ }
698
+ };
699
+
700
+ // src/native/recorder.ts
701
+ var FRAME_MS = 1e3;
702
+ var WIDTH = 360;
703
+ var BUFFER_MS = 6e4;
704
+ var CHUNK_FRAMES = 12;
705
+ var CHUNK_CHARS = 7e5;
706
+ var MAX_MS = 30 * 6e4;
707
+ var shareOf = (s) => {
708
+ let h3 = 2166136261;
709
+ for (let i = 0; i < s.length; i++) h3 = Math.imul(h3 ^ s.charCodeAt(i), 16777619);
710
+ return (h3 >>> 0) / 4294967296;
711
+ };
712
+ var createRecorder = (deps, st) => {
713
+ let timer = null;
714
+ let busy = false;
715
+ let last = "";
716
+ let rec = null;
717
+ let frames = [];
718
+ let marks = [];
719
+ const ensure = () => {
720
+ const session = st.session();
721
+ const cfg = st.config();
722
+ if (!rec || rec.session !== session) {
723
+ frames = [];
724
+ marks = [];
725
+ last = "";
726
+ const sampled = !!cfg && shareOf(session) < (cfg.sampleRate ?? 0.1);
727
+ rec = { session, start: deps.now(), seq: 0, uploading: sampled, trigger: "sample", stopped: false };
728
+ }
729
+ return rec;
730
+ };
731
+ const send = async (r) => {
732
+ if (!frames.length || r.stopped) return;
733
+ const batch = frames.splice(0, frames.length);
734
+ const ms = marks.splice(0, marks.length);
735
+ const size = deps.screen();
736
+ const { distinctId, identified } = st.who();
737
+ const body = JSON.stringify({
738
+ key: st.apiKey,
739
+ consent: { replay: true },
740
+ sessionId: r.session,
741
+ start: r.start,
742
+ seq: r.seq++,
743
+ enc: "frames",
744
+ data: JSON.stringify(batch),
745
+ meta: { first: batch[0].t, last: batch[batch.length - 1].t, vw: size ? Math.round(size.w) : null, vh: size ? Math.round(size.h) : null, marks: ms },
746
+ distinctId,
747
+ identified,
748
+ trigger: r.trigger,
749
+ app: st.app
750
+ });
751
+ const status = await deps.send(body);
752
+ if (status === 403 || status === 409 || status === 413) r.stopped = true;
753
+ };
754
+ const tick = async () => {
755
+ if (busy || !st.allowed() || !st.config()) return;
756
+ const r = ensure();
757
+ if (r.stopped || deps.now() - r.start > MAX_MS) return;
758
+ if (deps.keyboardOpen()) return;
759
+ busy = true;
760
+ try {
761
+ const shot = await deps.capture(WIDTH);
762
+ if (!shot || shot.img === last) return;
763
+ last = shot.img;
764
+ const size = deps.screen();
765
+ const scale = size && size.w ? shot.w / size.w : 1;
766
+ const measured = await deps.masks();
767
+ if (measured === null) {
768
+ last = "";
769
+ return;
770
+ }
771
+ const masks = measured.map(([x, y, w, h3]) => [x * scale, y * scale, w * scale, h3 * scale].map(Math.round));
772
+ frames.push({ t: deps.now(), w: shot.w, h: shot.h, img: shot.img, masks });
773
+ if (!r.uploading) {
774
+ const since = deps.now() - BUFFER_MS;
775
+ frames = frames.filter((f) => f.t >= since);
776
+ marks = marks.filter((m) => m.t >= since);
777
+ return;
778
+ }
779
+ const chars = frames.reduce((a, f) => a + f.img.length, 0);
780
+ if (frames.length >= CHUNK_FRAMES || chars >= CHUNK_CHARS) await send(r);
781
+ } catch {
782
+ } finally {
783
+ busy = false;
784
+ }
785
+ };
786
+ return {
787
+ /** Starts or stops the frame loop to match the app state (active) and whether recording is allowed. */
788
+ sync(active) {
789
+ const on = active && st.allowed() && !!st.config();
790
+ if (on && timer === null) timer = deps.setInterval(() => void tick(), FRAME_MS);
791
+ if (!on && timer !== null) {
792
+ deps.clearInterval(timer);
793
+ timer = null;
794
+ if (rec?.uploading) void send(rec);
795
+ }
796
+ },
797
+ /** A screen, tap, rage tap or error, for the timeline. */
798
+ mark(m) {
799
+ if (!st.allowed() || !st.config()) return;
800
+ ensure();
801
+ marks.push({ t: deps.now(), ...m });
802
+ if (marks.length > 300) marks.splice(0, marks.length - 300);
803
+ },
804
+ /** A rage tap or an error: a buffering recording uploads its last minute and keeps recording. */
805
+ trigger(kind) {
806
+ if (!rec || rec.uploading || rec.stopped || !st.allowed()) return;
807
+ rec.uploading = true;
808
+ rec.trigger = kind;
809
+ void send(rec);
810
+ },
811
+ /** Sends what waits (going to the background). */
812
+ flush() {
813
+ if (rec?.uploading) return send(rec);
814
+ return Promise.resolve();
815
+ },
816
+ /** Consent withdrawn: forget everything not sent. */
817
+ drop() {
818
+ frames = [];
819
+ marks = [];
820
+ rec = null;
821
+ last = "";
822
+ },
823
+ /** For tests: one frame now. */
824
+ tick
825
+ };
826
+ };
827
+ var maskedViews = /* @__PURE__ */ new Set();
828
+ var measureMasks = () => Promise.all(
829
+ [...maskedViews].map(
830
+ (ref) => new Promise((resolve) => {
831
+ const v = ref.current;
832
+ if (!v) return resolve("none");
833
+ const t = setTimeout(() => resolve("failed"), 100);
834
+ try {
835
+ v.measureInWindow((x, y, w, h3) => {
836
+ clearTimeout(t);
837
+ resolve(w > 0 && h3 > 0 ? [x, y, w, h3] : "none");
838
+ });
839
+ } catch {
840
+ clearTimeout(t);
841
+ resolve("failed");
842
+ }
843
+ })
844
+ )
845
+ ).then((rs) => rs.includes("failed") ? null : rs.filter((r) => Array.isArray(r)));
846
+
615
847
  // src/referral.ts
616
848
  var REF_PARAM = "mrr_ref";
617
849
  var CLICK_PARAM = "mrr_click";
@@ -634,7 +866,8 @@ var KEYS = {
634
866
  queue: "mrrocket:queue",
635
867
  config: "mrrocket:config",
636
868
  installed: "mrrocket:installed",
637
- referral: "mrrocket:referral"
869
+ referral: "mrrocket:referral",
870
+ surveys: "mrrocket:surveys"
638
871
  };
639
872
  var MAX_ERRORS = 20;
640
873
  var MAX_CRUMBS = 20;
@@ -697,6 +930,8 @@ var createClient = (options, deps) => {
697
930
  let backgroundSince = null;
698
931
  const early = [];
699
932
  const exposed = /* @__PURE__ */ new Set();
933
+ const surveysDone = /* @__PURE__ */ new Set();
934
+ const rage = new RageDetector();
700
935
  let textsFor = null;
701
936
  const texts = () => {
702
937
  const id = distinctId();
@@ -728,8 +963,10 @@ var createClient = (options, deps) => {
728
963
  flows.setConfig(config);
729
964
  void flows.refresh();
730
965
  };
966
+ let recorder = null;
731
967
  const changed = () => {
732
968
  version++;
969
+ recorder?.sync(appState === "active");
733
970
  for (const l of [...listeners]) {
734
971
  try {
735
972
  l();
@@ -904,6 +1141,8 @@ var createClient = (options, deps) => {
904
1141
  ...who ? { distinctId: who } : {},
905
1142
  ...session ? { sessionId: session.id } : {}
906
1143
  });
1144
+ recorder?.mark({ kind: "error", path: currentScreen ?? "/", label: clip2(d.message, 120) });
1145
+ recorder?.trigger("error");
907
1146
  if (fatal) void sendErrors();
908
1147
  else if (errorTimer === null) errorTimer = setTimeoutFn(() => void sendErrors(), 2e3);
909
1148
  };
@@ -921,11 +1160,34 @@ var createClient = (options, deps) => {
921
1160
  previousHandler?.(error, isFatal);
922
1161
  });
923
1162
  };
1163
+ let replayOn = options.replay === true;
1164
+ recorder = deps.recorder ? createRecorder(
1165
+ {
1166
+ now,
1167
+ setInterval: setIntervalFn,
1168
+ clearInterval: clearIntervalFn,
1169
+ capture: deps.recorder.capture,
1170
+ keyboardOpen: deps.recorder.keyboardOpen,
1171
+ masks: deps.recorder.masks,
1172
+ screen: () => deps.screen?.(),
1173
+ send: (body) => post("/api/sdk/replay", body)
1174
+ },
1175
+ {
1176
+ allowed: () => replayOn && loaded && !closed && consent !== "off" && !!apiKey,
1177
+ config: () => config?.replay,
1178
+ session: () => session?.id ?? touchSession(),
1179
+ who: () => ({ distinctId: sendId(), identified: !!identified }),
1180
+ apiKey,
1181
+ app: context
1182
+ }
1183
+ ) : null;
1184
+ if (options.replay && !deps.recorder) warn("Session recordings need react-native-view-shot: npx expo install react-native-view-shot");
924
1185
  let appStateSub;
925
1186
  const onAppState = (next) => {
926
1187
  if (closed) return;
927
1188
  const prev = appState;
928
1189
  appState = next;
1190
+ recorder?.sync(next === "active");
929
1191
  if (next === "active" && prev !== "active") {
930
1192
  startTimer();
931
1193
  if (backgroundSince !== null) {
@@ -938,6 +1200,7 @@ var createClient = (options, deps) => {
938
1200
  crumb("lifecycle", "app_background");
939
1201
  if (options.lifecycle !== false) record("$app_background");
940
1202
  stopTimer();
1203
+ void recorder?.flush();
941
1204
  void persistQueue();
942
1205
  void flush();
943
1206
  if (errorQueue.length) void sendErrors();
@@ -1030,12 +1293,13 @@ var createClient = (options, deps) => {
1030
1293
  const start = async () => {
1031
1294
  let firstOpen = false;
1032
1295
  if (consent !== "off") {
1033
- const [id, stored, cached, installed, ref] = await Promise.all([
1296
+ const [id, stored, cached, installed, ref, surveys] = await Promise.all([
1034
1297
  get(KEYS.id),
1035
1298
  get(KEYS.queue),
1036
1299
  get(KEYS.config),
1037
1300
  get(KEYS.installed),
1038
- get(KEYS.referral)
1301
+ get(KEYS.referral),
1302
+ get(KEYS.surveys)
1039
1303
  ]);
1040
1304
  if (consent === "persistent") {
1041
1305
  installId = id || newDistinctId();
@@ -1058,6 +1322,14 @@ var createClient = (options, deps) => {
1058
1322
  if (r && typeof r.code === "string") referral = r;
1059
1323
  } catch {
1060
1324
  }
1325
+ try {
1326
+ const done = surveys && consent === "persistent" ? JSON.parse(surveys) : null;
1327
+ if (Array.isArray(done)) {
1328
+ for (const x of done) if (typeof x === "string") surveysDone.add(x);
1329
+ }
1330
+ } catch {
1331
+ await remove(KEYS.surveys);
1332
+ }
1061
1333
  firstOpen = !!storage && !installed;
1062
1334
  if (firstOpen) await set(KEYS.installed, String(now()));
1063
1335
  } else {
@@ -1108,6 +1380,24 @@ var createClient = (options, deps) => {
1108
1380
  record(event, props);
1109
1381
  flows.onEvent(event);
1110
1382
  },
1383
+ tap: (x, y) => {
1384
+ if (options.heatmaps === false || consent === "off") return;
1385
+ const size = deps.screen?.();
1386
+ if (!size || !Number.isFinite(x) || !Number.isFinite(y)) return;
1387
+ const props = { x: Math.round(x), y: Math.round(y), vw: Math.round(size.w), vh: Math.round(size.h), docH: Math.round(size.h), selector: "", label: "", dead: false };
1388
+ record("$click", props);
1389
+ recorder?.mark({ kind: "click", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
1390
+ if (rage.push(x, y, now())) {
1391
+ record("$rageclick", props);
1392
+ recorder?.mark({ kind: "rage", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
1393
+ recorder?.trigger("rage");
1394
+ }
1395
+ },
1396
+ setReplay: (on) => {
1397
+ replayOn = on;
1398
+ if (!on) recorder?.drop();
1399
+ recorder?.sync(appState === "active");
1400
+ },
1111
1401
  screen: (name, props) => {
1112
1402
  if (typeof name !== "string" || !name.trim()) return;
1113
1403
  const path = (name.startsWith("/") ? name : `/${name}`).slice(0, 300);
@@ -1116,6 +1406,7 @@ var createClient = (options, deps) => {
1116
1406
  currentScreen = path;
1117
1407
  crumb("navigation", path);
1118
1408
  record("$screen", { ...props, path, screen: name, ...previous ? { previous } : {} });
1409
+ recorder?.mark({ kind: "pageview", path });
1119
1410
  },
1120
1411
  identify: (id, props) => {
1121
1412
  if (typeof id !== "string" || !id) return;
@@ -1163,6 +1454,7 @@ var createClient = (options, deps) => {
1163
1454
  const before = consent;
1164
1455
  consent = next;
1165
1456
  if (next === "off") {
1457
+ recorder?.drop();
1166
1458
  queue = [];
1167
1459
  session = null;
1168
1460
  errorQueue.length = 0;
@@ -1171,10 +1463,12 @@ var createClient = (options, deps) => {
1171
1463
  installId = null;
1172
1464
  void remove(KEYS.id);
1173
1465
  void remove(KEYS.referral);
1466
+ void remove(KEYS.surveys);
1174
1467
  } else {
1175
1468
  installId = identified ?? newDistinctId();
1176
1469
  void set(KEYS.id, installId);
1177
1470
  if (referral) void set(KEYS.referral, JSON.stringify(referral));
1471
+ if (surveysDone.size) void set(KEYS.surveys, JSON.stringify([...surveysDone]));
1178
1472
  }
1179
1473
  if (before === "off" && currentScreen && loaded) record("$screen", { path: currentScreen, screen: currentScreen });
1180
1474
  changed();
@@ -1217,6 +1511,22 @@ var createClient = (options, deps) => {
1217
1511
  isReady: () => isReady,
1218
1512
  getDistinctId: distinctId,
1219
1513
  getConsent: () => consent,
1514
+ getUser: () => {
1515
+ if (!identified) return void 0;
1516
+ const p = person;
1517
+ return { id: identified, ...typeof p?.name === "string" ? { name: p.name } : {}, ...typeof p?.email === "string" ? { email: p.email } : {} };
1518
+ },
1519
+ api: (as) => {
1520
+ if (!apiKey || consent === "off") return null;
1521
+ const known = client.getUser();
1522
+ const user = as?.name || as?.email ? { ...known, ...as.name ? { name: as.name } : {}, ...as.email ? { email: as.email } : {} } : known;
1523
+ return createApi({ host, apiKey, distinctId: distinctId(), user });
1524
+ },
1525
+ isSurveyDone: (id) => surveysDone.has(id),
1526
+ markSurveyDone: (id) => {
1527
+ surveysDone.add(id);
1528
+ if (consent === "persistent") void set(KEYS.surveys, JSON.stringify([...surveysDone].slice(-100)));
1529
+ },
1220
1530
  getConfig: () => config,
1221
1531
  subscribe: (listener) => {
1222
1532
  listeners.add(listener);
@@ -1226,6 +1536,7 @@ var createClient = (options, deps) => {
1226
1536
  close: async () => {
1227
1537
  if (closed) return;
1228
1538
  stopTimer();
1539
+ recorder?.sync(false);
1229
1540
  appStateSub?.remove();
1230
1541
  pushOff?.();
1231
1542
  pushOff = null;
@@ -1521,6 +1832,12 @@ try {
1521
1832
  } catch {
1522
1833
  expoLocalization = null;
1523
1834
  }
1835
+ var viewShot = null;
1836
+ try {
1837
+ viewShot = require("react-native-view-shot");
1838
+ } catch {
1839
+ viewShot = null;
1840
+ }
1524
1841
  var expoNotifications = null;
1525
1842
  try {
1526
1843
  expoNotifications = require("expo-notifications");
@@ -1594,6 +1911,30 @@ var readContext = () => {
1594
1911
  for (const k of Object.keys(ctx)) if (ctx[k] === void 0) delete ctx[k];
1595
1912
  return ctx;
1596
1913
  };
1914
+ var recorderDeps = () => {
1915
+ const capture = viewShot?.captureScreen;
1916
+ if (!capture) return null;
1917
+ let keyboard = false;
1918
+ try {
1919
+ RN2.Keyboard.addListener("keyboardWillShow", () => keyboard = true);
1920
+ RN2.Keyboard.addListener("keyboardDidShow", () => keyboard = true);
1921
+ RN2.Keyboard.addListener("keyboardDidHide", () => keyboard = false);
1922
+ } catch {
1923
+ return null;
1924
+ }
1925
+ return {
1926
+ capture: async (width) => {
1927
+ const s = RN2.Dimensions.get("screen");
1928
+ if (!s?.width) return null;
1929
+ const w = Math.round(width);
1930
+ const h3 = Math.round(s.height / s.width * width);
1931
+ const img = await capture({ format: "jpg", quality: 0.5, result: "base64", width: w, height: h3 });
1932
+ return img ? { img: img.replace(/^data:image\/\w+;base64,/, ""), w, h: h3 } : null;
1933
+ },
1934
+ keyboardOpen: () => keyboard,
1935
+ masks: measureMasks
1936
+ };
1937
+ };
1597
1938
  var nativeDeps = (overrides = {}) => ({
1598
1939
  storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
1599
1940
  fetch: ((url, init) => fetch(url, init)),
@@ -1607,17 +1948,815 @@ var nativeDeps = (overrides = {}) => ({
1607
1948
  },
1608
1949
  initialUrl: () => RN2.Linking.getInitialURL(),
1609
1950
  openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
1610
- push: expoPush()
1951
+ push: expoPush(),
1952
+ recorder: recorderDeps()
1611
1953
  });
1612
1954
  var optionalModules = () => ({
1613
1955
  asyncStorage: !!asyncStorage(),
1614
1956
  expoDevice: !!expoDevice,
1615
1957
  expoApplication: !!expoApplication,
1616
1958
  expoLocalization: !!expoLocalization,
1617
- expoNotifications: !!expoNotifications
1959
+ expoNotifications: !!expoNotifications,
1960
+ viewShot: !!viewShot?.captureScreen
1618
1961
  });
1619
1962
 
1963
+ // src/native/widgets-view.ts
1964
+ var import_react2 = require("react");
1965
+ var RN3 = require("react-native");
1966
+ var usePalette = (appearance = {}) => {
1967
+ const scheme = RN3.useColorScheme?.();
1968
+ const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
1969
+ return { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
1970
+ };
1971
+ var kit = (p) => {
1972
+ const text2 = (children, style = {}) => (0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
1973
+ const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
1974
+ const title = (children) => (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700", flex: 1 } }, children);
1975
+ const btn = (label, kind, onPress, disabled = false, style = {}) => (0, import_react2.createElement)(
1976
+ RN3.Pressable,
1977
+ {
1978
+ key: label,
1979
+ onPress,
1980
+ disabled,
1981
+ accessibilityRole: "button",
1982
+ accessibilityState: { disabled },
1983
+ style: {
1984
+ height: 44,
1985
+ paddingHorizontal: 16,
1986
+ borderRadius: 10,
1987
+ alignItems: "center",
1988
+ justifyContent: "center",
1989
+ opacity: disabled ? 0.55 : 1,
1990
+ backgroundColor: kind === "primary" ? p.accent : p.bg,
1991
+ borderWidth: kind === "secondary" ? 1 : 0,
1992
+ borderColor: p.line,
1993
+ ...style
1994
+ }
1995
+ },
1996
+ text2(label, { color: kind === "primary" ? "#ffffff" : p.fg, fontWeight: "600" })
1997
+ );
1998
+ const closeX = (label, onPress) => (0, import_react2.createElement)(RN3.Pressable, { onPress, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 12, style: { padding: 4 } }, text2("\xD7", { fontSize: 24, color: p.muted }));
1999
+ const field = (o) => (0, import_react2.createElement)(RN3.TextInput, {
2000
+ value: o.value,
2001
+ onChangeText: o.onChange,
2002
+ placeholder: o.placeholder,
2003
+ placeholderTextColor: p.muted,
2004
+ multiline: !!o.multiline,
2005
+ maxLength: o.max ?? 2e3,
2006
+ accessibilityLabel: o.placeholder,
2007
+ ...o.email ? { keyboardType: "email-address", autoCapitalize: "none", autoComplete: "email", textContentType: "emailAddress" } : {},
2008
+ ...o.onSubmit ? { onSubmitEditing: o.onSubmit, returnKeyType: "send" } : {},
2009
+ style: {
2010
+ minHeight: o.multiline ? 72 : 44,
2011
+ borderWidth: 1,
2012
+ borderColor: p.line,
2013
+ borderRadius: 10,
2014
+ paddingHorizontal: 12,
2015
+ paddingVertical: 10,
2016
+ color: p.fg,
2017
+ backgroundColor: p.bg,
2018
+ textAlignVertical: o.multiline ? "top" : "center",
2019
+ ...o.style
2020
+ }
2021
+ });
2022
+ const chip = (label, on, onPress, key = label) => (0, import_react2.createElement)(
2023
+ RN3.Pressable,
2024
+ {
2025
+ key,
2026
+ onPress,
2027
+ accessibilityRole: "button",
2028
+ accessibilityState: { selected: on },
2029
+ style: { paddingHorizontal: 12, height: 32, borderRadius: 16, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
2030
+ },
2031
+ text2(label, { fontSize: 13, fontWeight: "600", color: on ? p.accent : p.fg })
2032
+ );
2033
+ const page = (heading, onClose, children) => (0, import_react2.createElement)(
2034
+ RN3.Modal,
2035
+ { visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: onClose },
2036
+ (0, import_react2.createElement)(
2037
+ RN3.KeyboardAvoidingView,
2038
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
2039
+ (0, import_react2.createElement)(
2040
+ RN3.View,
2041
+ { style: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: p.line } },
2042
+ title(heading),
2043
+ closeX("Close", onClose)
2044
+ ),
2045
+ ...children
2046
+ )
2047
+ );
2048
+ return { text: text2, muted, title, btn, closeX, field, chip, page };
2049
+ };
2050
+ var timeAgo = (iso) => {
2051
+ const t = new Date(iso).getTime();
2052
+ if (Number.isNaN(t)) return "";
2053
+ const d = Math.max(0, Date.now() - t) / 1e3;
2054
+ if (d < 60) return "just now";
2055
+ if (d < 3600) return `${Math.floor(d / 60)}m ago`;
2056
+ if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
2057
+ return `${Math.floor(d / 86400)}d ago`;
2058
+ };
2059
+ var opener = () => {
2060
+ let open = false;
2061
+ const listeners = /* @__PURE__ */ new Set();
2062
+ const set = (v) => {
2063
+ open = v;
2064
+ listeners.forEach((l) => l(v));
2065
+ };
2066
+ const useOpen = () => {
2067
+ const [v, setV] = (0, import_react2.useState)(open);
2068
+ (0, import_react2.useEffect)(() => {
2069
+ listeners.add(setV);
2070
+ setV(open);
2071
+ return () => {
2072
+ listeners.delete(setV);
2073
+ };
2074
+ }, []);
2075
+ return v;
2076
+ };
2077
+ return { set, get: () => open, useOpen };
2078
+ };
2079
+ var isAnswered = (a) => Array.isArray(a) ? a.length > 0 : typeof a === "number" ? true : !!a?.trim();
2080
+ var SurveySheet = ({ survey, api, p, onDone }) => {
2081
+ const k = kit(p);
2082
+ const [step, setStep] = (0, import_react2.useState)(0);
2083
+ const [answers, setAnswers] = (0, import_react2.useState)({});
2084
+ const [done, setDone] = (0, import_react2.useState)(false);
2085
+ const [error, setError] = (0, import_react2.useState)("");
2086
+ const started = (0, import_react2.useRef)(Date.now());
2087
+ const finished = (0, import_react2.useRef)(false);
2088
+ const q = survey.questions[step];
2089
+ const last = step === survey.questions.length - 1;
2090
+ const value = answers[q.key];
2091
+ (0, import_react2.useEffect)(() => {
2092
+ api.seen(survey.id).catch(() => void 0);
2093
+ }, [api, survey.id]);
2094
+ (0, import_react2.useEffect)(() => {
2095
+ if (!done) return;
2096
+ const t = setTimeout(onDone, 2500);
2097
+ return () => clearTimeout(t);
2098
+ }, [done, onDone]);
2099
+ const finish = (completed, all = answers) => {
2100
+ if (finished.current) return;
2101
+ finished.current = true;
2102
+ api.respond(survey.id, { answers: all, completed, seconds: Math.round((Date.now() - started.current) / 1e3) }).catch(() => void 0);
2103
+ };
2104
+ const dismiss = () => {
2105
+ if (Object.values(answers).some(isAnswered)) finish(false);
2106
+ onDone();
2107
+ };
2108
+ const next = () => {
2109
+ if (q.required && !isAnswered(value)) return setError("Please answer this question.");
2110
+ setError("");
2111
+ if (!last) return setStep(step + 1);
2112
+ finish(true);
2113
+ setDone(true);
2114
+ };
2115
+ const pick = (v) => {
2116
+ setAnswers((a) => ({ ...a, [q.key]: v }));
2117
+ setError("");
2118
+ };
2119
+ let input;
2120
+ if (q.kind === "nps" || q.kind === "rating") {
2121
+ const scale = q.kind === "nps" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);
2122
+ const start = q.kind === "nps" ? 0 : 1;
2123
+ input = (0, import_react2.createElement)(
2124
+ RN3.View,
2125
+ { style: { gap: 6 } },
2126
+ (0, import_react2.createElement)(
2127
+ RN3.View,
2128
+ { style: { flexDirection: "row", flexWrap: "wrap", gap: 6 }, accessibilityRole: "radiogroup", accessibilityLabel: q.title },
2129
+ ...Array.from({ length: scale }, (_, i) => {
2130
+ const n = i + start;
2131
+ const on = value === n;
2132
+ return (0, import_react2.createElement)(
2133
+ RN3.Pressable,
2134
+ {
2135
+ key: n,
2136
+ onPress: () => pick(n),
2137
+ accessibilityRole: "radio",
2138
+ accessibilityState: { checked: on },
2139
+ accessibilityLabel: `${n} of ${scale - 1 + start}`,
2140
+ style: { width: 40, height: 40, borderRadius: 8, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.accent : p.bg }
2141
+ },
2142
+ k.text(String(n), { color: on ? "#ffffff" : p.fg, fontWeight: "600" })
2143
+ );
2144
+ })
2145
+ ),
2146
+ q.low || q.high || q.kind === "nps" ? (0, import_react2.createElement)(RN3.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, k.muted(q.low ?? (q.kind === "nps" ? "Not likely" : "")), k.muted(q.high ?? (q.kind === "nps" ? "Very likely" : ""))) : null
2147
+ );
2148
+ } else if (q.kind === "single" || q.kind === "multi") {
2149
+ const multi = q.kind === "multi";
2150
+ const picked = multi ? value ?? [] : [];
2151
+ input = (0, import_react2.createElement)(
2152
+ RN3.View,
2153
+ { style: { gap: 6 } },
2154
+ ...(q.options ?? []).map((o) => {
2155
+ const on = multi ? picked.includes(o) : value === o;
2156
+ return (0, import_react2.createElement)(
2157
+ RN3.Pressable,
2158
+ {
2159
+ key: o,
2160
+ onPress: () => pick(multi ? on ? picked.filter((x) => x !== o) : [...picked, o] : o),
2161
+ accessibilityRole: multi ? "checkbox" : "radio",
2162
+ accessibilityState: { checked: on },
2163
+ style: { minHeight: 44, paddingHorizontal: 12, borderRadius: 10, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
2164
+ },
2165
+ k.text(o, { fontWeight: "500" })
2166
+ );
2167
+ })
2168
+ );
2169
+ } else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? "Type your answer", multiline: true });
2170
+ const body = done ? [k.text(survey.thanks || "Thanks for your feedback.", { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
2171
+ (0, import_react2.createElement)(
2172
+ RN3.View,
2173
+ { key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
2174
+ ...survey.questions.map((x, i) => (0, import_react2.createElement)(RN3.View, { key: x.key, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= step ? p.accent : p.line } })),
2175
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
2176
+ k.closeX("Close survey", dismiss)
2177
+ ),
2178
+ (0, import_react2.createElement)(RN3.Text, { key: "q", accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700" } }, q.title, q.required ? "" : (0, import_react2.createElement)(RN3.Text, { style: { color: p.muted, fontSize: 13, fontWeight: "400" } }, " optional")),
2179
+ q.description ? k.muted(q.description) : null,
2180
+ input,
2181
+ error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2182
+ k.btn(last ? "Send" : "Next", "primary", next)
2183
+ ];
2184
+ return (0, import_react2.createElement)(
2185
+ RN3.Modal,
2186
+ { visible: true, transparent: true, animationType: "slide", onRequestClose: dismiss },
2187
+ (0, import_react2.createElement)(
2188
+ RN3.KeyboardAvoidingView,
2189
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: "rgba(0,0,0,0.35)", justifyContent: "flex-end" } },
2190
+ (0, import_react2.createElement)(
2191
+ RN3.View,
2192
+ {
2193
+ accessibilityViewIsModal: true,
2194
+ accessibilityLabel: survey.name,
2195
+ style: { backgroundColor: p.bg, borderTopLeftRadius: 16, borderTopRightRadius: 16, padding: 20, paddingBottom: 34, gap: 12 }
2196
+ },
2197
+ ...body
2198
+ )
2199
+ )
2200
+ );
2201
+ };
2202
+ var MRRocketSurveys = ({ client, appearance }) => {
2203
+ const p = usePalette(appearance);
2204
+ const [survey, setSurvey] = (0, import_react2.useState)(null);
2205
+ const [who, setWho] = (0, import_react2.useState)(() => client.getDistinctId());
2206
+ (0, import_react2.useEffect)(() => client.subscribe(() => setWho(client.getDistinctId())), [client]);
2207
+ (0, import_react2.useEffect)(() => {
2208
+ let stop = false;
2209
+ const ctrl = new AbortController();
2210
+ const timer = setTimeout(() => {
2211
+ void client.ready.then(() => {
2212
+ const api = client.api();
2213
+ if (stop || !api) return;
2214
+ api.survey("", ctrl.signal).then((r) => {
2215
+ const s = r.survey;
2216
+ if (stop || !s || !s.questions.length || client.isSurveyDone(s.id)) return;
2217
+ setSurvey({ s, api });
2218
+ }).catch(() => void 0);
2219
+ });
2220
+ }, 3e3);
2221
+ return () => {
2222
+ stop = true;
2223
+ clearTimeout(timer);
2224
+ ctrl.abort();
2225
+ };
2226
+ }, [client, who]);
2227
+ const done = (0, import_react2.useCallback)(() => {
2228
+ setSurvey((cur) => {
2229
+ if (cur) client.markSurveyDone(cur.s.id);
2230
+ return null;
2231
+ });
2232
+ }, [client]);
2233
+ if (!survey) return null;
2234
+ return (0, import_react2.createElement)(SurveySheet, { key: survey.s.id, survey: survey.s, api: survey.api, p, onDone: done });
2235
+ };
2236
+ var feedbackOpen = opener();
2237
+ var useFeedback = () => ({ open: () => feedbackOpen.set(true), close: () => feedbackOpen.set(false) });
2238
+ var FeedbackSheet = ({ client, p, title }) => {
2239
+ const k = kit(p);
2240
+ const [tab, setTab] = (0, import_react2.useState)("top");
2241
+ const [posts, setPosts] = (0, import_react2.useState)(null);
2242
+ const [boards, setBoards] = (0, import_react2.useState)([]);
2243
+ const [error, setError] = (0, import_react2.useState)("");
2244
+ const [board, setBoard] = (0, import_react2.useState)("feature");
2245
+ const [subject, setSubject] = (0, import_react2.useState)("");
2246
+ const [details, setDetails] = (0, import_react2.useState)("");
2247
+ const [name, setName] = (0, import_react2.useState)("");
2248
+ const [email, setEmail] = (0, import_react2.useState)("");
2249
+ const [sending, setSending] = (0, import_react2.useState)(false);
2250
+ const [sent, setSent] = (0, import_react2.useState)(false);
2251
+ const close = () => feedbackOpen.set(false);
2252
+ const user = client.getUser();
2253
+ (0, import_react2.useEffect)(() => {
2254
+ const api = client.api();
2255
+ if (!api) return setError("Feedback is off while tracking is off.");
2256
+ const ctrl = new AbortController();
2257
+ api.list({ sort: "top" }, ctrl.signal).then((l) => {
2258
+ setPosts(l.posts);
2259
+ setBoards(l.boards);
2260
+ if (l.boards.length && !l.boards.some((b) => b.id === "feature")) setBoard(l.boards[0].id);
2261
+ }).catch((e) => e.name !== "AbortError" && setError(e.message));
2262
+ return () => ctrl.abort();
2263
+ }, [client]);
2264
+ const vote = (post) => {
2265
+ const api = client.api();
2266
+ if (!api) return;
2267
+ setPosts((all) => all?.map((x) => x.id === post.id ? { ...x, voted: !x.voted, votes: x.votes + (x.voted ? -1 : 1) } : x) ?? null);
2268
+ api.vote(post.id).then((r) => setPosts((all) => all?.map((x) => x.id === post.id ? { ...x, voted: r.voted, votes: r.votes } : x) ?? null)).catch((e) => {
2269
+ setError(e.message);
2270
+ setPosts((all) => all?.map((x) => x.id === post.id ? post : x) ?? null);
2271
+ });
2272
+ };
2273
+ const submit = () => {
2274
+ const api = client.api({ name: name.trim(), email: email.trim() });
2275
+ if (!api || subject.trim().length < 3 || sending) return;
2276
+ setSending(true);
2277
+ setError("");
2278
+ api.submit({ title: subject.trim(), body: details.trim() || void 0, board }).then((post) => {
2279
+ setPosts((all) => [post, ...all ?? []]);
2280
+ setSubject("");
2281
+ setDetails("");
2282
+ setSent(true);
2283
+ }).catch((e) => setError(e.message)).finally(() => setSending(false));
2284
+ };
2285
+ const tabs = (0, import_react2.createElement)(
2286
+ RN3.View,
2287
+ { style: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingTop: 12 } },
2288
+ k.chip("Top requests", tab === "top", () => setTab("top")),
2289
+ k.chip("Suggest", tab === "new", () => {
2290
+ setTab("new");
2291
+ setSent(false);
2292
+ })
2293
+ );
2294
+ const list = (0, import_react2.createElement)(
2295
+ RN3.ScrollView,
2296
+ { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
2297
+ error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2298
+ posts === null && !error ? k.muted("Loading\u2026") : null,
2299
+ posts?.length === 0 ? k.muted("No requests yet. Be the first to suggest one.") : null,
2300
+ ...(posts ?? []).map(
2301
+ (post) => (0, import_react2.createElement)(
2302
+ RN3.View,
2303
+ { key: post.id, style: { flexDirection: "row", gap: 12, alignItems: "flex-start", paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: p.line } },
2304
+ (0, import_react2.createElement)(
2305
+ RN3.Pressable,
2306
+ {
2307
+ onPress: () => vote(post),
2308
+ accessibilityRole: "button",
2309
+ accessibilityState: { selected: post.voted },
2310
+ accessibilityLabel: `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
2311
+ style: { width: 44, minHeight: 48, borderRadius: 10, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: post.voted ? p.accent : p.line }
2312
+ },
2313
+ k.text("\u25B2", { fontSize: 11, color: post.voted ? p.accent : p.muted }),
2314
+ k.text(String(post.votes), { fontWeight: "700", fontSize: 14, color: post.voted ? p.accent : p.fg })
2315
+ ),
2316
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 2 } }, k.text(post.title, { fontWeight: "600" }), post.body ? k.muted(post.body.slice(0, 160)) : null)
2317
+ )
2318
+ )
2319
+ );
2320
+ const form = (0, import_react2.createElement)(
2321
+ RN3.ScrollView,
2322
+ { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 }, keyboardShouldPersistTaps: "handled" },
2323
+ sent ? k.text("Thanks! Your request is on the board.", { fontWeight: "600" }) : null,
2324
+ boards.length > 1 ? (0, import_react2.createElement)(RN3.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 6 } }, ...boards.map((b) => k.chip(b.label, board === b.id, () => setBoard(b.id), b.id))) : null,
2325
+ k.field({ value: subject, onChange: setSubject, placeholder: "Short title", max: 120 }),
2326
+ k.field({ value: details, onChange: setDetails, placeholder: "Details (optional)", multiline: true }),
2327
+ ...user?.email ? [] : [
2328
+ k.muted("Your name and email are optional; they let the team follow up."),
2329
+ k.field({ value: name, onChange: setName, placeholder: "Your name (optional)", max: 100 }),
2330
+ k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional)", max: 200, email: true })
2331
+ ],
2332
+ error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2333
+ k.btn(sending ? "Sending\u2026" : "Send request", "primary", submit, subject.trim().length < 3 || sending)
2334
+ );
2335
+ return k.page(title, close, [tabs, tab === "top" ? list : form]);
2336
+ };
2337
+ var MRRocketFeedback = ({ client, appearance, options = {} }) => {
2338
+ const p = usePalette(appearance);
2339
+ const open = feedbackOpen.useOpen();
2340
+ return open ? (0, import_react2.createElement)(FeedbackSheet, { client, p, title: options.title ?? "Feedback" }) : null;
2341
+ };
2342
+ var POLL_OPEN_MS = 4e3;
2343
+ var POLL_WAITING_MS = 8e3;
2344
+ var POLL_IDLE_MS = 3e4;
2345
+ var WAITING_FOR_MS = 10 * 6e4;
2346
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
2347
+ var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
2348
+ var msgView = "home";
2349
+ var messengerOpen = opener();
2350
+ var openAt = (v = "home") => {
2351
+ msgView = v;
2352
+ messengerOpen.set(true);
2353
+ };
2354
+ var useMessenger = () => ({
2355
+ open: (view) => openAt(view),
2356
+ close: () => messengerOpen.set(false),
2357
+ toggle: () => messengerOpen.get() ? messengerOpen.set(false) : openAt()
2358
+ });
2359
+ var haptics = null;
2360
+ try {
2361
+ haptics = require("expo-haptics");
2362
+ } catch {
2363
+ haptics = null;
2364
+ }
2365
+ var buzz = () => {
2366
+ try {
2367
+ void haptics?.notificationAsync?.(haptics.NotificationFeedbackType?.Success)?.catch(() => void 0);
2368
+ } catch {
2369
+ }
2370
+ };
2371
+ var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
2372
+ var useChat = (client, o, open, view) => {
2373
+ const [thread, setThread] = (0, import_react2.useState)(null);
2374
+ const [loaded, setLoaded] = (0, import_react2.useState)(false);
2375
+ const [error, setError] = (0, import_react2.useState)("");
2376
+ const [unseen, setUnseen] = (0, import_react2.useState)([]);
2377
+ const known = (0, import_react2.useRef)(null);
2378
+ const watching = open && view === "messages";
2379
+ const watch = (0, import_react2.useRef)(watching);
2380
+ watch.current = watching;
2381
+ const [active, setActive] = (0, import_react2.useState)(RN3.AppState.currentState === "active");
2382
+ (0, import_react2.useEffect)(() => {
2383
+ const sub = RN3.AppState.addEventListener("change", (st) => setActive(st === "active"));
2384
+ return () => sub.remove();
2385
+ }, []);
2386
+ const take = (0, import_react2.useCallback)(
2387
+ (t) => {
2388
+ const team = teamOf(t);
2389
+ if (known.current !== null) {
2390
+ const i = team.findIndex((m) => m.id === known.current);
2391
+ const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
2392
+ if (fresh.length) {
2393
+ if (o.haptics !== false) buzz();
2394
+ if (!watch.current) setUnseen((u) => [...u, ...fresh]);
2395
+ }
2396
+ }
2397
+ known.current = team.at(-1)?.id ?? "";
2398
+ setThread(t);
2399
+ },
2400
+ [o.haptics]
2401
+ );
2402
+ (0, import_react2.useEffect)(() => {
2403
+ if (watching) setUnseen([]);
2404
+ }, [watching]);
2405
+ (0, import_react2.useEffect)(() => {
2406
+ if (!active) return;
2407
+ let stop = false;
2408
+ let timer;
2409
+ let ctrl = null;
2410
+ const tick = async () => {
2411
+ const api = client.api();
2412
+ if (!api) {
2413
+ setLoaded(true);
2414
+ return;
2415
+ }
2416
+ ctrl = new AbortController();
2417
+ let t = null;
2418
+ try {
2419
+ await client.ready;
2420
+ t = await api.chat(ctrl.signal);
2421
+ if (stop) return;
2422
+ take(t);
2423
+ setError("");
2424
+ } catch (e) {
2425
+ if (e.name === "AbortError" || stop) return;
2426
+ if (watch.current) setError(e.message);
2427
+ }
2428
+ setLoaded(true);
2429
+ const waiting = !!t?.messages.some((m) => m.from === "visitor") && t?.conversation?.status !== "solved";
2430
+ const wrote = (t?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
2431
+ const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
2432
+ const ms = watch.current ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : waiting || open ? POLL_IDLE_MS : 0;
2433
+ if (!stop && ms) timer = setTimeout(tick, ms);
2434
+ };
2435
+ void tick();
2436
+ return () => {
2437
+ stop = true;
2438
+ ctrl?.abort();
2439
+ clearTimeout(timer);
2440
+ };
2441
+ }, [client, take, active, open, watching]);
2442
+ return { thread, loaded, error, setError, unseen, take };
2443
+ };
2444
+ var useArticles = (client, on, query) => {
2445
+ const [list, setList] = (0, import_react2.useState)(null);
2446
+ const [any, setAny] = (0, import_react2.useState)(null);
2447
+ (0, import_react2.useEffect)(() => {
2448
+ if (!on) return;
2449
+ let stop = false;
2450
+ const t = setTimeout(
2451
+ () => void client.ready.then(
2452
+ () => client.api()?.articles(query.trim() || void 0).then((r) => {
2453
+ if (stop) return;
2454
+ setList(r.articles);
2455
+ if (!query.trim()) setAny(r.articles.length > 0);
2456
+ }).catch(() => !stop && setList((l) => l ?? []))
2457
+ ),
2458
+ query ? 250 : 0
2459
+ );
2460
+ return () => {
2461
+ stop = true;
2462
+ clearTimeout(t);
2463
+ };
2464
+ }, [client, on, query]);
2465
+ return { list, any };
2466
+ };
2467
+ var spansOf = (p, spans, base) => spans.map(
2468
+ (s, i) => (0, import_react2.createElement)(
2469
+ RN3.Text,
2470
+ {
2471
+ key: i,
2472
+ onPress: s.href ? () => void RN3.Linking.openURL(s.href).catch(() => void 0) : void 0,
2473
+ accessibilityRole: s.href ? "link" : void 0,
2474
+ style: {
2475
+ ...base,
2476
+ ...s.bold ? { fontWeight: "700" } : {},
2477
+ ...s.italic ? { fontStyle: "italic" } : {},
2478
+ ...s.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
2479
+ ...s.href ? { color: p.accent, textDecorationLine: "underline" } : {}
2480
+ }
2481
+ },
2482
+ s.text
2483
+ )
2484
+ );
2485
+ var ArticleView = ({ client, p, id, onAsk }) => {
2486
+ const k = kit(p);
2487
+ const [a, setA] = (0, import_react2.useState)(null);
2488
+ const [error, setError] = (0, import_react2.useState)("");
2489
+ const [rated, setRated] = (0, import_react2.useState)(false);
2490
+ (0, import_react2.useEffect)(() => {
2491
+ const ctrl = new AbortController();
2492
+ client.api()?.article(id, ctrl.signal).then(setA).catch((e) => e.name !== "AbortError" && setError(e.message));
2493
+ return () => ctrl.abort();
2494
+ }, [client, id]);
2495
+ if (error) return k.text(error, { color: p.danger });
2496
+ if (!a) return k.muted("Loading\u2026");
2497
+ const base = { color: p.fg, fontSize: 15, lineHeight: 22 };
2498
+ const blocks = a.blocks.map((b, i) => {
2499
+ if (b.type === "h2" || b.type === "h3") return (0, import_react2.createElement)(RN3.Text, { key: i, accessibilityRole: "header", style: { ...base, fontSize: b.type === "h2" ? 17 : 16, fontWeight: "700", marginTop: 8 } }, ...spansOf(p, b.spans, {}));
2500
+ if (b.type === "p") return (0, import_react2.createElement)(RN3.Text, { key: i, style: base }, ...spansOf(p, b.spans, {}));
2501
+ if (b.type === "quote") return (0, import_react2.createElement)(RN3.View, { key: i, style: { borderLeftWidth: 3, borderLeftColor: p.line, paddingLeft: 10 } }, (0, import_react2.createElement)(RN3.Text, { style: { ...base, color: p.muted } }, ...spansOf(p, b.spans, {})));
2502
+ if (b.type === "ul" || b.type === "ol")
2503
+ return (0, import_react2.createElement)(
2504
+ RN3.View,
2505
+ { key: i, style: { gap: 4 } },
2506
+ ...b.items.map((it, j) => (0, import_react2.createElement)(RN3.View, { key: j, style: { flexDirection: "row", gap: 8 } }, k.text(b.type === "ol" ? `${j + 1}.` : "\u2022", base), (0, import_react2.createElement)(RN3.Text, { style: { ...base, flex: 1 } }, ...spansOf(p, it, {}))))
2507
+ );
2508
+ if (b.type === "code") return (0, import_react2.createElement)(RN3.View, { key: i, style: { backgroundColor: p.soft, borderRadius: 8, padding: 10 } }, k.text(b.text, { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", fontSize: 13 }));
2509
+ return (0, import_react2.createElement)(RN3.View, { key: i, style: { height: 1, backgroundColor: p.line } });
2510
+ });
2511
+ const rate = (helpful) => {
2512
+ setRated(true);
2513
+ client.api()?.rateArticle(id, helpful).catch(() => void 0);
2514
+ if (!helpful) onAsk();
2515
+ };
2516
+ return (0, import_react2.createElement)(
2517
+ RN3.View,
2518
+ { style: { gap: 10 } },
2519
+ k.muted(a.collection),
2520
+ (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 20, fontWeight: "700" } }, a.title),
2521
+ ...blocks,
2522
+ (0, import_react2.createElement)(
2523
+ RN3.View,
2524
+ { style: { flexDirection: "row", alignItems: "center", gap: 8, padding: 12, borderRadius: 10, backgroundColor: p.soft, marginTop: 8 } },
2525
+ rated ? k.muted("Thanks for letting us know.") : k.muted("Did this answer your question?", { flex: 1 }),
2526
+ rated ? null : k.btn("Yes", "secondary", () => rate(true), false, { height: 34 }),
2527
+ rated ? null : k.btn("No", "secondary", () => rate(false), false, { height: 34 })
2528
+ ),
2529
+ k.btn("Still need help? Send us a message", "secondary", onAsk)
2530
+ );
2531
+ };
2532
+ var MessengerSheet = ({
2533
+ client,
2534
+ p,
2535
+ o,
2536
+ chat,
2537
+ view,
2538
+ setView
2539
+ }) => {
2540
+ const k = kit(p);
2541
+ const [body, setBody] = (0, import_react2.useState)("");
2542
+ const [email, setEmail] = (0, import_react2.useState)("");
2543
+ const [pending, setPending] = (0, import_react2.useState)(null);
2544
+ const [query, setQuery] = (0, import_react2.useState)("");
2545
+ const helpOn = o.help !== false;
2546
+ const arts = useArticles(client, helpOn, query);
2547
+ const scroll = (0, import_react2.useRef)(null);
2548
+ const close = () => messengerOpen.set(false);
2549
+ const greeting = o.greeting ?? "Hi! How can we help?";
2550
+ const { thread } = chat;
2551
+ const count = (thread?.messages.length ?? 0) + (pending ? 1 : 0);
2552
+ (0, import_react2.useEffect)(() => {
2553
+ if (view === "messages") setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 50);
2554
+ }, [count, view]);
2555
+ const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
2556
+ const emailOk = !email.trim() || EMAIL.test(email.trim());
2557
+ const send = async () => {
2558
+ const text2 = body.trim();
2559
+ const api = client.api();
2560
+ if (!text2 || pending || !emailOk || !api) return;
2561
+ setPending({ id: "pending", from: "visitor", body: text2, at: (/* @__PURE__ */ new Date()).toISOString() });
2562
+ setBody("");
2563
+ chat.setError("");
2564
+ try {
2565
+ chat.take(await api.sendChat({ body: text2, email: needEmail && email.trim() ? email.trim() : void 0 }));
2566
+ } catch (e) {
2567
+ setBody(text2);
2568
+ chat.setError(e.message);
2569
+ } finally {
2570
+ setPending(null);
2571
+ }
2572
+ };
2573
+ const bubble = (m, isPending = false) => {
2574
+ const mine = m.from === "visitor";
2575
+ return (0, import_react2.createElement)(
2576
+ RN3.View,
2577
+ { key: m.id, style: { alignSelf: mine ? "flex-end" : "flex-start", maxWidth: "82%", gap: 4 } },
2578
+ (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: mine ? p.accent : p.soft, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8, opacity: isPending ? 0.7 : 1 } }, k.text(m.body, { color: mine ? "#ffffff" : p.fg })),
2579
+ ...(m.articles ?? []).map(
2580
+ (a) => (0, import_react2.createElement)(
2581
+ RN3.Pressable,
2582
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 6 } },
2583
+ k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
2584
+ )
2585
+ ),
2586
+ m.at ? k.muted(`${mine ? "" : `${whoLabel[m.from]} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left" }) : null
2587
+ );
2588
+ };
2589
+ const row = (a) => (0, import_react2.createElement)(
2590
+ RN3.Pressable,
2591
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "button", style: { flexDirection: "row", alignItems: "center", paddingVertical: 11, paddingHorizontal: 4 } },
2592
+ k.text(a.title, { flex: 1 }),
2593
+ k.muted("\u203A", { fontSize: 18 })
2594
+ );
2595
+ const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.bg, borderRadius: 12, padding: 12, borderWidth: 1, borderColor: p.line, ...style } }, ...children);
2596
+ const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200 });
2597
+ const last = thread?.messages.at(-1);
2598
+ const showHelp = helpOn && arts.any !== false;
2599
+ let content;
2600
+ if (view === "home")
2601
+ content = (0, import_react2.createElement)(
2602
+ RN3.ScrollView,
2603
+ { key: "home", style: { flex: 1, backgroundColor: p.soft }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
2604
+ (0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 22, fontWeight: "700", paddingVertical: 6 } }, greeting),
2605
+ (0, import_react2.createElement)(
2606
+ RN3.Pressable,
2607
+ { onPress: () => setView("messages"), accessibilityRole: "button" },
2608
+ card([
2609
+ k.text(last ? "Recent message" : "Send us a message", { fontWeight: "700" }),
2610
+ k.muted(last ? `${last.from === "visitor" ? "You" : whoLabel[last.from]}: ${last.body}` : "We usually answer within a few hours", { marginTop: 2 })
2611
+ ])
2612
+ ),
2613
+ showHelp ? card([(0, import_react2.createElement)(RN3.Pressable, { key: "s", onPress: () => setView("help") }, (0, import_react2.createElement)(RN3.View, { pointerEvents: "none" }, k.muted("\u{1F50D} Search for help", { paddingVertical: 8 }))), ...(arts.list ?? []).slice(0, 4).map(row)]) : null
2614
+ );
2615
+ else if (view === "messages")
2616
+ content = (0, import_react2.createElement)(
2617
+ RN3.View,
2618
+ { key: "messages", style: { flex: 1 } },
2619
+ (0, import_react2.createElement)(
2620
+ RN3.ScrollView,
2621
+ { ref: scroll, style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
2622
+ bubble({ id: "greeting", from: "team", body: greeting, at: "" }),
2623
+ !chat.loaded ? k.muted("Loading\u2026") : null,
2624
+ ...(thread?.messages ?? []).map((m) => bubble(m)),
2625
+ pending ? bubble(pending, true) : null,
2626
+ thread?.conversation?.status === "solved" && !pending ? k.muted("This conversation is closed. Write to start a new one.", { textAlign: "center" }) : null
2627
+ ),
2628
+ (0, import_react2.createElement)(
2629
+ RN3.View,
2630
+ { style: { padding: 12, gap: 8, borderTopWidth: 1, borderTopColor: p.line } },
2631
+ chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
2632
+ needEmail ? k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional), so we can follow up", email: true, max: 200, style: emailOk ? {} : { borderColor: p.danger } }) : null,
2633
+ (0, import_react2.createElement)(
2634
+ RN3.View,
2635
+ { style: { flexDirection: "row", gap: 8, alignItems: "flex-end" } },
2636
+ k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { flex: 1, minHeight: 44, maxHeight: 120 } }),
2637
+ k.btn("Send", "primary", () => void send(), !body.trim() || !!pending || !emailOk)
2638
+ )
2639
+ )
2640
+ );
2641
+ else if (view === "help") {
2642
+ const groups = /* @__PURE__ */ new Map();
2643
+ for (const a of arts.list ?? []) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
2644
+ content = (0, import_react2.createElement)(
2645
+ RN3.ScrollView,
2646
+ { key: "help", style: { flex: 1, backgroundColor: p.soft }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
2647
+ search,
2648
+ arts.list === null ? k.muted("Loading\u2026") : null,
2649
+ arts.list?.length === 0 ? (0, import_react2.createElement)(RN3.View, { style: { gap: 10, alignItems: "center", paddingVertical: 24 } }, k.muted(query.trim() ? `Nothing found for \u201C${query.trim()}\u201D.` : "No help articles yet."), k.btn("Send us a message", "primary", () => setView("messages"))) : null,
2650
+ ...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row))] : [] : [...groups].map(([name, items]) => (0, import_react2.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`), card(items.map(row))))
2651
+ );
2652
+ } else
2653
+ content = (0, import_react2.createElement)(
2654
+ RN3.ScrollView,
2655
+ { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { padding: 16, paddingBottom: 32 } },
2656
+ (0, import_react2.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
2657
+ );
2658
+ const current = typeof view === "string" ? view : "help";
2659
+ const tab = (v, name, glyph, badge = 0) => (0, import_react2.createElement)(
2660
+ RN3.Pressable,
2661
+ { key: v, onPress: () => setView(v), accessibilityRole: "tab", accessibilityState: { selected: current === v }, style: { flex: 1, alignItems: "center", paddingTop: 8, paddingBottom: 6, gap: 2 } },
2662
+ k.text(glyph, { fontSize: 18, color: current === v ? p.fg : p.muted }),
2663
+ k.text(name, { fontSize: 12, fontWeight: "600", color: current === v ? p.fg : p.muted }),
2664
+ badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 4, right: "30%", minWidth: 18, height: 18, borderRadius: 9, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(Math.min(badge, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
2665
+ );
2666
+ const titleOf = typeof view !== "string" || view === "help" ? "Help" : view === "messages" ? o.title ?? "Chat with us" : o.title ?? "Chat with us";
2667
+ return k.page(titleOf, close, [
2668
+ typeof view !== "string" ? (0, import_react2.createElement)(RN3.Pressable, { key: "back", onPress: () => setView("help"), accessibilityRole: "button", style: { paddingHorizontal: 16, paddingVertical: 8 } }, k.text("\u2039 All articles", { color: p.accent, fontWeight: "600" })) : null,
2669
+ content,
2670
+ (0, import_react2.createElement)(
2671
+ RN3.View,
2672
+ { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", borderTopWidth: 1, borderTopColor: p.line, paddingBottom: 20, backgroundColor: p.bg } },
2673
+ tab("home", "Home", "\u2302"),
2674
+ tab("messages", "Messages", "\u2709\uFE0E", view === "messages" ? 0 : chat.unseen.length),
2675
+ showHelp ? tab("help", "Help", "?") : null
2676
+ )
2677
+ ]);
2678
+ };
2679
+ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
2680
+ const [look, setLook] = (0, import_react2.useState)(null);
2681
+ (0, import_react2.useEffect)(() => {
2682
+ const ctrl = new AbortController();
2683
+ void client.ready.then(
2684
+ () => client.api()?.messengerLook(ctrl.signal).then(setLook).catch(() => void 0)
2685
+ );
2686
+ return () => ctrl.abort();
2687
+ }, [client]);
2688
+ const defined = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== void 0));
2689
+ const options = look ? { title: look.title, greeting: look.greeting, help: look.help, preview: look.preview, askEmail: look.askEmail, ...defined } : given;
2690
+ const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
2691
+ const k = kit(p);
2692
+ const open = messengerOpen.useOpen();
2693
+ const [view, setViewState] = (0, import_react2.useState)(msgView);
2694
+ const [banner, setBanner] = (0, import_react2.useState)(null);
2695
+ (0, import_react2.useEffect)(() => {
2696
+ if (open) setViewState(msgView);
2697
+ }, [open]);
2698
+ const setView = (v) => {
2699
+ msgView = v;
2700
+ setViewState(v);
2701
+ };
2702
+ const chat = useChat(client, options, open, view);
2703
+ const latest = chat.unseen.at(-1);
2704
+ (0, import_react2.useEffect)(() => {
2705
+ if (!latest || open || options.preview === false) return;
2706
+ setBanner(latest);
2707
+ const t = setTimeout(() => setBanner(null), 6e3);
2708
+ return () => clearTimeout(t);
2709
+ }, [latest, open, options.preview]);
2710
+ (0, import_react2.useEffect)(() => {
2711
+ if (open) setBanner(null);
2712
+ }, [open]);
2713
+ const unseen = chat.unseen.length;
2714
+ const launcher = options.launcher ? (0, import_react2.createElement)(
2715
+ RN3.Pressable,
2716
+ {
2717
+ key: "launcher",
2718
+ onPress: () => openAt(unseen ? "messages" : "home"),
2719
+ accessibilityRole: "button",
2720
+ accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
2721
+ style: { position: "absolute", right: 20, bottom: 34, width: 54, height: 54, borderRadius: 27, backgroundColor: p.accent, alignItems: "center", justifyContent: "center", shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, elevation: 6 }
2722
+ },
2723
+ k.text("\u{1F4AC}", { fontSize: 22 }),
2724
+ unseen ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: p.danger, alignItems: "center", justifyContent: "center", borderWidth: 2, borderColor: p.bg } }, k.text(String(Math.min(unseen, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
2725
+ ) : null;
2726
+ const toast = banner ? (0, import_react2.createElement)(
2727
+ RN3.Pressable,
2728
+ {
2729
+ key: "banner",
2730
+ onPress: () => openAt("messages"),
2731
+ accessibilityRole: "button",
2732
+ accessibilityLiveRegion: "polite",
2733
+ style: { position: "absolute", top: 54, left: 16, right: 16, backgroundColor: p.bg, borderRadius: 14, padding: 14, gap: 2, borderWidth: 1, borderColor: p.line, shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 16, elevation: 8 }
2734
+ },
2735
+ k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
2736
+ (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15 } }, banner.body)
2737
+ ) : null;
2738
+ if (!launcher && !toast && !open) return null;
2739
+ return (0, import_react2.createElement)(
2740
+ RN3.View,
2741
+ { pointerEvents: "box-none", style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 } },
2742
+ launcher,
2743
+ toast,
2744
+ open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView }) : null
2745
+ );
2746
+ };
2747
+
1620
2748
  // src/native/index.tsx
2749
+ var RN4 = require("react-native");
2750
+ var MRRocketMask = ({ children, style }) => {
2751
+ const ref = (0, import_react3.useRef)(null);
2752
+ (0, import_react3.useEffect)(() => {
2753
+ maskedViews.add(ref);
2754
+ return () => {
2755
+ maskedViews.delete(ref);
2756
+ };
2757
+ }, []);
2758
+ return (0, import_react3.createElement)(RN4.View, { ref, collapsable: false, style }, children);
2759
+ };
1621
2760
  var shared = null;
1622
2761
  var sharedKey = "";
1623
2762
  var createMRRocket = (options) => {
@@ -1633,20 +2772,40 @@ var createMRRocket = (options) => {
1633
2772
  return shared;
1634
2773
  };
1635
2774
  var getMRRocket = () => shared;
1636
- var Ctx = (0, import_react2.createContext)(null);
1637
- var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, ...options }) => {
1638
- const [client] = (0, import_react2.useState)(() => given ?? createMRRocket({ ...options, consent }));
1639
- (0, import_react2.useEffect)(() => {
2775
+ var Ctx = (0, import_react3.createContext)(null);
2776
+ var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, surveys = true, messenger = {}, feedback = {}, heatmaps = true, ...options }) => {
2777
+ const [client] = (0, import_react3.useState)(() => given ?? createMRRocket({ ...options, consent }));
2778
+ (0, import_react3.useEffect)(() => {
1640
2779
  if (consent) client.setConsent(consent);
1641
2780
  }, [client, consent]);
1642
- return (0, import_react2.createElement)(Ctx.Provider, { value: client }, children, flows ? (0, import_react2.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null);
2781
+ (0, import_react3.useEffect)(() => {
2782
+ if (options.replay !== void 0) client.setReplay(options.replay);
2783
+ }, [client, options.replay]);
2784
+ const start = (0, import_react3.useRef)(null);
2785
+ const touch = (e, end) => {
2786
+ const n = e.nativeEvent;
2787
+ const x = n.pageX ?? n.changedTouches?.[0]?.pageX;
2788
+ const y = n.pageY ?? n.changedTouches?.[0]?.pageY;
2789
+ if (x === void 0 || y === void 0) return;
2790
+ if (!end) start.current = { x, y, t: Date.now() };
2791
+ else if (start.current && Math.hypot(x - start.current.x, y - start.current.y) < 10 && Date.now() - start.current.t < 500) client.tap(x, y);
2792
+ };
2793
+ return (0, import_react3.createElement)(
2794
+ Ctx.Provider,
2795
+ { value: client },
2796
+ heatmaps ? (0, import_react3.createElement)(RN4.View, { style: { flex: 1 }, onTouchStartCapture: (e) => touch(e, false), onTouchEndCapture: (e) => touch(e, true) }, children) : children,
2797
+ flows ? (0, import_react3.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null,
2798
+ surveys ? (0, import_react3.createElement)(MRRocketSurveys, { client, appearance }) : null,
2799
+ feedback ? (0, import_react3.createElement)(MRRocketFeedback, { client, appearance, options: feedback }) : null,
2800
+ messenger ? (0, import_react3.createElement)(MRRocketMessenger, { client, appearance, options: messenger }) : null
2801
+ );
1643
2802
  };
1644
2803
  var useClient = () => {
1645
- const c = (0, import_react2.useContext)(Ctx);
2804
+ const c = (0, import_react3.useContext)(Ctx);
1646
2805
  if (!c) throw new Error("MRRocket: wrap your app in <MRRocketProvider apiKey=\u2026>.");
1647
2806
  return c;
1648
2807
  };
1649
- var useVersion = (client) => (0, import_react2.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
2808
+ var useVersion = (client) => (0, import_react3.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
1650
2809
  var useMRRocket = () => {
1651
2810
  const client = useClient();
1652
2811
  useVersion(client);
@@ -1656,6 +2815,7 @@ var useMRRocket = () => {
1656
2815
  identify: client.identify,
1657
2816
  reset: client.reset,
1658
2817
  setConsent: client.setConsent,
2818
+ setReplay: client.setReplay,
1659
2819
  captureError: client.captureError,
1660
2820
  flush: client.flush,
1661
2821
  distinctId: client.getDistinctId(),
@@ -1690,7 +2850,7 @@ var useVariant = (key) => {
1690
2850
  useVersion(client);
1691
2851
  const config = client.getConfig();
1692
2852
  const variant = client.peekVariant(key);
1693
- (0, import_react2.useEffect)(() => {
2853
+ (0, import_react3.useEffect)(() => {
1694
2854
  if (config) client.expose(key, variant);
1695
2855
  }, [client, config, key, variant]);
1696
2856
  return variant;
@@ -1701,7 +2861,7 @@ var useText = (id, fallback) => {
1701
2861
  const config = client.getConfig();
1702
2862
  const text2 = client.peekText(id);
1703
2863
  const person = client.getDistinctId();
1704
- (0, import_react2.useEffect)(() => {
2864
+ (0, import_react3.useEffect)(() => {
1705
2865
  if (config) client.exposeText(id);
1706
2866
  }, [client, config, id, person]);
1707
2867
  return text2 ?? fallback;
@@ -1726,13 +2886,13 @@ var useExpoRouterTracking = () => {
1726
2886
  console.warn("[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.");
1727
2887
  }
1728
2888
  const pathname = (router?.usePathname ?? noPathname)();
1729
- (0, import_react2.useEffect)(() => {
2889
+ (0, import_react3.useEffect)(() => {
1730
2890
  if (pathname) client.screen(pathname);
1731
2891
  }, [client, pathname]);
1732
2892
  };
1733
2893
  var useNavigationTracking = () => {
1734
2894
  const client = useClient();
1735
- return (0, import_react2.useCallback)(
2895
+ return (0, import_react3.useCallback)(
1736
2896
  (state) => {
1737
2897
  const name = activeRouteName(state);
1738
2898
  if (name) client.screen(name);