@mrrocket/sdk 0.15.0 → 0.17.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 (71) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +47 -3
  3. package/dist/api-client.d.ts +12 -5
  4. package/dist/api-client.js +12 -8
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/api.d.ts +4 -1
  7. package/dist/api.js +2 -1
  8. package/dist/api.js.map +1 -1
  9. package/dist/batch.d.ts +6 -4
  10. package/dist/batch.js +3 -3
  11. package/dist/batch.js.map +1 -1
  12. package/dist/browser/mrrocket-flows.js +9 -2
  13. package/dist/browser/mrrocket-forms.js +1 -1
  14. package/dist/browser/mrrocket-messenger.js +42 -43
  15. package/dist/browser/mrrocket-push.js +1 -1
  16. package/dist/browser/mrrocket-replay.js +3 -3
  17. package/dist/browser/mrrocket.js +1 -1
  18. package/dist/client.d.ts +10 -2
  19. package/dist/client.js +36 -15
  20. package/dist/client.js.map +1 -1
  21. package/dist/context.d.ts +5 -1
  22. package/dist/context.js.map +1 -1
  23. package/dist/core.d.ts +1 -1
  24. package/dist/core.js +1 -1
  25. package/dist/core.js.map +1 -1
  26. package/dist/errors.d.ts +5 -1
  27. package/dist/errors.js +8 -2
  28. package/dist/errors.js.map +1 -1
  29. package/dist/flows-ui.d.ts +1 -1
  30. package/dist/flows-ui.js +64 -14
  31. package/dist/flows-ui.js.map +1 -1
  32. package/dist/flows-web.d.ts +2 -0
  33. package/dist/flows-web.js +4 -1
  34. package/dist/flows-web.js.map +1 -1
  35. package/dist/flows.d.ts +59 -3
  36. package/dist/flows.js +88 -7
  37. package/dist/flows.js.map +1 -1
  38. package/dist/forms.d.ts +4 -0
  39. package/dist/forms.js +4 -1
  40. package/dist/forms.js.map +1 -1
  41. package/dist/identity.d.ts +42 -0
  42. package/dist/identity.js +24 -0
  43. package/dist/identity.js.map +1 -0
  44. package/dist/index.d.ts +3 -1
  45. package/dist/index.js +5 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/messenger-ui.d.ts +12 -2
  48. package/dist/messenger-ui.js +58 -47
  49. package/dist/messenger-ui.js.map +1 -1
  50. package/dist/messenger.js +3 -2
  51. package/dist/messenger.js.map +1 -1
  52. package/dist/push-web.d.ts +2 -13
  53. package/dist/push-web.js +4 -2
  54. package/dist/push-web.js.map +1 -1
  55. package/dist/react-native/index.d.cts +105 -14
  56. package/dist/react-native/index.d.ts +105 -14
  57. package/dist/react-native/index.js +238 -55
  58. package/dist/react-native/index.js.map +1 -1
  59. package/dist/replay.d.ts +4 -1
  60. package/dist/replay.js +8 -2
  61. package/dist/replay.js.map +1 -1
  62. package/dist/server.d.ts +2 -0
  63. package/dist/server.js +2 -0
  64. package/dist/server.js.map +1 -1
  65. package/dist/styles-css.d.ts +1 -1
  66. package/dist/styles-css.js +40 -41
  67. package/dist/styles-css.js.map +1 -1
  68. package/dist/user-hash.d.ts +27 -0
  69. package/dist/user-hash.js +24 -0
  70. package/dist/user-hash.js.map +1 -0
  71. package/package.json +5 -1
@@ -43,6 +43,7 @@ __export(native_exports, {
43
43
  useOnboarding: () => useOnboarding,
44
44
  usePaywall: () => usePaywall,
45
45
  usePush: () => usePush,
46
+ useReview: () => useReview,
46
47
  useText: () => useText,
47
48
  useVariant: () => useVariant
48
49
  });
@@ -50,7 +51,7 @@ module.exports = __toCommonJS(native_exports);
50
51
  var import_react3 = require("react");
51
52
 
52
53
  // src/core.ts
53
- var SDK_VERSION = "0.15.0";
54
+ var SDK_VERSION = "0.17.0";
54
55
  var propOf = (person, name) => {
55
56
  const want = name.trim().toLowerCase();
56
57
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -172,6 +173,23 @@ var textChanges = (config, distinctId, preview) => {
172
173
  };
173
174
  var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
174
175
 
176
+ // src/identity.ts
177
+ var UNSIGNED_HELP = "401: sign the user id on your server with userHash(id) from @mrrocket/sdk/next/server and pass identify(id, props, { hash }).";
178
+ var warnUnsigned = (status, log = (m) => console.warn(m)) => {
179
+ const g = globalThis;
180
+ if (status !== 401 || g.__mrrocketUnsigned) return;
181
+ g.__mrrocketUnsigned = true;
182
+ log(`[MRRocket] ${UNSIGNED_HELP}`);
183
+ };
184
+ var nextSigned = (signed, id, hash, log = (m) => console.warn(m)) => {
185
+ const h3 = typeof hash === "string" ? hash.trim().toLowerCase() : "";
186
+ if (/^[0-9a-f]{64}$/.test(h3)) return { id, hash: h3 };
187
+ if (h3) log("[MRRocket] identify: that hash is no userHash(id); not sent.");
188
+ return signed && signed.id === id ? signed : null;
189
+ };
190
+ var hashFor = (signed, id) => signed && id && signed.id === id ? signed.hash : void 0;
191
+ var hashForAll = (signed, ids) => signed && ids.some((id) => id === signed.id) && ids.every((id) => !id || id === signed.id) ? signed.hash : void 0;
192
+
175
193
  // src/api-client.ts
176
194
  var CHAT_FILE_MAX_BYTES = 10 * 1024 * 1024;
177
195
  var CHAT_FILES_PER_MESSAGE = 4;
@@ -212,6 +230,7 @@ var call = async (url, init, signal) => {
212
230
  throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
213
231
  }
214
232
  if (!res.ok) {
233
+ warnUnsigned(res.status);
215
234
  let detail = "";
216
235
  try {
217
236
  detail = (await res.json()).error ?? "";
@@ -223,21 +242,22 @@ var call = async (url, init, signal) => {
223
242
  };
224
243
  var createApi = (c) => {
225
244
  const base = `${c.host}/api/sdk`;
226
- const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
227
- const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
245
+ const who2 = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
246
+ const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
247
+ const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
228
248
  return {
229
- 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),
230
- post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
231
- roadmap: (signal) => call(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
249
+ list: (o, signal) => call(`${base}/feedback?${qs({ ...me, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
250
+ post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
251
+ roadmap: (signal) => call(`${base}/feedback/roadmap?${qs(me)}`, void 0, signal),
232
252
  changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
233
253
  submit: (p) => post("/feedback", p),
234
254
  vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
235
255
  comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
236
256
  comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
237
- survey: (url, signal) => call(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, void 0, signal),
257
+ survey: (url, signal) => call(`${base}/surveys?${qs({ ...me, url })}`, void 0, signal),
238
258
  seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
239
259
  respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
240
- chat: (signal) => call(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
260
+ chat: (signal) => call(`${base}/messages?${qs(me)}`, void 0, signal),
241
261
  /** A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). */
242
262
  sendChat: (m) => post("/messages", m),
243
263
  /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
@@ -245,14 +265,15 @@ var createApi = (c) => {
245
265
  const form = new FormData();
246
266
  form.append("key", c.apiKey);
247
267
  form.append("distinctId", c.distinctId);
268
+ if (c.userHash) form.append("userHash", c.userHash);
248
269
  if ("uri" in file) form.append("file", file);
249
270
  else form.append("file", file, file.name || "file");
250
271
  return call(`${base}/messages/attachments`, { method: "POST", body: form }, signal);
251
272
  },
252
273
  /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
253
- attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`,
274
+ attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me)}`,
254
275
  articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
255
- article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
276
+ article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
256
277
  messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
257
278
  rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
258
279
  };
@@ -274,8 +295,8 @@ var byteLength = (s) => {
274
295
  }
275
296
  return n;
276
297
  };
277
- var batchRequests = (key, events, sdk) => {
278
- const head = `{"key":${JSON.stringify(key)},"sdk":${JSON.stringify(sdk)},"events":[`;
298
+ var batchRequests = (key, events, sdk, userHash) => {
299
+ const head = `{"key":${JSON.stringify(key)},${userHash ? `"userHash":${JSON.stringify(userHash)},` : ""}"sdk":${JSON.stringify(sdk)},"events":[`;
279
300
  const empty = byteLength(head) + 2;
280
301
  const out = [];
281
302
  let chunk = [];
@@ -317,6 +338,7 @@ var describeError = (error) => {
317
338
  var CANCEL_EVENT = "$cancel_flow";
318
339
  var ONBOARDING_EVENT = "$onboarding";
319
340
  var INAPP_EVENT = "$inapp";
341
+ var REVIEW_EVENT = "$review_prompt";
320
342
  var PLAIN_CANCEL = {
321
343
  version: 0,
322
344
  question: "",
@@ -336,6 +358,25 @@ var matchesTrigger = (trigger, event) => {
336
358
  return t === e || SIGNUP.has(t) && SIGNUP.has(e);
337
359
  };
338
360
  var startsOnLoad = (trigger) => /^(first (launch|visit|open|session|login|sign ?in|app open)|app (opened|launched)|on load|every visit|always)?$/i.test(trigger.trim());
361
+ var isPageTrigger = (trigger) => trigger.trim().startsWith("/");
362
+ var manual = (trigger) => eventSlug(trigger) === "from_a_checklist";
363
+ var videoEmbed = (url) => {
364
+ const u = (url ?? "").trim();
365
+ if (!/^https?:\/\//i.test(u)) return null;
366
+ let m = u.match(/^https?:\/\/(?:www\.|m\.)?(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([\w-]{6,20})/i);
367
+ if (m) return { kind: "iframe", src: `https://www.youtube-nocookie.com/embed/${m[1]}?rel=0` };
368
+ m = u.match(/^https?:\/\/(?:www\.|player\.)?vimeo\.com\/(?:video\/)?(\d{4,12})/i);
369
+ if (m) return { kind: "iframe", src: `https://player.vimeo.com/video/${m[1]}?dnt=1` };
370
+ m = u.match(/^https?:\/\/(?:www\.)?loom\.com\/(?:share|embed)\/([\w-]{8,64})/i);
371
+ if (m) return { kind: "iframe", src: `https://www.loom.com/embed/${m[1]}` };
372
+ if (/\.(mp4|webm|mov|m4v)(\?|#|$)/i.test(u) || /\/media\/uploads\//.test(u)) return { kind: "file", src: u };
373
+ return null;
374
+ };
375
+ var mediaOf = (step) => {
376
+ const m = step.media;
377
+ if (!m || m.kind !== "image" && m.kind !== "video" || !/^https?:\/\//i.test(m.url ?? "")) return void 0;
378
+ return m.kind === "video" && !videoEmbed(m.url) ? void 0 : m;
379
+ };
339
380
  var runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
340
381
  var discounted = (amount, percent) => Math.round(amount * (1 - percent / 100) * 100) / 100;
341
382
  var money = (amount, currency) => {
@@ -383,6 +424,7 @@ var createFlows = (env) => {
383
424
  const seenEvents = /* @__PURE__ */ new Set();
384
425
  const now = () => env.now?.() ?? Date.now();
385
426
  let timer = null;
427
+ let returnTo = null;
386
428
  const flows = () => config?.onboarding ?? [];
387
429
  const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
388
430
  const prog = (id) => {
@@ -520,7 +562,14 @@ var createFlows = (env) => {
520
562
  shown.add(`s:${i.id}`);
521
563
  env.track(ONBOARDING_EVENT, { flow: flow.id, step: i.id, kind: "checklist", action: "shown" });
522
564
  }
523
- return { type: "checklist", flow, items: items.map((s) => ({ step: s, done: p.done.has(s.id) })) };
565
+ return {
566
+ type: "checklist",
567
+ flow,
568
+ items: items.map((s) => {
569
+ const tour = s.startsTour && s.startsTour !== flow.id ? flowById(s.startsTour) : void 0;
570
+ return { step: s, done: p.done.has(s.id), ...tour && nodesOf(tour).length ? { tour } : {} };
571
+ })
572
+ };
524
573
  }
525
574
  if (!shown.has(`s:${step.id}`)) {
526
575
  shown.add(`s:${step.id}`);
@@ -554,7 +603,28 @@ var createFlows = (env) => {
554
603
  }
555
604
  const ob = onboardingView();
556
605
  if (ob) return ob;
557
- if (active && !holding) active = null;
606
+ if (active && !holding) {
607
+ const back = returnTo;
608
+ const ended = active;
609
+ returnTo = null;
610
+ active = null;
611
+ const list = back ? flowById(back.flow) : void 0;
612
+ if (back && list) {
613
+ const item = stepsIn(nodesOf(list)).find((s) => s.id === back.step);
614
+ const p = prog(list.id);
615
+ if (item && !item.event && !progress.get(ended)?.dismissed && !p.done.has(item.id)) {
616
+ markDone(p, item.id);
617
+ record({ flow: list.id, step: item.id, kind: "checklist", action: "completed" });
618
+ }
619
+ active = list.id;
620
+ const again = onboardingView();
621
+ if (again) return again;
622
+ if (!holding) active = null;
623
+ } else if (known) {
624
+ const after = flowById(ended)?.trigger;
625
+ Promise.resolve().then(() => autoStart(after));
626
+ }
627
+ }
558
628
  const m = messages.find((x) => !handled.has(x.id));
559
629
  return m ? { type: "message", message: m } : null;
560
630
  };
@@ -579,10 +649,15 @@ var createFlows = (env) => {
579
649
  emit();
580
650
  return true;
581
651
  };
582
- const autoStart = () => {
652
+ const allowed = (f) => f.audience !== "signed_in" || !!env.personId();
653
+ const autoStart = (after) => {
583
654
  if (!auto || active || !known) return;
584
655
  if (!env.personId() && !env.storage?.()) return;
585
- const next = flows().find((f) => startsOnLoad(f.trigger) && nodesOf(f).length && !finished(f));
656
+ const path = env.path?.();
657
+ const same2 = after && !manual(after) ? eventSlug(after) : "";
658
+ const next = flows().find(
659
+ (f) => (isPageTrigger(f.trigger) ? path !== void 0 && pageMatches(f.trigger, path) : startsOnLoad(f.trigger) || !!same2 && eventSlug(f.trigger) === same2) && allowed(f) && nodesOf(f).length && !finished(f)
660
+ );
586
661
  if (next) start(next);
587
662
  };
588
663
  const refresh = () => {
@@ -592,6 +667,7 @@ var createFlows = (env) => {
592
667
  seenEvents.clear();
593
668
  handled.clear();
594
669
  active = null;
670
+ returnTo = null;
595
671
  loadedFor = void 0;
596
672
  }
597
673
  progressFor = pid;
@@ -630,9 +706,9 @@ var createFlows = (env) => {
630
706
  };
631
707
  const offerAt = (c) => c.round > 1 ? { round: 2 } : c.rule >= 0 ? { rule: c.rule } : {};
632
708
  const toOffer = (c) => {
633
- const who = { answer: c.answer, plan: c.options.plan, months: monthsSince(c.options.customerSince, env.now?.()) };
709
+ const who2 = { answer: c.answer, plan: c.options.plan, months: monthsSince(c.options.customerSince, env.now?.()) };
634
710
  const rules = c.flow.rules?.[c.reason] ?? [];
635
- c.rule = rules.findIndex((r) => ruleMatches(r, who));
711
+ c.rule = rules.findIndex((r) => ruleMatches(r, who2));
636
712
  c.offer = c.rule < 0 ? c.flow.offers[c.reason] : rules[c.rule].offer ?? void 0;
637
713
  c.round = 1;
638
714
  c.screen = c.offer ? "offer" : "confirm";
@@ -701,6 +777,7 @@ var createFlows = (env) => {
701
777
  },
702
778
  refresh,
703
779
  onEvent: (name) => {
780
+ if (name === "$pageview") autoStart();
704
781
  if (!name || name.startsWith("$")) return;
705
782
  const slug = eventSlug(name);
706
783
  const fresh = !!slug && !seenEvents.has(slug);
@@ -721,12 +798,12 @@ var createFlows = (env) => {
721
798
  emit();
722
799
  } else if (fresh) save();
723
800
  if (!auto || !env.personId() && !env.storage?.()) return;
724
- const next = flows().find((f) => nodesOf(f).length && matchesTrigger(f.trigger, name) && !finished(f));
801
+ const next = flows().find((f) => nodesOf(f).length && !manual(f.trigger) && matchesTrigger(f.trigger, name) && allowed(f) && !finished(f));
725
802
  if (next) start(next);
726
803
  },
727
804
  openCancelFlow,
728
805
  startOnboarding: (id) => {
729
- const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !finished(x));
806
+ const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !manual(x.trigger) && !finished(x));
730
807
  if (!f || !nodesOf(f).length) return false;
731
808
  const p = prog(f.id);
732
809
  p.dismissed = false;
@@ -822,6 +899,8 @@ var createFlows = (env) => {
822
899
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
823
900
  save();
824
901
  emit();
902
+ const url = v.step.kind === "modal" ? safeUrl(v.step.url) : void 0;
903
+ if (url) env.openUrl?.(url);
825
904
  },
826
905
  check: (stepId) => {
827
906
  const v = onboardingView();
@@ -838,9 +917,26 @@ var createFlows = (env) => {
838
917
  if (!v || v.type !== "step" && v.type !== "checklist") return;
839
918
  prog(v.flow.id).dismissed = true;
840
919
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, ...v.type === "step" ? { step: v.step.id } : {}, action: "dismissed" });
841
- active = null;
920
+ active = returnTo && returnTo.flow !== v.flow.id && flowById(returnTo.flow) ? returnTo.flow : null;
921
+ returnTo = null;
842
922
  save();
843
923
  emit();
924
+ },
925
+ startTour: (stepId) => {
926
+ const v = onboardingView();
927
+ if (v?.type !== "checklist") return false;
928
+ const tour = v.items.find((i) => i.step.id === stepId)?.tour;
929
+ if (!tour) return false;
930
+ const p = prog(tour.id);
931
+ p.done.clear();
932
+ p.at.clear();
933
+ p.dismissed = false;
934
+ returnTo = { flow: v.flow.id, step: stepId };
935
+ active = tour.id;
936
+ env.track(ONBOARDING_EVENT, { flow: tour.id, action: "started", from: v.flow.id });
937
+ save();
938
+ emit();
939
+ return true;
844
940
  }
845
941
  },
846
942
  message: {
@@ -1047,7 +1143,7 @@ var createRecorder = (deps, st) => {
1047
1143
  const batch = frames.splice(0, frames.length);
1048
1144
  const ms = marks.splice(0, marks.length);
1049
1145
  const size = deps.screen();
1050
- const { distinctId, identified } = st.who();
1146
+ const { distinctId, identified, userHash } = st.who();
1051
1147
  const body = JSON.stringify({
1052
1148
  key: st.apiKey,
1053
1149
  consent: { replay: true },
@@ -1059,11 +1155,12 @@ var createRecorder = (deps, st) => {
1059
1155
  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 },
1060
1156
  distinctId,
1061
1157
  identified,
1158
+ ...userHash ? { userHash } : {},
1062
1159
  trigger: r.trigger,
1063
1160
  app: st.app
1064
1161
  });
1065
1162
  const status = await deps.send(body);
1066
- if (status === 403 || status === 409 || status === 413) r.stopped = true;
1163
+ if (status === 401 || status === 403 || status === 409 || status === 413) r.stopped = true;
1067
1164
  };
1068
1165
  const tick = async () => {
1069
1166
  if (busy || !st.allowed() || !st.config()) return;
@@ -1223,6 +1320,7 @@ var createClient = (options, deps) => {
1223
1320
  let installId = null;
1224
1321
  let memoryId = newDistinctId();
1225
1322
  let identified;
1323
+ let signed = null;
1226
1324
  let person;
1227
1325
  let config = null;
1228
1326
  let queue = [];
@@ -1257,6 +1355,13 @@ var createClient = (options, deps) => {
1257
1355
  const errorQueue = [];
1258
1356
  const errorsSeen = /* @__PURE__ */ new Set();
1259
1357
  let errorsSent = 0;
1358
+ const fetchInApp = async (id) => {
1359
+ if (!apiKey) return null;
1360
+ const hash = hashOf(id);
1361
+ const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}${hash ? `&userHash=${hash}` : ""}`, { method: "GET" });
1362
+ warnUnsigned(r.status, deps.log);
1363
+ return r.ok ? await r.json() : null;
1364
+ };
1260
1365
  const paywall = createPaywallHost({ config: () => config, distinctId: () => distinctId(), track: (event, props) => record(event, props) });
1261
1366
  const flows = createFlows({
1262
1367
  track: (event, props) => record(event, props),
@@ -1267,11 +1372,7 @@ var createClient = (options, deps) => {
1267
1372
  set: (k, v) => void set(k, v)
1268
1373
  } : null,
1269
1374
  openUrl: deps.openUrl,
1270
- fetchInApp: async (id) => {
1271
- if (!apiKey) return null;
1272
- const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}`, { method: "GET" });
1273
- return r.ok ? await r.json() : null;
1274
- }
1375
+ fetchInApp: (id) => fetchInApp(id)
1275
1376
  });
1276
1377
  const refreshFlows = () => {
1277
1378
  if (!isReady || closed) return;
@@ -1334,6 +1435,7 @@ var createClient = (options, deps) => {
1334
1435
  };
1335
1436
  const distinctId = () => identified ?? (consent === "persistent" && installId ? installId : memoryId);
1336
1437
  const sendId = () => consent === "persistent" ? distinctId() : identified;
1438
+ const hashOf = (id) => hashFor(signed, id);
1337
1439
  const touchSession = () => {
1338
1440
  const t = now();
1339
1441
  if (!session || t - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t };
@@ -1371,6 +1473,7 @@ var createClient = (options, deps) => {
1371
1473
  const timer = ctl ? setTimeoutFn(() => ctl.abort(), REQUEST_TIMEOUT_MS) : null;
1372
1474
  try {
1373
1475
  const r = await deps.fetch(`${host}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body, signal: ctl?.signal });
1476
+ warnUnsigned(r.status, deps.log);
1374
1477
  return r.status;
1375
1478
  } catch {
1376
1479
  return 0;
@@ -1382,7 +1485,7 @@ var createClient = (options, deps) => {
1382
1485
  if (!queue.length || consent === "off" || !apiKey) return;
1383
1486
  if (now() < backoffUntil) return;
1384
1487
  const snapshot = queue.slice();
1385
- const requests = batchRequests(apiKey, snapshot, sdk);
1488
+ const requests = batchRequests(apiKey, snapshot, sdk, hashOf(sendId()));
1386
1489
  const included = new Set(requests.flatMap((r) => r.events));
1387
1490
  const done = /* @__PURE__ */ new Set();
1388
1491
  for (const r of requests) {
@@ -1393,7 +1496,7 @@ var createClient = (options, deps) => {
1393
1496
  backoffUntil = 0;
1394
1497
  } else if (status >= 400 && status < 500 && status !== 408 && status !== 429) {
1395
1498
  r.events.forEach((e) => done.add(e));
1396
- warn(`Events were rejected with ${status}. Check the public key (MRRocket \u203A Settings \u203A Products) and the host.`);
1499
+ if (status !== 401) warn(`Events were rejected with ${status}. Check the public key (MRRocket \u203A Settings \u203A Products) and the host.`);
1397
1500
  } else {
1398
1501
  failures++;
1399
1502
  backoffUntil = now() + Math.min(MAX_BACKOFF_MS, 1e3 * 2 ** failures);
@@ -1432,8 +1535,9 @@ var createClient = (options, deps) => {
1432
1535
  }
1433
1536
  while (errorQueue.length) {
1434
1537
  const batch = errorQueue.splice(0, 4);
1435
- const status = await post("/api/sdk/errors", JSON.stringify({ key: apiKey, sdk, errors: batch }));
1436
- if (status >= 400 && status < 500 && status !== 429) warn(`Error reports were rejected with ${status}. Check the public key and the host.`);
1538
+ const userHash = hashForAll(signed, batch.map((r) => r.distinctId));
1539
+ const status = await post("/api/sdk/errors", JSON.stringify({ key: apiKey, ...userHash ? { userHash } : {}, sdk, errors: batch }));
1540
+ if (status >= 400 && status < 500 && status !== 429 && status !== 401) warn(`Error reports were rejected with ${status}. Check the public key and the host.`);
1437
1541
  }
1438
1542
  };
1439
1543
  const capture = (error, mechanism, fatal = false) => {
@@ -1444,7 +1548,7 @@ var createClient = (options, deps) => {
1444
1548
  if (errorsSeen.has(mark)) return;
1445
1549
  errorsSeen.add(mark);
1446
1550
  errorsSent++;
1447
- const who = sendId();
1551
+ const who2 = sendId();
1448
1552
  errorQueue.push({
1449
1553
  ...d,
1450
1554
  mechanism,
@@ -1453,7 +1557,7 @@ var createClient = (options, deps) => {
1453
1557
  ts: now(),
1454
1558
  crumbs: crumbs.slice(),
1455
1559
  ...release ? { release } : {},
1456
- ...who ? { distinctId: who } : {},
1560
+ ...who2 ? { distinctId: who2 } : {},
1457
1561
  ...session ? { sessionId: session.id } : {}
1458
1562
  });
1459
1563
  recorder?.mark({ kind: "error", path: currentScreen ?? "/", label: clip2(d.message, 120) });
@@ -1491,7 +1595,7 @@ var createClient = (options, deps) => {
1491
1595
  allowed: () => replayOn && loaded && !closed && consent !== "off" && !!apiKey,
1492
1596
  config: () => config?.replay,
1493
1597
  session: () => session?.id ?? touchSession(),
1494
- who: () => ({ distinctId: sendId(), identified: !!identified }),
1598
+ who: () => ({ distinctId: sendId(), identified: !!identified, userHash: hashOf(sendId()) }),
1495
1599
  apiKey,
1496
1600
  app: context
1497
1601
  }
@@ -1541,6 +1645,7 @@ var createClient = (options, deps) => {
1541
1645
  };
1542
1646
  let pushToken = null;
1543
1647
  let pushFor;
1648
+ let pushHash;
1544
1649
  let pushOff = null;
1545
1650
  const opened = /* @__PURE__ */ new Set();
1546
1651
  const onPushOpen = (data) => {
@@ -1567,19 +1672,21 @@ var createClient = (options, deps) => {
1567
1672
  await loadedNow();
1568
1673
  if (!apiKey || consent === "off") return { status: "error", reason: !apiKey ? "No public key." : "The consent is off." };
1569
1674
  if (!identified) return { status: "not-signed-in" };
1570
- const who = identified;
1675
+ const who2 = identified;
1676
+ const userHash = hashOf(who2);
1571
1677
  try {
1572
1678
  const permission = await deps.push.permission(o.ask !== false);
1573
1679
  if (permission !== "granted") return { status: permission === "denied" ? "denied" : "not-asked" };
1574
1680
  const token = await deps.push.token(o.projectId);
1575
1681
  if (!token) return { status: "unavailable", reason: "This device has no push token (a simulator, or no EAS project id)." };
1576
- const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: who, platform: "expo", token, app: context }));
1682
+ const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: who2, userHash, platform: "expo", token, app: context }));
1577
1683
  if (status < 200 || status >= 300) {
1578
1684
  warn(`Push registration was rejected with ${status || "no answer"}. Check the public key and the host.`);
1579
1685
  return { status: "error", reason: `MRRocket answered ${status || "nothing"}.` };
1580
1686
  }
1581
1687
  pushToken = token;
1582
- pushFor = who;
1688
+ pushFor = who2;
1689
+ pushHash = userHash;
1583
1690
  listenForOpens();
1584
1691
  debug("Registered for push.");
1585
1692
  return { status: "registered", token };
@@ -1591,9 +1698,28 @@ var createClient = (options, deps) => {
1591
1698
  const token = pushToken;
1592
1699
  if (!token || !apiKey) return false;
1593
1700
  pushToken = null;
1594
- const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: pushFor, platform: "expo", token, remove: true }));
1701
+ const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: pushFor, userHash: pushHash, platform: "expo", token, remove: true }));
1595
1702
  return status >= 200 && status < 300;
1596
1703
  };
1704
+ let reviewing = false;
1705
+ const requestReview = async () => {
1706
+ if (!deps.storeReview) return "unavailable";
1707
+ await loadedNow();
1708
+ if (!apiKey || consent === "off") return "not-now";
1709
+ if (!identified) return "not-signed-in";
1710
+ if (reviewing) return "not-now";
1711
+ reviewing = true;
1712
+ try {
1713
+ const ask = (await fetchInApp(identified).catch(() => null))?.review;
1714
+ if (!ask) return "not-now";
1715
+ const asked = await deps.storeReview.request().catch(() => false);
1716
+ record(REVIEW_EVENT, { campaign: ask.campaign, store: ask.store, action: asked ? "requested" : "unavailable" });
1717
+ void flush();
1718
+ return asked ? "requested" : "unavailable";
1719
+ } finally {
1720
+ reviewing = false;
1721
+ }
1722
+ };
1597
1723
  let resolveReady = () => {
1598
1724
  };
1599
1725
  const ready = new Promise((r) => {
@@ -1723,13 +1849,14 @@ var createClient = (options, deps) => {
1723
1849
  record("$screen", { ...props, path, screen: name, ...previous ? { previous } : {} });
1724
1850
  recorder?.mark({ kind: "pageview", path });
1725
1851
  },
1726
- identify: (id, props) => {
1852
+ identify: (id, props, options2) => {
1727
1853
  if (typeof id !== "string" || !id) return;
1728
1854
  if (!loaded) {
1729
- early.push(() => client.identify(id, props));
1855
+ early.push(() => client.identify(id, props, options2));
1730
1856
  return;
1731
1857
  }
1732
1858
  const previous = sendId();
1859
+ signed = nextSigned(signed, id, options2?.hash, deps.log);
1733
1860
  identified = id;
1734
1861
  person = { ...props, id };
1735
1862
  record("$identify", { ...props, id, ...previous && previous !== id ? { previousId: previous } : {} });
@@ -1743,7 +1870,9 @@ var createClient = (options, deps) => {
1743
1870
  return;
1744
1871
  }
1745
1872
  if (pushToken) void unregisterForPush();
1873
+ void flush();
1746
1874
  identified = void 0;
1875
+ signed = null;
1747
1876
  person = void 0;
1748
1877
  session = null;
1749
1878
  utm = null;
@@ -1822,6 +1951,7 @@ var createClient = (options, deps) => {
1822
1951
  paywall,
1823
1952
  registerForPush,
1824
1953
  unregisterForPush,
1954
+ requestReview,
1825
1955
  flows,
1826
1956
  flush,
1827
1957
  ready,
@@ -1837,7 +1967,7 @@ var createClient = (options, deps) => {
1837
1967
  if (!apiKey || consent === "off") return null;
1838
1968
  const known = client.getUser();
1839
1969
  const user = as?.name || as?.email ? { ...known, ...as.name ? { name: as.name } : {}, ...as.email ? { email: as.email } : {} } : known;
1840
- return createApi({ host, apiKey, distinctId: distinctId(), user });
1970
+ return createApi({ host, apiKey, distinctId: distinctId(), user, userHash: hashOf(distinctId()) });
1841
1971
  },
1842
1972
  isSurveyDone: (id) => surveysDone.has(id),
1843
1973
  markSurveyDone: (id) => {
@@ -2109,6 +2239,22 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2109
2239
  ];
2110
2240
  return sheet([top, ...body, error], flows.cancel.close);
2111
2241
  }
2242
+ const media = (s) => {
2243
+ const m2 = mediaOf(s);
2244
+ if (!m2) return null;
2245
+ if (m2.kind === "image")
2246
+ return RN.Image ? (0, import_react.createElement)(RN.Image, {
2247
+ key: "media",
2248
+ source: { uri: m2.url },
2249
+ accessibilityLabel: m2.alt,
2250
+ resizeMode: "cover",
2251
+ style: { width: "100%", aspectRatio: 16 / 9, borderRadius: 10, backgroundColor: p.soft }
2252
+ }) : null;
2253
+ const v2 = videoEmbed(m2.url);
2254
+ const open = v2?.kind === "file" ? v2.src : m2.url;
2255
+ return RN.Linking ? btn("\u25B6 Watch the video", "secondary", () => void RN.Linking?.openURL(open).catch(() => {
2256
+ })) : null;
2257
+ };
2112
2258
  if (v.type === "step")
2113
2259
  return sheet(
2114
2260
  [
@@ -2118,6 +2264,7 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2118
2264
  v.total > 1 ? muted(`Step ${v.index + 1} of ${v.total}`) : null,
2119
2265
  close("Close the tour", flows.onboarding.dismiss)
2120
2266
  ),
2267
+ media(v.step),
2121
2268
  title(v.step.title),
2122
2269
  v.step.body ? muted(v.step.body, { fontSize: 15 }) : null,
2123
2270
  btn(v.step.button || (v.last ? "Done" : "Next"), "primary", flows.onboarding.next)
@@ -2138,13 +2285,14 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2138
2285
  ),
2139
2286
  muted(`${done} of ${v.items.length} done`),
2140
2287
  ...v.items.map(
2141
- ({ step, done: d }) => (0, import_react.createElement)(
2288
+ ({ step, done: d, tour }) => (0, import_react.createElement)(
2142
2289
  RN.Pressable,
2143
2290
  {
2144
2291
  key: step.id,
2145
- disabled: d || !!step.event,
2146
- onPress: () => flows.onboarding.check(step.id),
2147
- accessibilityRole: "checkbox",
2292
+ // 0.17.0: an item that starts a tour opens it; finishing the tour ticks it.
2293
+ disabled: d || !!step.event && !tour,
2294
+ onPress: () => tour ? flows.onboarding.startTour(step.id) : flows.onboarding.check(step.id),
2295
+ accessibilityRole: tour && !d ? "button" : "checkbox",
2148
2296
  accessibilityState: { checked: d },
2149
2297
  style: { flexDirection: "row", gap: 10, alignItems: "flex-start", paddingVertical: 6 }
2150
2298
  },
@@ -2227,6 +2375,12 @@ try {
2227
2375
  } catch {
2228
2376
  expoLocalization = null;
2229
2377
  }
2378
+ var expoStoreReview = null;
2379
+ try {
2380
+ expoStoreReview = require("expo-store-review");
2381
+ } catch {
2382
+ expoStoreReview = null;
2383
+ }
2230
2384
  var viewShot = null;
2231
2385
  try {
2232
2386
  viewShot = require("react-native-view-shot");
@@ -2330,6 +2484,17 @@ var recorderDeps = () => {
2330
2484
  masks: measureMasks
2331
2485
  };
2332
2486
  };
2487
+ var storeReview = () => {
2488
+ const m = expoStoreReview;
2489
+ if (!m?.requestReview) return null;
2490
+ return {
2491
+ request: async () => {
2492
+ if (m.isAvailableAsync && !await m.isAvailableAsync()) return false;
2493
+ await m.requestReview();
2494
+ return true;
2495
+ }
2496
+ };
2497
+ };
2333
2498
  var nativeDeps = (overrides = {}) => ({
2334
2499
  storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
2335
2500
  fetch: ((url, init) => fetch(url, init)),
@@ -2344,6 +2509,7 @@ var nativeDeps = (overrides = {}) => ({
2344
2509
  initialUrl: () => RN2.Linking.getInitialURL(),
2345
2510
  openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
2346
2511
  push: expoPush(),
2512
+ storeReview: storeReview(),
2347
2513
  recorder: recorderDeps()
2348
2514
  });
2349
2515
  var optionalModules = () => ({
@@ -2352,6 +2518,7 @@ var optionalModules = () => ({
2352
2518
  expoApplication: !!expoApplication,
2353
2519
  expoLocalization: !!expoLocalization,
2354
2520
  expoNotifications: !!expoNotifications,
2521
+ expoStoreReview: !!expoStoreReview?.requestReview,
2355
2522
  viewShot: !!viewShot?.captureScreen
2356
2523
  });
2357
2524
 
@@ -2597,7 +2764,7 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
2597
2764
  var MRRocketSurveys = ({ client, appearance }) => {
2598
2765
  const p = usePalette(appearance);
2599
2766
  const [survey, setSurvey] = (0, import_react2.useState)(null);
2600
- const [who, setWho] = (0, import_react2.useState)(() => client.getDistinctId());
2767
+ const [who2, setWho] = (0, import_react2.useState)(() => client.getDistinctId());
2601
2768
  (0, import_react2.useEffect)(() => client.subscribe(() => setWho(client.getDistinctId())), [client]);
2602
2769
  (0, import_react2.useEffect)(() => {
2603
2770
  let stop = false;
@@ -2618,7 +2785,7 @@ var MRRocketSurveys = ({ client, appearance }) => {
2618
2785
  clearTimeout(timer);
2619
2786
  ctrl.abort();
2620
2787
  };
2621
- }, [client, who]);
2788
+ }, [client, who2]);
2622
2789
  const done = (0, import_react2.useCallback)(() => {
2623
2790
  setSurvey((cur) => {
2624
2791
  if (cur) client.markSurveyDone(cur.s.id);
@@ -2740,6 +2907,7 @@ var POLL_IDLE_MS = 3e4;
2740
2907
  var WAITING_FOR_MS = 10 * 6e4;
2741
2908
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
2742
2909
  var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
2910
+ var who = (from, agentName) => from === "ai" && agentName?.trim() || whoLabel[from];
2743
2911
  var msgView = "home";
2744
2912
  var messengerOpen = opener();
2745
2913
  var openAt = (v = "home") => {
@@ -3124,7 +3292,7 @@ var MessengerSheet = ({
3124
3292
  k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
3125
3293
  )
3126
3294
  ),
3127
- m.at ? k.muted(`${mine ? "" : `${whoLabel[m.from]} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
3295
+ m.at ? k.muted(`${mine ? "" : `${who(m.from, o.agentName)} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
3128
3296
  );
3129
3297
  };
3130
3298
  const row = (a) => (0, import_react2.createElement)(
@@ -3182,7 +3350,7 @@ var MessengerSheet = ({
3182
3350
  RN3.View,
3183
3351
  { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 10 } },
3184
3352
  title ? teamMark(title, alpha(p.accent, 0.16), p.accent, 26) : null,
3185
- (0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 1 } }, (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.fg, fontSize: 14, fontWeight: "500" } }, last.body || (last.attachments?.length ? `Sent ${last.attachments.length === 1 ? last.attachments[0].name : `${last.attachments.length} files`}` : "")), k.muted(`${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`, { fontSize: 11.5 })),
3353
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 1 } }, (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.fg, fontSize: 14, fontWeight: "500" } }, last.body || (last.attachments?.length ? `Sent ${last.attachments.length === 1 ? last.attachments[0].name : `${last.attachments.length} files`}` : "")), k.muted(`${last.from === "visitor" ? "You" : who(last.from, o.agentName)} \xB7 ${timeAgo(last.at)}`, { fontSize: 11.5 })),
3186
3354
  chat.unseen.length ? (0, import_react2.createElement)(RN3.View, { style: { width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : glyph("next", p.muted, 14)
3187
3355
  )
3188
3356
  ], { paddingVertical: 10, paddingHorizontal: 12 })
@@ -3217,7 +3385,6 @@ var MessengerSheet = ({
3217
3385
  RN3.View,
3218
3386
  { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 6, gap: 8 } },
3219
3387
  chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
3220
- 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: r.field, paddingHorizontal: 16, backgroundColor: hu.surface, ...surf } }) : null,
3221
3388
  // Photos picked for the next message, each with a way to take it out again.
3222
3389
  photos.length ? (0, import_react2.createElement)(
3223
3390
  RN3.View,
@@ -3245,6 +3412,15 @@ var MessengerSheet = ({
3245
3412
  (0, import_react2.createElement)(
3246
3413
  RN3.View,
3247
3414
  { style: { gap: 2, borderRadius: 20, paddingTop: 10, paddingBottom: 6, paddingRight: 6, paddingLeft: 16, backgroundColor: hu.surface, ...surf } },
3415
+ // The optional email: the box's first line, under a hairline the message follows.
3416
+ needEmail ? k.field({
3417
+ value: email,
3418
+ onChange: setEmail,
3419
+ placeholder: "Email for our reply (optional)",
3420
+ email: true,
3421
+ max: 200,
3422
+ style: { minHeight: 0, marginRight: 10, marginBottom: 4, paddingHorizontal: 0, paddingTop: 0, paddingBottom: 8, borderWidth: 0, borderRadius: 0, borderBottomWidth: 1, borderBottomColor: emailOk ? p.line : p.danger, backgroundColor: "transparent", fontSize: 13.5 }
3423
+ }) : null,
3248
3424
  k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { minHeight: 28, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 4, backgroundColor: "transparent" } }),
3249
3425
  (0, import_react2.createElement)(
3250
3426
  RN3.View,
@@ -3296,11 +3472,13 @@ var MessengerSheet = ({
3296
3472
  key: v,
3297
3473
  onPress: () => setView(v),
3298
3474
  accessibilityRole: "tab",
3475
+ accessibilityLabel: name,
3299
3476
  accessibilityState: { selected: on },
3300
- style: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 6, height: 36, borderRadius: 18, ...on ? { backgroundColor: hu.segment, ...segLift } : {} }
3477
+ // Only the open tab carries its name; the others are icons, so the bar fits a narrow phone.
3478
+ style: { ...on ? { flex: 1 } : { width: 48 }, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 6, height: 36, borderRadius: 18, ...on ? { backgroundColor: hu.segment, ...segLift } : {} }
3301
3479
  },
3302
3480
  glyph(icon, on ? hu.fg : hu.muted, 17),
3303
- k.text(name, { fontSize: 13.5, fontWeight: "500", color: on ? hu.fg : hu.muted }),
3481
+ on ? k.text(name, { fontSize: 13.5, fontWeight: "500", color: hu.fg }) : null,
3304
3482
  badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 7, right: 12, width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : null
3305
3483
  );
3306
3484
  };
@@ -3314,7 +3492,7 @@ var MessengerSheet = ({
3314
3492
  const head = (0, import_react2.createElement)(
3315
3493
  RN3.View,
3316
3494
  { key: "head", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingLeft: 12, paddingRight: 12, paddingTop: 12, paddingBottom: 6, backgroundColor: ground } },
3317
- typeof view !== "string" ? back("help", "All articles") : view === "messages" ? back("home", "Back to home") : view === "home" && (o.logo || title) ? o.logo ? (0, import_react2.createElement)(RN3.Image, { key: "logo", source: { uri: o.logo }, accessibilityIgnoresInvertColors: true, style: { width: 28, height: 28, borderRadius: 8, marginLeft: 4 } }) : teamMark(title, alpha(p.accent, 0.16), p.accent, 28) : null,
3495
+ typeof view !== "string" ? back("help", "All articles") : view === "messages" ? back("home", "Back to home") : view === "home" && (o.logo || title) ? o.logo ? (0, import_react2.createElement)(RN3.Image, { key: "logo", source: { uri: o.logo }, accessibilityIgnoresInvertColors: true, resizeMode: "contain", style: { height: 28, aspectRatio: 1, maxWidth: 132, borderRadius: 8, marginLeft: 4 } }) : teamMark(title, alpha(p.accent, 0.16), p.accent, 28) : null,
3318
3496
  // In the chat: who answers, in the middle.
3319
3497
  view === "messages" && title ? (0, import_react2.createElement)(
3320
3498
  RN3.View,
@@ -3378,6 +3556,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3378
3556
  corners: look.corners,
3379
3557
  header: look.header,
3380
3558
  logo: look.logo,
3559
+ agentName: look.agentName,
3381
3560
  ...defined
3382
3561
  } : given;
3383
3562
  const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
@@ -3467,7 +3646,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3467
3646
  elevation: 6
3468
3647
  }
3469
3648
  },
3470
- k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
3649
+ k.muted(`${who(banner.from, options.agentName)}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
3471
3650
  (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15, lineHeight: 20 } }, banner.body)
3472
3651
  ) : null;
3473
3652
  if (!launcher && !toast && !open) return null;
@@ -3570,6 +3749,10 @@ var usePaywall = () => {
3570
3749
  const client = useClient();
3571
3750
  return { open: client.openPaywall };
3572
3751
  };
3752
+ var useReview = () => {
3753
+ const client = useClient();
3754
+ return { request: client.requestReview };
3755
+ };
3573
3756
  var useOnboarding = () => {
3574
3757
  const client = useClient();
3575
3758
  return { start: client.startOnboarding };