@mrrocket/sdk 0.8.0 → 0.10.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 +20 -0
  2. package/README.md +35 -1
  3. package/dist/api-client.d.ts +221 -0
  4. package/dist/api-client.js +79 -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 +348 -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 +132 -24
  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 +49 -0
  27. package/dist/messenger-ui.js +619 -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 +349 -4
  36. package/dist/react-native/index.d.ts +349 -4
  37. package/dist/react-native/index.js +1345 -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 +362 -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 +17 -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.10.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,72 @@ 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 replyLine = (t) => t === "minutes" ? "We usually reply in a few minutes" : t === "day" ? "We usually reply within a day" : "We usually reply in a few hours";
176
+ var inkOn = (hex) => {
177
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
178
+ if (!m) return "#ffffff";
179
+ const [r, g, b] = [m[1], m[2], m[3]].map((x) => {
180
+ const c = parseInt(x, 16) / 255;
181
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
182
+ });
183
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
184
+ };
185
+ var ApiError = class extends Error {
186
+ constructor(message, status) {
187
+ super(message);
188
+ __publicField(this, "status");
189
+ this.status = status;
190
+ }
191
+ };
192
+ var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
193
+ var call = async (url, init, signal) => {
194
+ let res;
195
+ try {
196
+ res = await fetch(url, {
197
+ ...init,
198
+ signal,
199
+ headers: init?.body ? { "content-type": "application/json" } : void 0
200
+ });
201
+ } catch (e) {
202
+ if (e.name === "AbortError") throw e;
203
+ throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
204
+ }
205
+ if (!res.ok) {
206
+ let detail = "";
207
+ try {
208
+ detail = (await res.json()).error ?? "";
209
+ } catch {
210
+ }
211
+ throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
212
+ }
213
+ return await res.json();
214
+ };
215
+ var createApi = (c) => {
216
+ const base = `${c.host}/api/sdk`;
217
+ const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
218
+ const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
219
+ return {
220
+ 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),
221
+ post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
222
+ roadmap: (signal) => call(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
223
+ changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
224
+ submit: (p) => post("/feedback", p),
225
+ vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
226
+ comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
227
+ comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
228
+ survey: (url, signal) => call(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, void 0, signal),
229
+ seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
230
+ respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
231
+ chat: (signal) => call(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
232
+ sendChat: (m) => post("/messages", m),
233
+ articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
234
+ article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
235
+ messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
236
+ rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
237
+ };
238
+ };
239
+
166
240
  // src/batch.ts
167
241
  var MAX_BATCH = 100;
168
242
  var MAX_BYTES = 6e4;
@@ -612,6 +686,174 @@ var createFlows = (env) => {
612
686
  return controller;
613
687
  };
614
688
 
689
+ // src/rage.ts
690
+ var RageDetector = class {
691
+ constructor(windowMs = 1e3, radius = 30, count = 3) {
692
+ __publicField(this, "windowMs", windowMs);
693
+ __publicField(this, "radius", radius);
694
+ __publicField(this, "count", count);
695
+ __publicField(this, "clicks", []);
696
+ __publicField(this, "fired", false);
697
+ }
698
+ push(x, y, t) {
699
+ const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
700
+ if (near.length !== this.clicks.length) this.fired = false;
701
+ this.clicks = [...near, { x, y, t }];
702
+ if (this.clicks.length >= this.count && !this.fired) {
703
+ this.fired = true;
704
+ return true;
705
+ }
706
+ return false;
707
+ }
708
+ };
709
+
710
+ // src/native/recorder.ts
711
+ var FRAME_MS = 1e3;
712
+ var WIDTH = 360;
713
+ var BUFFER_MS = 6e4;
714
+ var CHUNK_FRAMES = 12;
715
+ var CHUNK_CHARS = 7e5;
716
+ var MAX_MS = 30 * 6e4;
717
+ var shareOf = (s) => {
718
+ let h3 = 2166136261;
719
+ for (let i = 0; i < s.length; i++) h3 = Math.imul(h3 ^ s.charCodeAt(i), 16777619);
720
+ return (h3 >>> 0) / 4294967296;
721
+ };
722
+ var createRecorder = (deps, st) => {
723
+ let timer = null;
724
+ let busy = false;
725
+ let last = "";
726
+ let rec = null;
727
+ let frames = [];
728
+ let marks = [];
729
+ const ensure = () => {
730
+ const session = st.session();
731
+ const cfg = st.config();
732
+ if (!rec || rec.session !== session) {
733
+ frames = [];
734
+ marks = [];
735
+ last = "";
736
+ const sampled = !!cfg && shareOf(session) < (cfg.sampleRate ?? 0.1);
737
+ rec = { session, start: deps.now(), seq: 0, uploading: sampled, trigger: "sample", stopped: false };
738
+ }
739
+ return rec;
740
+ };
741
+ const send = async (r) => {
742
+ if (!frames.length || r.stopped) return;
743
+ const batch = frames.splice(0, frames.length);
744
+ const ms = marks.splice(0, marks.length);
745
+ const size = deps.screen();
746
+ const { distinctId, identified } = st.who();
747
+ const body = JSON.stringify({
748
+ key: st.apiKey,
749
+ consent: { replay: true },
750
+ sessionId: r.session,
751
+ start: r.start,
752
+ seq: r.seq++,
753
+ enc: "frames",
754
+ data: JSON.stringify(batch),
755
+ 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 },
756
+ distinctId,
757
+ identified,
758
+ trigger: r.trigger,
759
+ app: st.app
760
+ });
761
+ const status = await deps.send(body);
762
+ if (status === 403 || status === 409 || status === 413) r.stopped = true;
763
+ };
764
+ const tick = async () => {
765
+ if (busy || !st.allowed() || !st.config()) return;
766
+ const r = ensure();
767
+ if (r.stopped || deps.now() - r.start > MAX_MS) return;
768
+ if (deps.keyboardOpen()) return;
769
+ busy = true;
770
+ try {
771
+ const shot = await deps.capture(WIDTH);
772
+ if (!shot || shot.img === last) return;
773
+ last = shot.img;
774
+ const size = deps.screen();
775
+ const scale = size && size.w ? shot.w / size.w : 1;
776
+ const measured = await deps.masks();
777
+ if (measured === null) {
778
+ last = "";
779
+ return;
780
+ }
781
+ const masks = measured.map(([x, y, w, h3]) => [x * scale, y * scale, w * scale, h3 * scale].map(Math.round));
782
+ frames.push({ t: deps.now(), w: shot.w, h: shot.h, img: shot.img, masks });
783
+ if (!r.uploading) {
784
+ const since = deps.now() - BUFFER_MS;
785
+ frames = frames.filter((f) => f.t >= since);
786
+ marks = marks.filter((m) => m.t >= since);
787
+ return;
788
+ }
789
+ const chars = frames.reduce((a, f) => a + f.img.length, 0);
790
+ if (frames.length >= CHUNK_FRAMES || chars >= CHUNK_CHARS) await send(r);
791
+ } catch {
792
+ } finally {
793
+ busy = false;
794
+ }
795
+ };
796
+ return {
797
+ /** Starts or stops the frame loop to match the app state (active) and whether recording is allowed. */
798
+ sync(active) {
799
+ const on = active && st.allowed() && !!st.config();
800
+ if (on && timer === null) timer = deps.setInterval(() => void tick(), FRAME_MS);
801
+ if (!on && timer !== null) {
802
+ deps.clearInterval(timer);
803
+ timer = null;
804
+ if (rec?.uploading) void send(rec);
805
+ }
806
+ },
807
+ /** A screen, tap, rage tap or error, for the timeline. */
808
+ mark(m) {
809
+ if (!st.allowed() || !st.config()) return;
810
+ ensure();
811
+ marks.push({ t: deps.now(), ...m });
812
+ if (marks.length > 300) marks.splice(0, marks.length - 300);
813
+ },
814
+ /** A rage tap or an error: a buffering recording uploads its last minute and keeps recording. */
815
+ trigger(kind) {
816
+ if (!rec || rec.uploading || rec.stopped || !st.allowed()) return;
817
+ rec.uploading = true;
818
+ rec.trigger = kind;
819
+ void send(rec);
820
+ },
821
+ /** Sends what waits (going to the background). */
822
+ flush() {
823
+ if (rec?.uploading) return send(rec);
824
+ return Promise.resolve();
825
+ },
826
+ /** Consent withdrawn: forget everything not sent. */
827
+ drop() {
828
+ frames = [];
829
+ marks = [];
830
+ rec = null;
831
+ last = "";
832
+ },
833
+ /** For tests: one frame now. */
834
+ tick
835
+ };
836
+ };
837
+ var maskedViews = /* @__PURE__ */ new Set();
838
+ var measureMasks = () => Promise.all(
839
+ [...maskedViews].map(
840
+ (ref) => new Promise((resolve) => {
841
+ const v = ref.current;
842
+ if (!v) return resolve("none");
843
+ const t = setTimeout(() => resolve("failed"), 100);
844
+ try {
845
+ v.measureInWindow((x, y, w, h3) => {
846
+ clearTimeout(t);
847
+ resolve(w > 0 && h3 > 0 ? [x, y, w, h3] : "none");
848
+ });
849
+ } catch {
850
+ clearTimeout(t);
851
+ resolve("failed");
852
+ }
853
+ })
854
+ )
855
+ ).then((rs) => rs.includes("failed") ? null : rs.filter((r) => Array.isArray(r)));
856
+
615
857
  // src/referral.ts
616
858
  var REF_PARAM = "mrr_ref";
617
859
  var CLICK_PARAM = "mrr_click";
@@ -634,7 +876,8 @@ var KEYS = {
634
876
  queue: "mrrocket:queue",
635
877
  config: "mrrocket:config",
636
878
  installed: "mrrocket:installed",
637
- referral: "mrrocket:referral"
879
+ referral: "mrrocket:referral",
880
+ surveys: "mrrocket:surveys"
638
881
  };
639
882
  var MAX_ERRORS = 20;
640
883
  var MAX_CRUMBS = 20;
@@ -697,6 +940,8 @@ var createClient = (options, deps) => {
697
940
  let backgroundSince = null;
698
941
  const early = [];
699
942
  const exposed = /* @__PURE__ */ new Set();
943
+ const surveysDone = /* @__PURE__ */ new Set();
944
+ const rage = new RageDetector();
700
945
  let textsFor = null;
701
946
  const texts = () => {
702
947
  const id = distinctId();
@@ -728,8 +973,10 @@ var createClient = (options, deps) => {
728
973
  flows.setConfig(config);
729
974
  void flows.refresh();
730
975
  };
976
+ let recorder = null;
731
977
  const changed = () => {
732
978
  version++;
979
+ recorder?.sync(appState === "active");
733
980
  for (const l of [...listeners]) {
734
981
  try {
735
982
  l();
@@ -904,6 +1151,8 @@ var createClient = (options, deps) => {
904
1151
  ...who ? { distinctId: who } : {},
905
1152
  ...session ? { sessionId: session.id } : {}
906
1153
  });
1154
+ recorder?.mark({ kind: "error", path: currentScreen ?? "/", label: clip2(d.message, 120) });
1155
+ recorder?.trigger("error");
907
1156
  if (fatal) void sendErrors();
908
1157
  else if (errorTimer === null) errorTimer = setTimeoutFn(() => void sendErrors(), 2e3);
909
1158
  };
@@ -921,11 +1170,34 @@ var createClient = (options, deps) => {
921
1170
  previousHandler?.(error, isFatal);
922
1171
  });
923
1172
  };
1173
+ let replayOn = options.replay === true;
1174
+ recorder = deps.recorder ? createRecorder(
1175
+ {
1176
+ now,
1177
+ setInterval: setIntervalFn,
1178
+ clearInterval: clearIntervalFn,
1179
+ capture: deps.recorder.capture,
1180
+ keyboardOpen: deps.recorder.keyboardOpen,
1181
+ masks: deps.recorder.masks,
1182
+ screen: () => deps.screen?.(),
1183
+ send: (body) => post("/api/sdk/replay", body)
1184
+ },
1185
+ {
1186
+ allowed: () => replayOn && loaded && !closed && consent !== "off" && !!apiKey,
1187
+ config: () => config?.replay,
1188
+ session: () => session?.id ?? touchSession(),
1189
+ who: () => ({ distinctId: sendId(), identified: !!identified }),
1190
+ apiKey,
1191
+ app: context
1192
+ }
1193
+ ) : null;
1194
+ if (options.replay && !deps.recorder) warn("Session recordings need react-native-view-shot: npx expo install react-native-view-shot");
924
1195
  let appStateSub;
925
1196
  const onAppState = (next) => {
926
1197
  if (closed) return;
927
1198
  const prev = appState;
928
1199
  appState = next;
1200
+ recorder?.sync(next === "active");
929
1201
  if (next === "active" && prev !== "active") {
930
1202
  startTimer();
931
1203
  if (backgroundSince !== null) {
@@ -938,6 +1210,7 @@ var createClient = (options, deps) => {
938
1210
  crumb("lifecycle", "app_background");
939
1211
  if (options.lifecycle !== false) record("$app_background");
940
1212
  stopTimer();
1213
+ void recorder?.flush();
941
1214
  void persistQueue();
942
1215
  void flush();
943
1216
  if (errorQueue.length) void sendErrors();
@@ -1030,12 +1303,13 @@ var createClient = (options, deps) => {
1030
1303
  const start = async () => {
1031
1304
  let firstOpen = false;
1032
1305
  if (consent !== "off") {
1033
- const [id, stored, cached, installed, ref] = await Promise.all([
1306
+ const [id, stored, cached, installed, ref, surveys] = await Promise.all([
1034
1307
  get(KEYS.id),
1035
1308
  get(KEYS.queue),
1036
1309
  get(KEYS.config),
1037
1310
  get(KEYS.installed),
1038
- get(KEYS.referral)
1311
+ get(KEYS.referral),
1312
+ get(KEYS.surveys)
1039
1313
  ]);
1040
1314
  if (consent === "persistent") {
1041
1315
  installId = id || newDistinctId();
@@ -1058,6 +1332,14 @@ var createClient = (options, deps) => {
1058
1332
  if (r && typeof r.code === "string") referral = r;
1059
1333
  } catch {
1060
1334
  }
1335
+ try {
1336
+ const done = surveys && consent === "persistent" ? JSON.parse(surveys) : null;
1337
+ if (Array.isArray(done)) {
1338
+ for (const x of done) if (typeof x === "string") surveysDone.add(x);
1339
+ }
1340
+ } catch {
1341
+ await remove(KEYS.surveys);
1342
+ }
1061
1343
  firstOpen = !!storage && !installed;
1062
1344
  if (firstOpen) await set(KEYS.installed, String(now()));
1063
1345
  } else {
@@ -1108,6 +1390,24 @@ var createClient = (options, deps) => {
1108
1390
  record(event, props);
1109
1391
  flows.onEvent(event);
1110
1392
  },
1393
+ tap: (x, y) => {
1394
+ if (options.heatmaps === false || consent === "off") return;
1395
+ const size = deps.screen?.();
1396
+ if (!size || !Number.isFinite(x) || !Number.isFinite(y)) return;
1397
+ 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 };
1398
+ record("$click", props);
1399
+ recorder?.mark({ kind: "click", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
1400
+ if (rage.push(x, y, now())) {
1401
+ record("$rageclick", props);
1402
+ recorder?.mark({ kind: "rage", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
1403
+ recorder?.trigger("rage");
1404
+ }
1405
+ },
1406
+ setReplay: (on) => {
1407
+ replayOn = on;
1408
+ if (!on) recorder?.drop();
1409
+ recorder?.sync(appState === "active");
1410
+ },
1111
1411
  screen: (name, props) => {
1112
1412
  if (typeof name !== "string" || !name.trim()) return;
1113
1413
  const path = (name.startsWith("/") ? name : `/${name}`).slice(0, 300);
@@ -1116,6 +1416,7 @@ var createClient = (options, deps) => {
1116
1416
  currentScreen = path;
1117
1417
  crumb("navigation", path);
1118
1418
  record("$screen", { ...props, path, screen: name, ...previous ? { previous } : {} });
1419
+ recorder?.mark({ kind: "pageview", path });
1119
1420
  },
1120
1421
  identify: (id, props) => {
1121
1422
  if (typeof id !== "string" || !id) return;
@@ -1163,6 +1464,7 @@ var createClient = (options, deps) => {
1163
1464
  const before = consent;
1164
1465
  consent = next;
1165
1466
  if (next === "off") {
1467
+ recorder?.drop();
1166
1468
  queue = [];
1167
1469
  session = null;
1168
1470
  errorQueue.length = 0;
@@ -1171,10 +1473,12 @@ var createClient = (options, deps) => {
1171
1473
  installId = null;
1172
1474
  void remove(KEYS.id);
1173
1475
  void remove(KEYS.referral);
1476
+ void remove(KEYS.surveys);
1174
1477
  } else {
1175
1478
  installId = identified ?? newDistinctId();
1176
1479
  void set(KEYS.id, installId);
1177
1480
  if (referral) void set(KEYS.referral, JSON.stringify(referral));
1481
+ if (surveysDone.size) void set(KEYS.surveys, JSON.stringify([...surveysDone]));
1178
1482
  }
1179
1483
  if (before === "off" && currentScreen && loaded) record("$screen", { path: currentScreen, screen: currentScreen });
1180
1484
  changed();
@@ -1217,6 +1521,22 @@ var createClient = (options, deps) => {
1217
1521
  isReady: () => isReady,
1218
1522
  getDistinctId: distinctId,
1219
1523
  getConsent: () => consent,
1524
+ getUser: () => {
1525
+ if (!identified) return void 0;
1526
+ const p = person;
1527
+ return { id: identified, ...typeof p?.name === "string" ? { name: p.name } : {}, ...typeof p?.email === "string" ? { email: p.email } : {} };
1528
+ },
1529
+ api: (as) => {
1530
+ if (!apiKey || consent === "off") return null;
1531
+ const known = client.getUser();
1532
+ const user = as?.name || as?.email ? { ...known, ...as.name ? { name: as.name } : {}, ...as.email ? { email: as.email } : {} } : known;
1533
+ return createApi({ host, apiKey, distinctId: distinctId(), user });
1534
+ },
1535
+ isSurveyDone: (id) => surveysDone.has(id),
1536
+ markSurveyDone: (id) => {
1537
+ surveysDone.add(id);
1538
+ if (consent === "persistent") void set(KEYS.surveys, JSON.stringify([...surveysDone].slice(-100)));
1539
+ },
1220
1540
  getConfig: () => config,
1221
1541
  subscribe: (listener) => {
1222
1542
  listeners.add(listener);
@@ -1226,6 +1546,7 @@ var createClient = (options, deps) => {
1226
1546
  close: async () => {
1227
1547
  if (closed) return;
1228
1548
  stopTimer();
1549
+ recorder?.sync(false);
1229
1550
  appStateSub?.remove();
1230
1551
  pushOff?.();
1231
1552
  pushOff = null;
@@ -1521,6 +1842,12 @@ try {
1521
1842
  } catch {
1522
1843
  expoLocalization = null;
1523
1844
  }
1845
+ var viewShot = null;
1846
+ try {
1847
+ viewShot = require("react-native-view-shot");
1848
+ } catch {
1849
+ viewShot = null;
1850
+ }
1524
1851
  var expoNotifications = null;
1525
1852
  try {
1526
1853
  expoNotifications = require("expo-notifications");
@@ -1594,6 +1921,30 @@ var readContext = () => {
1594
1921
  for (const k of Object.keys(ctx)) if (ctx[k] === void 0) delete ctx[k];
1595
1922
  return ctx;
1596
1923
  };
1924
+ var recorderDeps = () => {
1925
+ const capture = viewShot?.captureScreen;
1926
+ if (!capture) return null;
1927
+ let keyboard = false;
1928
+ try {
1929
+ RN2.Keyboard.addListener("keyboardWillShow", () => keyboard = true);
1930
+ RN2.Keyboard.addListener("keyboardDidShow", () => keyboard = true);
1931
+ RN2.Keyboard.addListener("keyboardDidHide", () => keyboard = false);
1932
+ } catch {
1933
+ return null;
1934
+ }
1935
+ return {
1936
+ capture: async (width) => {
1937
+ const s = RN2.Dimensions.get("screen");
1938
+ if (!s?.width) return null;
1939
+ const w = Math.round(width);
1940
+ const h3 = Math.round(s.height / s.width * width);
1941
+ const img = await capture({ format: "jpg", quality: 0.5, result: "base64", width: w, height: h3 });
1942
+ return img ? { img: img.replace(/^data:image\/\w+;base64,/, ""), w, h: h3 } : null;
1943
+ },
1944
+ keyboardOpen: () => keyboard,
1945
+ masks: measureMasks
1946
+ };
1947
+ };
1597
1948
  var nativeDeps = (overrides = {}) => ({
1598
1949
  storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
1599
1950
  fetch: ((url, init) => fetch(url, init)),
@@ -1607,17 +1958,959 @@ var nativeDeps = (overrides = {}) => ({
1607
1958
  },
1608
1959
  initialUrl: () => RN2.Linking.getInitialURL(),
1609
1960
  openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
1610
- push: expoPush()
1961
+ push: expoPush(),
1962
+ recorder: recorderDeps()
1611
1963
  });
1612
1964
  var optionalModules = () => ({
1613
1965
  asyncStorage: !!asyncStorage(),
1614
1966
  expoDevice: !!expoDevice,
1615
1967
  expoApplication: !!expoApplication,
1616
1968
  expoLocalization: !!expoLocalization,
1617
- expoNotifications: !!expoNotifications
1969
+ expoNotifications: !!expoNotifications,
1970
+ viewShot: !!viewShot?.captureScreen
1618
1971
  });
1619
1972
 
1973
+ // src/native/widgets-view.ts
1974
+ var import_react2 = require("react");
1975
+ var RN3 = require("react-native");
1976
+ var usePalette = (appearance = {}) => {
1977
+ const scheme = RN3.useColorScheme?.();
1978
+ const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
1979
+ return { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
1980
+ };
1981
+ var kit = (p) => {
1982
+ const text2 = (children, style = {}) => (0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
1983
+ const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
1984
+ const title = (children) => (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700", flex: 1 } }, children);
1985
+ const btn = (label, kind, onPress, disabled = false, style = {}) => (0, import_react2.createElement)(
1986
+ RN3.Pressable,
1987
+ {
1988
+ key: label,
1989
+ onPress,
1990
+ disabled,
1991
+ accessibilityRole: "button",
1992
+ accessibilityState: { disabled },
1993
+ style: {
1994
+ height: 44,
1995
+ paddingHorizontal: 16,
1996
+ borderRadius: 10,
1997
+ alignItems: "center",
1998
+ justifyContent: "center",
1999
+ opacity: disabled ? 0.55 : 1,
2000
+ backgroundColor: kind === "primary" ? p.accent : p.bg,
2001
+ borderWidth: kind === "secondary" ? 1 : 0,
2002
+ borderColor: p.line,
2003
+ ...style
2004
+ }
2005
+ },
2006
+ text2(label, { color: kind === "primary" ? "#ffffff" : p.fg, fontWeight: "600" })
2007
+ );
2008
+ 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 }));
2009
+ const field = (o) => (0, import_react2.createElement)(RN3.TextInput, {
2010
+ value: o.value,
2011
+ onChangeText: o.onChange,
2012
+ placeholder: o.placeholder,
2013
+ placeholderTextColor: p.muted,
2014
+ multiline: !!o.multiline,
2015
+ maxLength: o.max ?? 2e3,
2016
+ accessibilityLabel: o.placeholder,
2017
+ ...o.email ? { keyboardType: "email-address", autoCapitalize: "none", autoComplete: "email", textContentType: "emailAddress" } : {},
2018
+ ...o.onSubmit ? { onSubmitEditing: o.onSubmit, returnKeyType: "send" } : {},
2019
+ style: {
2020
+ minHeight: o.multiline ? 72 : 44,
2021
+ borderWidth: 1,
2022
+ borderColor: p.line,
2023
+ borderRadius: 10,
2024
+ paddingHorizontal: 12,
2025
+ paddingVertical: 10,
2026
+ color: p.fg,
2027
+ backgroundColor: p.bg,
2028
+ textAlignVertical: o.multiline ? "top" : "center",
2029
+ ...o.style
2030
+ }
2031
+ });
2032
+ const chip = (label, on, onPress, key = label) => (0, import_react2.createElement)(
2033
+ RN3.Pressable,
2034
+ {
2035
+ key,
2036
+ onPress,
2037
+ accessibilityRole: "button",
2038
+ accessibilityState: { selected: on },
2039
+ style: { paddingHorizontal: 12, height: 32, borderRadius: 16, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
2040
+ },
2041
+ text2(label, { fontSize: 13, fontWeight: "600", color: on ? p.accent : p.fg })
2042
+ );
2043
+ const page = (heading, onClose, children) => (0, import_react2.createElement)(
2044
+ RN3.Modal,
2045
+ { visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: onClose },
2046
+ (0, import_react2.createElement)(
2047
+ RN3.KeyboardAvoidingView,
2048
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
2049
+ (0, import_react2.createElement)(
2050
+ RN3.View,
2051
+ { style: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: p.line } },
2052
+ title(heading),
2053
+ closeX("Close", onClose)
2054
+ ),
2055
+ ...children
2056
+ )
2057
+ );
2058
+ return { text: text2, muted, title, btn, closeX, field, chip, page };
2059
+ };
2060
+ var timeAgo = (iso) => {
2061
+ const t = new Date(iso).getTime();
2062
+ if (Number.isNaN(t)) return "";
2063
+ const d = Math.max(0, Date.now() - t) / 1e3;
2064
+ if (d < 60) return "just now";
2065
+ if (d < 3600) return `${Math.floor(d / 60)}m ago`;
2066
+ if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
2067
+ return `${Math.floor(d / 86400)}d ago`;
2068
+ };
2069
+ var opener = () => {
2070
+ let open = false;
2071
+ const listeners = /* @__PURE__ */ new Set();
2072
+ const set = (v) => {
2073
+ open = v;
2074
+ listeners.forEach((l) => l(v));
2075
+ };
2076
+ const useOpen = () => {
2077
+ const [v, setV] = (0, import_react2.useState)(open);
2078
+ (0, import_react2.useEffect)(() => {
2079
+ listeners.add(setV);
2080
+ setV(open);
2081
+ return () => {
2082
+ listeners.delete(setV);
2083
+ };
2084
+ }, []);
2085
+ return v;
2086
+ };
2087
+ return { set, get: () => open, useOpen };
2088
+ };
2089
+ var isAnswered = (a) => Array.isArray(a) ? a.length > 0 : typeof a === "number" ? true : !!a?.trim();
2090
+ var SurveySheet = ({ survey, api, p, onDone }) => {
2091
+ const k = kit(p);
2092
+ const [step, setStep] = (0, import_react2.useState)(0);
2093
+ const [answers, setAnswers] = (0, import_react2.useState)({});
2094
+ const [done, setDone] = (0, import_react2.useState)(false);
2095
+ const [error, setError] = (0, import_react2.useState)("");
2096
+ const started = (0, import_react2.useRef)(Date.now());
2097
+ const finished = (0, import_react2.useRef)(false);
2098
+ const q = survey.questions[step];
2099
+ const last = step === survey.questions.length - 1;
2100
+ const value = answers[q.key];
2101
+ (0, import_react2.useEffect)(() => {
2102
+ api.seen(survey.id).catch(() => void 0);
2103
+ }, [api, survey.id]);
2104
+ (0, import_react2.useEffect)(() => {
2105
+ if (!done) return;
2106
+ const t = setTimeout(onDone, 2500);
2107
+ return () => clearTimeout(t);
2108
+ }, [done, onDone]);
2109
+ const finish = (completed, all = answers) => {
2110
+ if (finished.current) return;
2111
+ finished.current = true;
2112
+ api.respond(survey.id, { answers: all, completed, seconds: Math.round((Date.now() - started.current) / 1e3) }).catch(() => void 0);
2113
+ };
2114
+ const dismiss = () => {
2115
+ if (Object.values(answers).some(isAnswered)) finish(false);
2116
+ onDone();
2117
+ };
2118
+ const next = () => {
2119
+ if (q.required && !isAnswered(value)) return setError("Please answer this question.");
2120
+ setError("");
2121
+ if (!last) return setStep(step + 1);
2122
+ finish(true);
2123
+ setDone(true);
2124
+ };
2125
+ const pick = (v) => {
2126
+ setAnswers((a) => ({ ...a, [q.key]: v }));
2127
+ setError("");
2128
+ };
2129
+ let input;
2130
+ if (q.kind === "nps" || q.kind === "rating") {
2131
+ const scale = q.kind === "nps" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);
2132
+ const start = q.kind === "nps" ? 0 : 1;
2133
+ input = (0, import_react2.createElement)(
2134
+ RN3.View,
2135
+ { style: { gap: 6 } },
2136
+ (0, import_react2.createElement)(
2137
+ RN3.View,
2138
+ { style: { flexDirection: "row", flexWrap: "wrap", gap: 6 }, accessibilityRole: "radiogroup", accessibilityLabel: q.title },
2139
+ ...Array.from({ length: scale }, (_, i) => {
2140
+ const n = i + start;
2141
+ const on = value === n;
2142
+ return (0, import_react2.createElement)(
2143
+ RN3.Pressable,
2144
+ {
2145
+ key: n,
2146
+ onPress: () => pick(n),
2147
+ accessibilityRole: "radio",
2148
+ accessibilityState: { checked: on },
2149
+ accessibilityLabel: `${n} of ${scale - 1 + start}`,
2150
+ 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 }
2151
+ },
2152
+ k.text(String(n), { color: on ? "#ffffff" : p.fg, fontWeight: "600" })
2153
+ );
2154
+ })
2155
+ ),
2156
+ 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
2157
+ );
2158
+ } else if (q.kind === "single" || q.kind === "multi") {
2159
+ const multi = q.kind === "multi";
2160
+ const picked = multi ? value ?? [] : [];
2161
+ input = (0, import_react2.createElement)(
2162
+ RN3.View,
2163
+ { style: { gap: 6 } },
2164
+ ...(q.options ?? []).map((o) => {
2165
+ const on = multi ? picked.includes(o) : value === o;
2166
+ return (0, import_react2.createElement)(
2167
+ RN3.Pressable,
2168
+ {
2169
+ key: o,
2170
+ onPress: () => pick(multi ? on ? picked.filter((x) => x !== o) : [...picked, o] : o),
2171
+ accessibilityRole: multi ? "checkbox" : "radio",
2172
+ accessibilityState: { checked: on },
2173
+ style: { minHeight: 44, paddingHorizontal: 12, borderRadius: 10, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
2174
+ },
2175
+ k.text(o, { fontWeight: "500" })
2176
+ );
2177
+ })
2178
+ );
2179
+ } else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? "Type your answer", multiline: true });
2180
+ const body = done ? [k.text(survey.thanks || "Thanks for your feedback.", { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
2181
+ (0, import_react2.createElement)(
2182
+ RN3.View,
2183
+ { key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
2184
+ ...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 } })),
2185
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
2186
+ k.closeX("Close survey", dismiss)
2187
+ ),
2188
+ (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")),
2189
+ q.description ? k.muted(q.description) : null,
2190
+ input,
2191
+ error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2192
+ k.btn(last ? "Send" : "Next", "primary", next)
2193
+ ];
2194
+ return (0, import_react2.createElement)(
2195
+ RN3.Modal,
2196
+ { visible: true, transparent: true, animationType: "slide", onRequestClose: dismiss },
2197
+ (0, import_react2.createElement)(
2198
+ RN3.KeyboardAvoidingView,
2199
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: "rgba(0,0,0,0.35)", justifyContent: "flex-end" } },
2200
+ (0, import_react2.createElement)(
2201
+ RN3.View,
2202
+ {
2203
+ accessibilityViewIsModal: true,
2204
+ accessibilityLabel: survey.name,
2205
+ style: { backgroundColor: p.bg, borderTopLeftRadius: 16, borderTopRightRadius: 16, padding: 20, paddingBottom: 34, gap: 12 }
2206
+ },
2207
+ ...body
2208
+ )
2209
+ )
2210
+ );
2211
+ };
2212
+ var MRRocketSurveys = ({ client, appearance }) => {
2213
+ const p = usePalette(appearance);
2214
+ const [survey, setSurvey] = (0, import_react2.useState)(null);
2215
+ const [who, setWho] = (0, import_react2.useState)(() => client.getDistinctId());
2216
+ (0, import_react2.useEffect)(() => client.subscribe(() => setWho(client.getDistinctId())), [client]);
2217
+ (0, import_react2.useEffect)(() => {
2218
+ let stop = false;
2219
+ const ctrl = new AbortController();
2220
+ const timer = setTimeout(() => {
2221
+ void client.ready.then(() => {
2222
+ const api = client.api();
2223
+ if (stop || !api) return;
2224
+ api.survey("", ctrl.signal).then((r) => {
2225
+ const s = r.survey;
2226
+ if (stop || !s || !s.questions.length || client.isSurveyDone(s.id)) return;
2227
+ setSurvey({ s, api });
2228
+ }).catch(() => void 0);
2229
+ });
2230
+ }, 3e3);
2231
+ return () => {
2232
+ stop = true;
2233
+ clearTimeout(timer);
2234
+ ctrl.abort();
2235
+ };
2236
+ }, [client, who]);
2237
+ const done = (0, import_react2.useCallback)(() => {
2238
+ setSurvey((cur) => {
2239
+ if (cur) client.markSurveyDone(cur.s.id);
2240
+ return null;
2241
+ });
2242
+ }, [client]);
2243
+ if (!survey) return null;
2244
+ return (0, import_react2.createElement)(SurveySheet, { key: survey.s.id, survey: survey.s, api: survey.api, p, onDone: done });
2245
+ };
2246
+ var feedbackOpen = opener();
2247
+ var useFeedback = () => ({ open: () => feedbackOpen.set(true), close: () => feedbackOpen.set(false) });
2248
+ var FeedbackSheet = ({ client, p, title }) => {
2249
+ const k = kit(p);
2250
+ const [tab, setTab] = (0, import_react2.useState)("top");
2251
+ const [posts, setPosts] = (0, import_react2.useState)(null);
2252
+ const [boards, setBoards] = (0, import_react2.useState)([]);
2253
+ const [error, setError] = (0, import_react2.useState)("");
2254
+ const [board, setBoard] = (0, import_react2.useState)("feature");
2255
+ const [subject, setSubject] = (0, import_react2.useState)("");
2256
+ const [details, setDetails] = (0, import_react2.useState)("");
2257
+ const [name, setName] = (0, import_react2.useState)("");
2258
+ const [email, setEmail] = (0, import_react2.useState)("");
2259
+ const [sending, setSending] = (0, import_react2.useState)(false);
2260
+ const [sent, setSent] = (0, import_react2.useState)(false);
2261
+ const close = () => feedbackOpen.set(false);
2262
+ const user = client.getUser();
2263
+ (0, import_react2.useEffect)(() => {
2264
+ const api = client.api();
2265
+ if (!api) return setError("Feedback is off while tracking is off.");
2266
+ const ctrl = new AbortController();
2267
+ api.list({ sort: "top" }, ctrl.signal).then((l) => {
2268
+ setPosts(l.posts);
2269
+ setBoards(l.boards);
2270
+ if (l.boards.length && !l.boards.some((b) => b.id === "feature")) setBoard(l.boards[0].id);
2271
+ }).catch((e) => e.name !== "AbortError" && setError(e.message));
2272
+ return () => ctrl.abort();
2273
+ }, [client]);
2274
+ const vote = (post) => {
2275
+ const api = client.api();
2276
+ if (!api) return;
2277
+ setPosts((all) => all?.map((x) => x.id === post.id ? { ...x, voted: !x.voted, votes: x.votes + (x.voted ? -1 : 1) } : x) ?? null);
2278
+ 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) => {
2279
+ setError(e.message);
2280
+ setPosts((all) => all?.map((x) => x.id === post.id ? post : x) ?? null);
2281
+ });
2282
+ };
2283
+ const submit = () => {
2284
+ const api = client.api({ name: name.trim(), email: email.trim() });
2285
+ if (!api || subject.trim().length < 3 || sending) return;
2286
+ setSending(true);
2287
+ setError("");
2288
+ api.submit({ title: subject.trim(), body: details.trim() || void 0, board }).then((post) => {
2289
+ setPosts((all) => [post, ...all ?? []]);
2290
+ setSubject("");
2291
+ setDetails("");
2292
+ setSent(true);
2293
+ }).catch((e) => setError(e.message)).finally(() => setSending(false));
2294
+ };
2295
+ const tabs = (0, import_react2.createElement)(
2296
+ RN3.View,
2297
+ { style: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingTop: 12 } },
2298
+ k.chip("Top requests", tab === "top", () => setTab("top")),
2299
+ k.chip("Suggest", tab === "new", () => {
2300
+ setTab("new");
2301
+ setSent(false);
2302
+ })
2303
+ );
2304
+ const list = (0, import_react2.createElement)(
2305
+ RN3.ScrollView,
2306
+ { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
2307
+ error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2308
+ posts === null && !error ? k.muted("Loading\u2026") : null,
2309
+ posts?.length === 0 ? k.muted("No requests yet. Be the first to suggest one.") : null,
2310
+ ...(posts ?? []).map(
2311
+ (post) => (0, import_react2.createElement)(
2312
+ RN3.View,
2313
+ { key: post.id, style: { flexDirection: "row", gap: 12, alignItems: "flex-start", paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: p.line } },
2314
+ (0, import_react2.createElement)(
2315
+ RN3.Pressable,
2316
+ {
2317
+ onPress: () => vote(post),
2318
+ accessibilityRole: "button",
2319
+ accessibilityState: { selected: post.voted },
2320
+ accessibilityLabel: `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
2321
+ style: { width: 44, minHeight: 48, borderRadius: 10, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: post.voted ? p.accent : p.line }
2322
+ },
2323
+ k.text("\u25B2", { fontSize: 11, color: post.voted ? p.accent : p.muted }),
2324
+ k.text(String(post.votes), { fontWeight: "700", fontSize: 14, color: post.voted ? p.accent : p.fg })
2325
+ ),
2326
+ (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)
2327
+ )
2328
+ )
2329
+ );
2330
+ const form = (0, import_react2.createElement)(
2331
+ RN3.ScrollView,
2332
+ { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 }, keyboardShouldPersistTaps: "handled" },
2333
+ sent ? k.text("Thanks! Your request is on the board.", { fontWeight: "600" }) : null,
2334
+ 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,
2335
+ k.field({ value: subject, onChange: setSubject, placeholder: "Short title", max: 120 }),
2336
+ k.field({ value: details, onChange: setDetails, placeholder: "Details (optional)", multiline: true }),
2337
+ ...user?.email ? [] : [
2338
+ k.muted("Your name and email are optional; they let the team follow up."),
2339
+ k.field({ value: name, onChange: setName, placeholder: "Your name (optional)", max: 100 }),
2340
+ k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional)", max: 200, email: true })
2341
+ ],
2342
+ error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2343
+ k.btn(sending ? "Sending\u2026" : "Send request", "primary", submit, subject.trim().length < 3 || sending)
2344
+ );
2345
+ return k.page(title, close, [tabs, tab === "top" ? list : form]);
2346
+ };
2347
+ var MRRocketFeedback = ({ client, appearance, options = {} }) => {
2348
+ const p = usePalette(appearance);
2349
+ const open = feedbackOpen.useOpen();
2350
+ return open ? (0, import_react2.createElement)(FeedbackSheet, { client, p, title: options.title ?? "Feedback" }) : null;
2351
+ };
2352
+ var POLL_OPEN_MS = 4e3;
2353
+ var POLL_WAITING_MS = 8e3;
2354
+ var POLL_IDLE_MS = 3e4;
2355
+ var WAITING_FOR_MS = 10 * 6e4;
2356
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
2357
+ var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
2358
+ var msgView = "home";
2359
+ var messengerOpen = opener();
2360
+ var openAt = (v = "home") => {
2361
+ msgView = v;
2362
+ messengerOpen.set(true);
2363
+ };
2364
+ var useMessenger = () => ({
2365
+ open: (view) => openAt(view),
2366
+ close: () => messengerOpen.set(false),
2367
+ toggle: () => messengerOpen.get() ? messengerOpen.set(false) : openAt()
2368
+ });
2369
+ var haptics = null;
2370
+ try {
2371
+ haptics = require("expo-haptics");
2372
+ } catch {
2373
+ haptics = null;
2374
+ }
2375
+ var buzz = () => {
2376
+ try {
2377
+ void haptics?.notificationAsync?.(haptics.NotificationFeedbackType?.Success)?.catch(() => void 0);
2378
+ } catch {
2379
+ }
2380
+ };
2381
+ var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
2382
+ var svg = null;
2383
+ try {
2384
+ svg = require("react-native-svg");
2385
+ } catch {
2386
+ svg = null;
2387
+ }
2388
+ var GLYPH = {
2389
+ chat: "M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z",
2390
+ home: "M2.5 7 8 2.5 13.5 7v6a1 1 0 0 1-1 1h-3v-4h-3v4h-3a1 1 0 0 1-1-1z",
2391
+ help: "M8 14.5A6.5 6.5 0 1 0 8 1.5a6.5 6.5 0 0 0 0 13zM6.2 6.2a1.9 1.9 0 1 1 2.6 1.8c-.5.2-.8.6-.8 1.1v.4M8 11.6v.1",
2392
+ send: "M2.5 8h10M8.5 3.5 13 8l-4.5 4.5",
2393
+ up: "M8 13V3M3.5 7.5 8 3l4.5 4.5",
2394
+ next: "M6 3.5 10.5 8 6 12.5",
2395
+ search: "M7 11.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9zM10.5 10.5l3 3",
2396
+ cross: "m4 4 8 8M12 4l-8 8"
2397
+ };
2398
+ var FALLBACK = { chat: "\u{1F4AC}", home: "\u2302", help: "?", send: "\u2192", up: "\u2191", next: "\u203A", search: "\u2315", cross: "\xD7" };
2399
+ var glyph = (name, color, size = 20) => {
2400
+ const Svg = svg?.Svg ?? svg?.default;
2401
+ if (!svg || !Svg) return (0, import_react2.createElement)(RN3.Text, { style: { color, fontSize: Math.round(size * 0.85), lineHeight: size + 2, textAlign: "center", minWidth: size } }, FALLBACK[name]);
2402
+ return (0, import_react2.createElement)(Svg, { width: size, height: size, viewBox: "0 0 16 16", fill: "none" }, (0, import_react2.createElement)(svg.Path, { d: GLYPH[name], stroke: color, strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }));
2403
+ };
2404
+ var alpha = (hex, a) => /^#[0-9a-f]{6}$/i.test(hex) ? `${hex}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex;
2405
+ var teamMark = (title, bg, fg, size = 36) => (0, import_react2.createElement)(
2406
+ RN3.View,
2407
+ { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: bg, alignItems: "center", justifyContent: "center" } },
2408
+ (0, import_react2.createElement)(RN3.Text, { style: { color: fg, fontSize: Math.round(size * 0.42), fontWeight: "700" } }, (title.trim()[0] ?? "?").toUpperCase())
2409
+ );
2410
+ var lift = { shadowColor: "#101218", shadowOpacity: 0.07, shadowRadius: 14, shadowOffset: { width: 0, height: 4 }, elevation: 2 };
2411
+ var useChat = (client, o, open, view) => {
2412
+ const [thread, setThread] = (0, import_react2.useState)(null);
2413
+ const [loaded, setLoaded] = (0, import_react2.useState)(false);
2414
+ const [error, setError] = (0, import_react2.useState)("");
2415
+ const [unseen, setUnseen] = (0, import_react2.useState)([]);
2416
+ const known = (0, import_react2.useRef)(null);
2417
+ const watching = open && view === "messages";
2418
+ const watch = (0, import_react2.useRef)(watching);
2419
+ watch.current = watching;
2420
+ const [active, setActive] = (0, import_react2.useState)(RN3.AppState.currentState === "active");
2421
+ (0, import_react2.useEffect)(() => {
2422
+ const sub = RN3.AppState.addEventListener("change", (st) => setActive(st === "active"));
2423
+ return () => sub.remove();
2424
+ }, []);
2425
+ const take = (0, import_react2.useCallback)(
2426
+ (t) => {
2427
+ const team = teamOf(t);
2428
+ if (known.current !== null) {
2429
+ const i = team.findIndex((m) => m.id === known.current);
2430
+ const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
2431
+ if (fresh.length) {
2432
+ if (o.haptics !== false) buzz();
2433
+ if (!watch.current) setUnseen((u) => [...u, ...fresh]);
2434
+ }
2435
+ }
2436
+ known.current = team.at(-1)?.id ?? "";
2437
+ setThread(t);
2438
+ },
2439
+ [o.haptics]
2440
+ );
2441
+ (0, import_react2.useEffect)(() => {
2442
+ if (watching) setUnseen([]);
2443
+ }, [watching]);
2444
+ (0, import_react2.useEffect)(() => {
2445
+ if (!active) return;
2446
+ let stop = false;
2447
+ let timer;
2448
+ let ctrl = null;
2449
+ const tick = async () => {
2450
+ const api = client.api();
2451
+ if (!api) {
2452
+ setLoaded(true);
2453
+ return;
2454
+ }
2455
+ ctrl = new AbortController();
2456
+ let t = null;
2457
+ try {
2458
+ await client.ready;
2459
+ t = await api.chat(ctrl.signal);
2460
+ if (stop) return;
2461
+ take(t);
2462
+ setError("");
2463
+ } catch (e) {
2464
+ if (e.name === "AbortError" || stop) return;
2465
+ if (watch.current) setError(e.message);
2466
+ }
2467
+ setLoaded(true);
2468
+ const waiting = !!t?.messages.some((m) => m.from === "visitor") && t?.conversation?.status !== "solved";
2469
+ const wrote = (t?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
2470
+ const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
2471
+ const ms = watch.current ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : waiting || open ? POLL_IDLE_MS : 0;
2472
+ if (!stop && ms) timer = setTimeout(tick, ms);
2473
+ };
2474
+ void tick();
2475
+ return () => {
2476
+ stop = true;
2477
+ ctrl?.abort();
2478
+ clearTimeout(timer);
2479
+ };
2480
+ }, [client, take, active, open, watching]);
2481
+ return { thread, loaded, error, setError, unseen, take };
2482
+ };
2483
+ var useArticles = (client, on, query) => {
2484
+ const [list, setList] = (0, import_react2.useState)(null);
2485
+ const [any, setAny] = (0, import_react2.useState)(null);
2486
+ (0, import_react2.useEffect)(() => {
2487
+ if (!on) return;
2488
+ let stop = false;
2489
+ const t = setTimeout(
2490
+ () => void client.ready.then(
2491
+ () => client.api()?.articles(query.trim() || void 0).then((r) => {
2492
+ if (stop) return;
2493
+ setList(r.articles);
2494
+ if (!query.trim()) setAny(r.articles.length > 0);
2495
+ }).catch(() => !stop && setList((l) => l ?? []))
2496
+ ),
2497
+ query ? 250 : 0
2498
+ );
2499
+ return () => {
2500
+ stop = true;
2501
+ clearTimeout(t);
2502
+ };
2503
+ }, [client, on, query]);
2504
+ return { list, any };
2505
+ };
2506
+ var spansOf = (p, spans, base) => spans.map(
2507
+ (s, i) => (0, import_react2.createElement)(
2508
+ RN3.Text,
2509
+ {
2510
+ key: i,
2511
+ onPress: s.href ? () => void RN3.Linking.openURL(s.href).catch(() => void 0) : void 0,
2512
+ accessibilityRole: s.href ? "link" : void 0,
2513
+ style: {
2514
+ ...base,
2515
+ ...s.bold ? { fontWeight: "700" } : {},
2516
+ ...s.italic ? { fontStyle: "italic" } : {},
2517
+ ...s.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
2518
+ ...s.href ? { color: p.accent, textDecorationLine: "underline" } : {}
2519
+ }
2520
+ },
2521
+ s.text
2522
+ )
2523
+ );
2524
+ var ArticleView = ({ client, p, id, onAsk }) => {
2525
+ const k = kit(p);
2526
+ const [a, setA] = (0, import_react2.useState)(null);
2527
+ const [error, setError] = (0, import_react2.useState)("");
2528
+ const [rated, setRated] = (0, import_react2.useState)(false);
2529
+ (0, import_react2.useEffect)(() => {
2530
+ const ctrl = new AbortController();
2531
+ client.api()?.article(id, ctrl.signal).then(setA).catch((e) => e.name !== "AbortError" && setError(e.message));
2532
+ return () => ctrl.abort();
2533
+ }, [client, id]);
2534
+ if (error) return k.text(error, { color: p.danger });
2535
+ if (!a) return k.muted("Loading\u2026");
2536
+ const base = { color: p.fg, fontSize: 15, lineHeight: 22 };
2537
+ const blocks = a.blocks.map((b, i) => {
2538
+ 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, {}));
2539
+ if (b.type === "p") return (0, import_react2.createElement)(RN3.Text, { key: i, style: base }, ...spansOf(p, b.spans, {}));
2540
+ 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, {})));
2541
+ if (b.type === "ul" || b.type === "ol")
2542
+ return (0, import_react2.createElement)(
2543
+ RN3.View,
2544
+ { key: i, style: { gap: 4 } },
2545
+ ...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, {}))))
2546
+ );
2547
+ 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 }));
2548
+ return (0, import_react2.createElement)(RN3.View, { key: i, style: { height: 1, backgroundColor: p.line } });
2549
+ });
2550
+ const rate = (helpful) => {
2551
+ setRated(true);
2552
+ client.api()?.rateArticle(id, helpful).catch(() => void 0);
2553
+ if (!helpful) onAsk();
2554
+ };
2555
+ return (0, import_react2.createElement)(
2556
+ RN3.View,
2557
+ { style: { gap: 10 } },
2558
+ k.muted(a.collection),
2559
+ (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 20, fontWeight: "700" } }, a.title),
2560
+ ...blocks,
2561
+ (0, import_react2.createElement)(
2562
+ RN3.View,
2563
+ { style: { flexDirection: "row", alignItems: "center", gap: 8, padding: 12, borderRadius: 10, backgroundColor: p.soft, marginTop: 8 } },
2564
+ rated ? k.muted("Thanks for letting us know.") : k.muted("Did this answer your question?", { flex: 1 }),
2565
+ rated ? null : k.btn("Yes", "secondary", () => rate(true), false, { height: 34 }),
2566
+ rated ? null : k.btn("No", "secondary", () => rate(false), false, { height: 34 })
2567
+ ),
2568
+ k.btn("Still need help? Send us a message", "secondary", onAsk)
2569
+ );
2570
+ };
2571
+ var MessengerSheet = ({
2572
+ client,
2573
+ p,
2574
+ o,
2575
+ chat,
2576
+ view,
2577
+ setView
2578
+ }) => {
2579
+ const k = kit(p);
2580
+ const [body, setBody] = (0, import_react2.useState)("");
2581
+ const [email, setEmail] = (0, import_react2.useState)("");
2582
+ const [pending, setPending] = (0, import_react2.useState)(null);
2583
+ const [query, setQuery] = (0, import_react2.useState)("");
2584
+ const helpOn = o.help !== false;
2585
+ const arts = useArticles(client, helpOn, query);
2586
+ const scroll = (0, import_react2.useRef)(null);
2587
+ const close = () => messengerOpen.set(false);
2588
+ const greeting = o.greeting ?? "Hi! How can we help?";
2589
+ const { thread } = chat;
2590
+ const count = (thread?.messages.length ?? 0) + (pending ? 1 : 0);
2591
+ (0, import_react2.useEffect)(() => {
2592
+ if (view === "messages") setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 50);
2593
+ }, [count, view]);
2594
+ const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
2595
+ const emailOk = !email.trim() || EMAIL.test(email.trim());
2596
+ const send = async () => {
2597
+ const text2 = body.trim();
2598
+ const api = client.api();
2599
+ if (!text2 || pending || !emailOk || !api) return;
2600
+ setPending({ id: "pending", from: "visitor", body: text2, at: (/* @__PURE__ */ new Date()).toISOString() });
2601
+ setBody("");
2602
+ chat.setError("");
2603
+ try {
2604
+ chat.take(await api.sendChat({ body: text2, email: needEmail && email.trim() ? email.trim() : void 0 }));
2605
+ } catch (e) {
2606
+ setBody(text2);
2607
+ chat.setError(e.message);
2608
+ } finally {
2609
+ setPending(null);
2610
+ }
2611
+ };
2612
+ const ink = inkOn(p.accent);
2613
+ const dark = p.bg === DARK.bg;
2614
+ const canvas = dark ? p.bg : p.soft;
2615
+ const raised = dark ? p.soft : p.bg;
2616
+ const title = o.title ?? "Chat with us";
2617
+ const reply = replyLine(o.replyTime);
2618
+ const bubble = (m, isPending = false) => {
2619
+ const mine = m.from === "visitor";
2620
+ return (0, import_react2.createElement)(
2621
+ RN3.View,
2622
+ { key: m.id, style: { alignSelf: mine ? "flex-end" : "flex-start", maxWidth: "82%", gap: 4 } },
2623
+ (0, import_react2.createElement)(
2624
+ RN3.View,
2625
+ {
2626
+ style: {
2627
+ backgroundColor: mine ? p.accent : dark ? p.soft : "#eeeff1",
2628
+ borderRadius: 18,
2629
+ borderBottomRightRadius: mine ? 6 : 18,
2630
+ borderBottomLeftRadius: mine ? 18 : 6,
2631
+ paddingHorizontal: 14,
2632
+ paddingVertical: 9,
2633
+ opacity: isPending ? 0.7 : 1
2634
+ }
2635
+ },
2636
+ k.text(m.body, { color: mine ? ink : p.fg })
2637
+ ),
2638
+ ...(m.articles ?? []).map(
2639
+ (a) => (0, import_react2.createElement)(
2640
+ RN3.Pressable,
2641
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { borderWidth: 1, borderColor: p.line, borderRadius: 12, paddingHorizontal: 10, paddingVertical: 7 } },
2642
+ k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
2643
+ )
2644
+ ),
2645
+ m.at ? k.muted(`${mine ? "" : `${whoLabel[m.from]} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
2646
+ );
2647
+ };
2648
+ const row = (a) => (0, import_react2.createElement)(
2649
+ RN3.Pressable,
2650
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "button", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 11, paddingHorizontal: 8 } },
2651
+ k.text(a.title, { flex: 1 }),
2652
+ glyph("next", p.accent, 14)
2653
+ );
2654
+ const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: raised, borderRadius: 16, padding: 16, ...dark ? { borderWidth: 1, borderColor: p.line } : lift, ...style } }, ...children);
2655
+ const searchRow = (onPress) => (0, import_react2.createElement)(
2656
+ RN3.Pressable,
2657
+ { key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: 12, paddingHorizontal: 14, backgroundColor: canvas } },
2658
+ k.muted("Search for help", { flex: 1, fontSize: 15 }),
2659
+ glyph("search", p.accent, 16)
2660
+ );
2661
+ const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: 12, backgroundColor: raised, ...dark ? {} : lift } });
2662
+ const last = thread?.messages.at(-1);
2663
+ const showHelp = helpOn && arts.any !== false;
2664
+ let content;
2665
+ if (view === "home")
2666
+ content = (0, import_react2.createElement)(
2667
+ RN3.ScrollView,
2668
+ { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingBottom: 24 }, keyboardShouldPersistTaps: "handled" },
2669
+ (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, (0, import_react2.createElement)(RN3.Text, { style: { color: ink, fontSize: 28, fontWeight: "700", letterSpacing: -0.4, lineHeight: 34 } }, greeting)),
2670
+ (0, import_react2.createElement)(
2671
+ RN3.View,
2672
+ { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } },
2673
+ last ? (0, import_react2.createElement)(
2674
+ RN3.Pressable,
2675
+ { key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
2676
+ card([
2677
+ k.muted("Recent message", { fontSize: 12, fontWeight: "600", marginBottom: 10 }),
2678
+ (0, import_react2.createElement)(
2679
+ RN3.View,
2680
+ { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 12 } },
2681
+ teamMark(title, p.accent, ink),
2682
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 2 } }, (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { color: p.fg, fontSize: 15 } }, last.body), k.muted(`${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`, { fontSize: 12 })),
2683
+ chat.unseen.length ? (0, import_react2.createElement)(RN3.View, { style: { minWidth: 20, height: 20, borderRadius: 10, paddingHorizontal: 6, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(chat.unseen.length), { color: "#fff", fontSize: 12, fontWeight: "700" })) : glyph("next", p.muted, 14)
2684
+ )
2685
+ ])
2686
+ ) : null,
2687
+ (0, import_react2.createElement)(
2688
+ RN3.Pressable,
2689
+ { key: "send", onPress: () => setView("messages"), accessibilityRole: "button" },
2690
+ card(
2691
+ [
2692
+ (0, import_react2.createElement)(RN3.View, { key: "t", style: { flex: 1, gap: 2 } }, k.text("Send us a message", { fontWeight: "700", fontSize: 16 }), k.muted(reply)),
2693
+ (0, import_react2.createElement)(RN3.View, { key: "g", style: { width: 36, height: 36, borderRadius: 18, backgroundColor: alpha(p.accent, 0.12), alignItems: "center", justifyContent: "center" } }, glyph("send", p.accent, 18))
2694
+ ],
2695
+ { flexDirection: "row", alignItems: "center", gap: 12 }
2696
+ )
2697
+ ),
2698
+ showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
2699
+ )
2700
+ );
2701
+ else if (view === "messages")
2702
+ content = (0, import_react2.createElement)(
2703
+ RN3.View,
2704
+ { key: "messages", style: { flex: 1, backgroundColor: p.bg } },
2705
+ (0, import_react2.createElement)(
2706
+ RN3.ScrollView,
2707
+ { ref: scroll, style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 12 } },
2708
+ bubble({ id: "greeting", from: "team", body: greeting, at: "" }),
2709
+ !chat.loaded ? k.muted("Loading\u2026") : null,
2710
+ ...(thread?.messages ?? []).map((m) => bubble(m)),
2711
+ pending ? bubble(pending, true) : null,
2712
+ thread?.conversation?.status === "solved" && !pending ? k.muted("This conversation is closed. Write to start a new one.", { textAlign: "center" }) : null
2713
+ ),
2714
+ (0, import_react2.createElement)(
2715
+ RN3.View,
2716
+ { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 10, gap: 8 } },
2717
+ chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
2718
+ needEmail ? k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional), so we can follow up", email: true, max: 200, style: { borderWidth: emailOk ? 0 : 1, borderColor: p.danger, borderRadius: 12, backgroundColor: canvas } }) : null,
2719
+ (0, import_react2.createElement)(
2720
+ RN3.View,
2721
+ { style: { flexDirection: "row", alignItems: "flex-end", gap: 6, borderWidth: 1, borderColor: p.line, borderRadius: 22, paddingVertical: 5, paddingRight: 5, paddingLeft: 16, backgroundColor: p.bg } },
2722
+ k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { flex: 1, minHeight: 34, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 7, backgroundColor: "transparent" } }),
2723
+ (0, import_react2.createElement)(
2724
+ RN3.Pressable,
2725
+ {
2726
+ onPress: () => void send(),
2727
+ disabled: !body.trim() || !!pending || !emailOk,
2728
+ accessibilityRole: "button",
2729
+ accessibilityLabel: "Send",
2730
+ style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: !body.trim() || !!pending || !emailOk ? p.line : p.accent }
2731
+ },
2732
+ glyph("up", !body.trim() || !!pending || !emailOk ? p.muted : ink, 18)
2733
+ )
2734
+ )
2735
+ )
2736
+ );
2737
+ else if (view === "help") {
2738
+ const groups = /* @__PURE__ */ new Map();
2739
+ for (const a of arts.list ?? []) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
2740
+ content = (0, import_react2.createElement)(
2741
+ RN3.ScrollView,
2742
+ { key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
2743
+ search,
2744
+ arts.list === null ? k.muted("Loading\u2026") : null,
2745
+ 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,
2746
+ ...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row), { padding: 8 })] : [] : [...groups].map(([name, items]) => (0, import_react2.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`, { fontSize: 12, fontWeight: "600", paddingHorizontal: 2 }), card(items.map(row), { padding: 8 })))
2747
+ );
2748
+ } else
2749
+ content = (0, import_react2.createElement)(
2750
+ RN3.ScrollView,
2751
+ { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { padding: 16, paddingBottom: 32 } },
2752
+ (0, import_react2.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
2753
+ );
2754
+ const current = typeof view === "string" ? view : "help";
2755
+ const tab = (v, name, icon, badge = 0) => (0, import_react2.createElement)(
2756
+ RN3.Pressable,
2757
+ { key: v, onPress: () => setView(v), accessibilityRole: "tab", accessibilityState: { selected: current === v }, style: { flex: 1, alignItems: "center", paddingTop: 8, paddingBottom: 6, gap: 3 } },
2758
+ glyph(icon, current === v ? p.accent : p.muted, 22),
2759
+ k.text(name, { fontSize: 12, fontWeight: "600", color: current === v ? p.accent : p.muted }),
2760
+ badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 4, right: "28%", 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
2761
+ );
2762
+ const onHome = view === "home";
2763
+ const headFg = onHome ? ink : p.fg;
2764
+ const head = (0, import_react2.createElement)(
2765
+ RN3.View,
2766
+ { key: "head", style: { flexDirection: "row", alignItems: "center", gap: 10, paddingLeft: 16, paddingRight: 12, paddingTop: 16, paddingBottom: 12, backgroundColor: onHome ? p.accent : p.bg, borderBottomWidth: onHome ? 0 : 1, borderBottomColor: p.line } },
2767
+ typeof view !== "string" ? (0, import_react2.createElement)(RN3.Pressable, { key: "back", onPress: () => setView("help"), accessibilityRole: "button", hitSlop: 10 }, k.text("\u2039 All articles", { color: p.accent, fontWeight: "600" })) : view === "help" ? null : teamMark(title, onHome ? ink : p.accent, onHome ? p.accent : ink, onHome ? 28 : 36),
2768
+ (0, import_react2.createElement)(
2769
+ RN3.View,
2770
+ { style: { flex: 1, minWidth: 0 } },
2771
+ typeof view !== "string" ? null : (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", numberOfLines: 1, style: { color: headFg, fontSize: 17, fontWeight: "700" } }, view === "help" ? "Help" : title),
2772
+ view === "messages" ? k.muted(reply, { fontSize: 12 }) : null
2773
+ ),
2774
+ (0, import_react2.createElement)(RN3.Pressable, { onPress: close, accessibilityRole: "button", accessibilityLabel: "Close", hitSlop: 12, style: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" } }, glyph("cross", onHome ? ink : p.muted, 18))
2775
+ );
2776
+ return (0, import_react2.createElement)(
2777
+ RN3.Modal,
2778
+ { visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: close },
2779
+ (0, import_react2.createElement)(
2780
+ RN3.KeyboardAvoidingView,
2781
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
2782
+ head,
2783
+ content,
2784
+ (0, import_react2.createElement)(
2785
+ RN3.View,
2786
+ { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", borderTopWidth: 1, borderTopColor: p.line, paddingHorizontal: 8, paddingBottom: 22, backgroundColor: p.bg } },
2787
+ tab("home", "Home", "home"),
2788
+ tab("messages", "Messages", "chat", view === "messages" ? 0 : chat.unseen.length),
2789
+ showHelp ? tab("help", "Help", "help") : null
2790
+ )
2791
+ )
2792
+ );
2793
+ };
2794
+ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
2795
+ const [look, setLook] = (0, import_react2.useState)(null);
2796
+ (0, import_react2.useEffect)(() => {
2797
+ const ctrl = new AbortController();
2798
+ void client.ready.then(
2799
+ () => client.api()?.messengerLook(ctrl.signal).then(setLook).catch(() => void 0)
2800
+ );
2801
+ return () => ctrl.abort();
2802
+ }, [client]);
2803
+ const defined = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== void 0));
2804
+ const options = look ? {
2805
+ title: look.title,
2806
+ greeting: look.greeting,
2807
+ replyTime: look.replyTime,
2808
+ launcher: look.appLauncher ?? false,
2809
+ position: look.position,
2810
+ label: look.label,
2811
+ help: look.help,
2812
+ preview: look.preview,
2813
+ askEmail: look.askEmail,
2814
+ haptics: look.sound,
2815
+ ...defined
2816
+ } : given;
2817
+ const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
2818
+ const k = kit(p);
2819
+ const open = messengerOpen.useOpen();
2820
+ const [view, setViewState] = (0, import_react2.useState)(msgView);
2821
+ const [banner, setBanner] = (0, import_react2.useState)(null);
2822
+ (0, import_react2.useEffect)(() => {
2823
+ if (open) setViewState(msgView);
2824
+ }, [open]);
2825
+ const setView = (v) => {
2826
+ msgView = v;
2827
+ setViewState(v);
2828
+ };
2829
+ const chat = useChat(client, options, open, view);
2830
+ const latest = chat.unseen.at(-1);
2831
+ (0, import_react2.useEffect)(() => {
2832
+ if (!latest || open || options.preview === false) return;
2833
+ setBanner(latest);
2834
+ const t = setTimeout(() => setBanner(null), 6e3);
2835
+ return () => clearTimeout(t);
2836
+ }, [latest, open, options.preview]);
2837
+ (0, import_react2.useEffect)(() => {
2838
+ if (open) setBanner(null);
2839
+ }, [open]);
2840
+ const unseen = chat.unseen.length;
2841
+ const ink = inkOn(p.accent);
2842
+ const label = options.label?.trim() ?? "";
2843
+ const side = options.position === "bottom-left" ? { left: 20 } : { right: 20 };
2844
+ const launcher = options.launcher ? (0, import_react2.createElement)(
2845
+ RN3.Pressable,
2846
+ {
2847
+ key: "launcher",
2848
+ onPress: () => openAt(unseen ? "messages" : "home"),
2849
+ accessibilityRole: "button",
2850
+ accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
2851
+ style: {
2852
+ position: "absolute",
2853
+ ...side,
2854
+ bottom: 34,
2855
+ height: label ? 50 : 56,
2856
+ minWidth: label ? 0 : 56,
2857
+ paddingHorizontal: label ? 18 : 0,
2858
+ borderRadius: 28,
2859
+ flexDirection: "row",
2860
+ gap: 8,
2861
+ backgroundColor: p.accent,
2862
+ alignItems: "center",
2863
+ justifyContent: "center",
2864
+ shadowColor: p.accent,
2865
+ shadowOpacity: 0.4,
2866
+ shadowRadius: 12,
2867
+ shadowOffset: { width: 0, height: 6 },
2868
+ elevation: 6
2869
+ }
2870
+ },
2871
+ glyph("chat", ink, label ? 20 : 26),
2872
+ label ? k.text(label, { color: ink, fontWeight: "700" }) : null,
2873
+ 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
2874
+ ) : null;
2875
+ const toast = banner ? (0, import_react2.createElement)(
2876
+ RN3.Pressable,
2877
+ {
2878
+ key: "banner",
2879
+ onPress: () => openAt("messages"),
2880
+ accessibilityRole: "button",
2881
+ accessibilityLiveRegion: "polite",
2882
+ style: { position: "absolute", top: 54, left: 16, right: 16, flexDirection: "row", alignItems: "flex-start", gap: 12, backgroundColor: p.bg, borderRadius: 18, padding: 14, borderWidth: 1, borderColor: p.line, shadowColor: "#000", shadowOpacity: 0.16, shadowRadius: 18, shadowOffset: { width: 0, height: 8 }, elevation: 8 }
2883
+ },
2884
+ teamMark(options.title ?? "Chat with us", p.accent, ink),
2885
+ (0, import_react2.createElement)(
2886
+ RN3.View,
2887
+ { style: { flex: 1, gap: 2 } },
2888
+ k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
2889
+ (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15 } }, banner.body)
2890
+ )
2891
+ ) : null;
2892
+ if (!launcher && !toast && !open) return null;
2893
+ return (0, import_react2.createElement)(
2894
+ RN3.View,
2895
+ { pointerEvents: "box-none", style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 } },
2896
+ launcher,
2897
+ toast,
2898
+ open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView }) : null
2899
+ );
2900
+ };
2901
+
1620
2902
  // src/native/index.tsx
2903
+ var RN4 = require("react-native");
2904
+ var MRRocketMask = ({ children, style }) => {
2905
+ const ref = (0, import_react3.useRef)(null);
2906
+ (0, import_react3.useEffect)(() => {
2907
+ maskedViews.add(ref);
2908
+ return () => {
2909
+ maskedViews.delete(ref);
2910
+ };
2911
+ }, []);
2912
+ return (0, import_react3.createElement)(RN4.View, { ref, collapsable: false, style }, children);
2913
+ };
1621
2914
  var shared = null;
1622
2915
  var sharedKey = "";
1623
2916
  var createMRRocket = (options) => {
@@ -1633,20 +2926,40 @@ var createMRRocket = (options) => {
1633
2926
  return shared;
1634
2927
  };
1635
2928
  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)(() => {
2929
+ var Ctx = (0, import_react3.createContext)(null);
2930
+ var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, surveys = true, messenger = {}, feedback = {}, heatmaps = true, ...options }) => {
2931
+ const [client] = (0, import_react3.useState)(() => given ?? createMRRocket({ ...options, consent }));
2932
+ (0, import_react3.useEffect)(() => {
1640
2933
  if (consent) client.setConsent(consent);
1641
2934
  }, [client, consent]);
1642
- return (0, import_react2.createElement)(Ctx.Provider, { value: client }, children, flows ? (0, import_react2.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null);
2935
+ (0, import_react3.useEffect)(() => {
2936
+ if (options.replay !== void 0) client.setReplay(options.replay);
2937
+ }, [client, options.replay]);
2938
+ const start = (0, import_react3.useRef)(null);
2939
+ const touch = (e, end) => {
2940
+ const n = e.nativeEvent;
2941
+ const x = n.pageX ?? n.changedTouches?.[0]?.pageX;
2942
+ const y = n.pageY ?? n.changedTouches?.[0]?.pageY;
2943
+ if (x === void 0 || y === void 0) return;
2944
+ if (!end) start.current = { x, y, t: Date.now() };
2945
+ 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);
2946
+ };
2947
+ return (0, import_react3.createElement)(
2948
+ Ctx.Provider,
2949
+ { value: client },
2950
+ heatmaps ? (0, import_react3.createElement)(RN4.View, { style: { flex: 1 }, onTouchStartCapture: (e) => touch(e, false), onTouchEndCapture: (e) => touch(e, true) }, children) : children,
2951
+ flows ? (0, import_react3.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null,
2952
+ surveys ? (0, import_react3.createElement)(MRRocketSurveys, { client, appearance }) : null,
2953
+ feedback ? (0, import_react3.createElement)(MRRocketFeedback, { client, appearance, options: feedback }) : null,
2954
+ messenger ? (0, import_react3.createElement)(MRRocketMessenger, { client, appearance, options: messenger }) : null
2955
+ );
1643
2956
  };
1644
2957
  var useClient = () => {
1645
- const c = (0, import_react2.useContext)(Ctx);
2958
+ const c = (0, import_react3.useContext)(Ctx);
1646
2959
  if (!c) throw new Error("MRRocket: wrap your app in <MRRocketProvider apiKey=\u2026>.");
1647
2960
  return c;
1648
2961
  };
1649
- var useVersion = (client) => (0, import_react2.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
2962
+ var useVersion = (client) => (0, import_react3.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
1650
2963
  var useMRRocket = () => {
1651
2964
  const client = useClient();
1652
2965
  useVersion(client);
@@ -1656,6 +2969,7 @@ var useMRRocket = () => {
1656
2969
  identify: client.identify,
1657
2970
  reset: client.reset,
1658
2971
  setConsent: client.setConsent,
2972
+ setReplay: client.setReplay,
1659
2973
  captureError: client.captureError,
1660
2974
  flush: client.flush,
1661
2975
  distinctId: client.getDistinctId(),
@@ -1690,7 +3004,7 @@ var useVariant = (key) => {
1690
3004
  useVersion(client);
1691
3005
  const config = client.getConfig();
1692
3006
  const variant = client.peekVariant(key);
1693
- (0, import_react2.useEffect)(() => {
3007
+ (0, import_react3.useEffect)(() => {
1694
3008
  if (config) client.expose(key, variant);
1695
3009
  }, [client, config, key, variant]);
1696
3010
  return variant;
@@ -1701,7 +3015,7 @@ var useText = (id, fallback) => {
1701
3015
  const config = client.getConfig();
1702
3016
  const text2 = client.peekText(id);
1703
3017
  const person = client.getDistinctId();
1704
- (0, import_react2.useEffect)(() => {
3018
+ (0, import_react3.useEffect)(() => {
1705
3019
  if (config) client.exposeText(id);
1706
3020
  }, [client, config, id, person]);
1707
3021
  return text2 ?? fallback;
@@ -1726,13 +3040,13 @@ var useExpoRouterTracking = () => {
1726
3040
  console.warn("[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.");
1727
3041
  }
1728
3042
  const pathname = (router?.usePathname ?? noPathname)();
1729
- (0, import_react2.useEffect)(() => {
3043
+ (0, import_react3.useEffect)(() => {
1730
3044
  if (pathname) client.screen(pathname);
1731
3045
  }, [client, pathname]);
1732
3046
  };
1733
3047
  var useNavigationTracking = () => {
1734
3048
  const client = useClient();
1735
- return (0, import_react2.useCallback)(
3049
+ return (0, import_react3.useCallback)(
1736
3050
  (state) => {
1737
3051
  const name = activeRouteName(state);
1738
3052
  if (name) client.screen(name);