@mrrocket/sdk 0.14.0 → 0.16.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 (70) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +45 -4
  3. package/dist/api-client.d.ts +67 -5
  4. package/dist/api-client.js +39 -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 +2 -2
  13. package/dist/browser/mrrocket-forms.js +1 -1
  14. package/dist/browser/mrrocket-messenger.js +202 -46
  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.js +48 -1
  30. package/dist/flows-ui.js.map +1 -1
  31. package/dist/flows-web.d.ts +2 -0
  32. package/dist/flows-web.js +4 -1
  33. package/dist/flows-web.js.map +1 -1
  34. package/dist/flows.d.ts +61 -1
  35. package/dist/flows.js +64 -21
  36. package/dist/flows.js.map +1 -1
  37. package/dist/forms.d.ts +4 -0
  38. package/dist/forms.js +4 -1
  39. package/dist/forms.js.map +1 -1
  40. package/dist/identity.d.ts +42 -0
  41. package/dist/identity.js +24 -0
  42. package/dist/identity.js.map +1 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/messenger-ui.d.ts +21 -6
  46. package/dist/messenger-ui.js +334 -80
  47. package/dist/messenger-ui.js.map +1 -1
  48. package/dist/messenger.d.ts +11 -3
  49. package/dist/messenger.js +9 -5
  50. package/dist/messenger.js.map +1 -1
  51. package/dist/push-web.d.ts +2 -13
  52. package/dist/push-web.js +4 -2
  53. package/dist/push-web.js.map +1 -1
  54. package/dist/react-native/index.d.cts +162 -14
  55. package/dist/react-native/index.d.ts +162 -14
  56. package/dist/react-native/index.js +552 -207
  57. package/dist/react-native/index.js.map +1 -1
  58. package/dist/replay.d.ts +4 -1
  59. package/dist/replay.js +8 -2
  60. package/dist/replay.js.map +1 -1
  61. package/dist/server.d.ts +2 -0
  62. package/dist/server.js +2 -0
  63. package/dist/server.js.map +1 -1
  64. package/dist/styles-css.d.ts +1 -1
  65. package/dist/styles-css.js +200 -44
  66. package/dist/styles-css.js.map +1 -1
  67. package/dist/user-hash.d.ts +27 -0
  68. package/dist/user-hash.js +24 -0
  69. package/dist/user-hash.js.map +1 -0
  70. package/package.json +9 -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.14.0";
54
+ var SDK_VERSION = "0.16.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,8 +173,32 @@ 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
- 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";
194
+ var CHAT_FILE_MAX_BYTES = 10 * 1024 * 1024;
195
+ var CHAT_FILES_PER_MESSAGE = 4;
196
+ var CORNERS = {
197
+ round: { panel: 22, bubble: 18, tail: 6, card: 16, field: 20 },
198
+ soft: { panel: 14, bubble: 12, tail: 4, card: 10, field: 10 },
199
+ square: { panel: 6, bubble: 6, tail: 2, card: 4, field: 4 }
200
+ };
201
+ var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-by";
177
202
  var inkOn = (hex) => {
178
203
  const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
179
204
  if (!m) return "#ffffff";
@@ -197,13 +222,15 @@ var call = async (url, init, signal) => {
197
222
  res = await fetch(url, {
198
223
  ...init,
199
224
  signal,
200
- headers: init?.body ? { "content-type": "application/json" } : void 0
225
+ // A form (a file upload) sets its own multipart type with the boundary.
226
+ headers: typeof init?.body === "string" ? { "content-type": "application/json" } : void 0
201
227
  });
202
228
  } catch (e) {
203
229
  if (e.name === "AbortError") throw e;
204
230
  throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
205
231
  }
206
232
  if (!res.ok) {
233
+ warnUnsigned(res.status);
207
234
  let detail = "";
208
235
  try {
209
236
  detail = (await res.json()).error ?? "";
@@ -215,24 +242,38 @@ var call = async (url, init, signal) => {
215
242
  };
216
243
  var createApi = (c) => {
217
244
  const base = `${c.host}/api/sdk`;
218
- const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
245
+ const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
246
+ const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
219
247
  const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
220
248
  return {
221
- 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),
222
- post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
223
- 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),
224
252
  changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
225
253
  submit: (p) => post("/feedback", p),
226
254
  vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
227
255
  comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
228
256
  comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
229
- 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),
230
258
  seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
231
259
  respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
232
- 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),
261
+ /** A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). */
233
262
  sendChat: (m) => post("/messages", m),
263
+ /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
264
+ uploadChatFile: (file, signal) => {
265
+ const form = new FormData();
266
+ form.append("key", c.apiKey);
267
+ form.append("distinctId", c.distinctId);
268
+ if (c.userHash) form.append("userHash", c.userHash);
269
+ if ("uri" in file) form.append("file", file);
270
+ else form.append("file", file, file.name || "file");
271
+ return call(`${base}/messages/attachments`, { method: "POST", body: form }, signal);
272
+ },
273
+ /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
274
+ attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me)}`,
234
275
  articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
235
- 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),
236
277
  messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
237
278
  rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
238
279
  };
@@ -254,8 +295,8 @@ var byteLength = (s) => {
254
295
  }
255
296
  return n;
256
297
  };
257
- var batchRequests = (key, events, sdk) => {
258
- 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":[`;
259
300
  const empty = byteLength(head) + 2;
260
301
  const out = [];
261
302
  let chunk = [];
@@ -297,6 +338,7 @@ var describeError = (error) => {
297
338
  var CANCEL_EVENT = "$cancel_flow";
298
339
  var ONBOARDING_EVENT = "$onboarding";
299
340
  var INAPP_EVENT = "$inapp";
341
+ var REVIEW_EVENT = "$review_prompt";
300
342
  var PLAIN_CANCEL = {
301
343
  version: 0,
302
344
  question: "",
@@ -343,6 +385,9 @@ var pageMatches = (pattern, path) => {
343
385
  return at === (p.replace(/\/+$/, "") || "/");
344
386
  };
345
387
  var stepsIn = (nodes) => nodes.flatMap((n) => n.kind === "condition" ? [...stepsIn(n.yes), ...stepsIn(n.no)] : n.kind === "wait" ? [] : [n]);
388
+ var monthsSince = (since, now = Date.now()) => since == null || since === "" ? NaN : Math.floor((now - new Date(since).getTime()) / 2630016e3);
389
+ var low = (x) => x?.trim().toLowerCase();
390
+ var ruleMatches = ({ when: w = {} }, c) => (w.answer == null || low(w.answer) === low(c.answer)) && (w.plan == null || [w.plan].flat().some((x) => low(x) === low(c.plan))) && !(c.months >= w.tenureUnder) && !(c.months < w.tenureOver) && (w.tenureUnder == null || c.months >= 0) && (w.tenureOver == null || c.months >= 0);
346
391
  var createFlows = (env) => {
347
392
  let config = null;
348
393
  let cancel = null;
@@ -510,18 +555,23 @@ var createFlows = (env) => {
510
555
  };
511
556
  const view = () => {
512
557
  if (cancel) {
513
- const offer = cancel.reason ? cancel.flow.offers[cancel.reason] : void 0;
558
+ const c = cancel;
559
+ const followUp = c.flow.followUps?.[c.reason];
560
+ const f = followUp ? 1 : 0;
561
+ const steps = c.plain ? 1 : 3 + f + (c.flow.second?.[c.reason] ? 1 : 0);
514
562
  return {
515
563
  type: "cancel",
516
- screen: cancel.screen,
517
- flow: cancel.flow,
518
- reason: cancel.reason,
519
- offer: cancel.screen === "offer" ? offer : void 0,
520
- price: cancel.options.price,
521
- busy: cancel.busy,
522
- error: cancel.error,
523
- step: cancel.plain ? 1 : cancel.screen === "survey" ? 1 : cancel.screen === "offer" ? 2 : 3,
524
- steps: cancel.plain ? 1 : 3
564
+ screen: c.screen,
565
+ flow: c.flow,
566
+ reason: c.reason,
567
+ ...c.screen === "followup" && followUp ? { followUp, answer: c.answer } : {},
568
+ offer: c.screen === "offer" ? c.offer : void 0,
569
+ ...c.screen === "offer" ? { round: c.round } : {},
570
+ price: c.options.price,
571
+ busy: c.busy,
572
+ error: c.error,
573
+ step: c.screen === "survey" ? 1 : c.screen === "followup" ? 2 : c.screen === "offer" ? 1 + f + c.round : steps,
574
+ steps
525
575
  };
526
576
  }
527
577
  const ob = onboardingView();
@@ -600,12 +650,22 @@ var createFlows = (env) => {
600
650
  });
601
651
  return loading;
602
652
  };
653
+ const offerAt = (c) => c.round > 1 ? { round: 2 } : c.rule >= 0 ? { rule: c.rule } : {};
654
+ const toOffer = (c) => {
655
+ const who = { answer: c.answer, plan: c.options.plan, months: monthsSince(c.options.customerSince, env.now?.()) };
656
+ const rules = c.flow.rules?.[c.reason] ?? [];
657
+ c.rule = rules.findIndex((r) => ruleMatches(r, who));
658
+ c.offer = c.rule < 0 ? c.flow.offers[c.reason] : rules[c.rule].offer ?? void 0;
659
+ c.round = 1;
660
+ c.screen = c.offer ? "offer" : "confirm";
661
+ };
603
662
  const result = (c, outcome, comment) => {
604
- const offer = c.reason ? c.flow.offers[c.reason] : void 0;
663
+ const offer = c.screen === "offer" ? c.offer : void 0;
605
664
  return {
606
665
  outcome,
607
666
  run: c.run,
608
667
  ...c.reason ? { reason: c.reason, reasonLabel: c.flow.reasons.find((r) => r.id === c.reason)?.label } : {},
668
+ ...c.answer ? { answer: c.answer } : {},
609
669
  ...outcome === "saved" && offer ? { offer } : {},
610
670
  ...comment?.trim() ? { comment: comment.trim().slice(0, 500) } : {}
611
671
  };
@@ -618,7 +678,8 @@ var createFlows = (env) => {
618
678
  outcome: r.outcome,
619
679
  customer: c.options.customer ?? "consumer",
620
680
  ...r.reason ? { reason: r.reason } : {},
621
- ...r.offer ? { offer: r.offer.kind } : {},
681
+ ...r.answer ? { answer: r.answer } : {},
682
+ ...r.offer ? { offer: r.offer.kind, ...offerAt(c) } : {},
622
683
  ...r.comment ? { comment: r.comment } : {}
623
684
  });
624
685
  if (cancel === c) cancel = null;
@@ -648,7 +709,7 @@ var createFlows = (env) => {
648
709
  let resolve = () => {
649
710
  };
650
711
  const promise = new Promise((r) => resolve = r);
651
- const c = { flow, options, plain, screen: plain ? "confirm" : "survey", run: runId(), busy: false, resolve, promise };
712
+ const c = { flow, options, plain, screen: plain ? "confirm" : "survey", round: 1, run: runId(), busy: false, resolve, promise };
652
713
  cancel = c;
653
714
  env.track(CANCEL_EVENT, { action: "shown", run: c.run, version: flow.version, customer: options.customer ?? "consumer", plain });
654
715
  emit();
@@ -700,29 +761,43 @@ var createFlows = (env) => {
700
761
  },
701
762
  cancel: {
702
763
  pick: (reason) => {
703
- if (!cancel || cancel.busy || !cancel.flow.reasons.some((r) => r.id === reason)) return;
764
+ if (!cancel || cancel.busy || cancel.screen !== "survey" || !cancel.flow.reasons.some((r) => r.id === reason)) return;
765
+ if (cancel.reason !== reason) cancel.answer = void 0;
704
766
  cancel.reason = reason;
705
767
  cancel.error = void 0;
706
768
  emit();
707
769
  },
770
+ answer: (text2) => {
771
+ const c = cancel;
772
+ if (!c || c.busy || c.screen !== "followup") return;
773
+ const t = text2.slice(0, 500);
774
+ c.answer = t.trim() ? t : void 0;
775
+ emit();
776
+ },
708
777
  next: () => {
709
778
  const c = cancel;
710
- if (!c || c.screen !== "survey" || !c.reason) return;
711
- env.track(CANCEL_EVENT, { action: "answered", run: c.run, version: c.flow.version, reason: c.reason });
712
- c.screen = c.flow.offers[c.reason] ? "offer" : "confirm";
779
+ if (!c || !c.reason) return;
780
+ if (c.screen === "survey") {
781
+ env.track(CANCEL_EVENT, { action: "answered", run: c.run, version: c.flow.version, reason: c.reason });
782
+ if (c.flow.followUps?.[c.reason]) c.screen = "followup";
783
+ else toOffer(c);
784
+ } else if (c.screen === "followup") {
785
+ c.answer = c.answer?.trim();
786
+ toOffer(c);
787
+ } else return;
713
788
  emit();
714
789
  },
715
790
  accept: (comment) => {
716
791
  const c = cancel;
717
792
  if (!c || c.screen !== "offer" || !c.reason) return;
718
- const offer = c.flow.offers[c.reason];
719
793
  if (!c.busy)
720
794
  env.track(CANCEL_EVENT, {
721
795
  action: "offer",
722
796
  run: c.run,
723
797
  version: c.flow.version,
724
798
  reason: c.reason,
725
- offer: offer?.kind,
799
+ offer: c.offer?.kind,
800
+ ...offerAt(c),
726
801
  outcome: "accepted"
727
802
  });
728
803
  settle(c, "saved", comment);
@@ -735,10 +810,16 @@ var createFlows = (env) => {
735
810
  run: c.run,
736
811
  version: c.flow.version,
737
812
  reason: c.reason,
738
- offer: c.reason ? c.flow.offers[c.reason]?.kind : void 0,
813
+ offer: c.offer?.kind,
814
+ ...offerAt(c),
739
815
  outcome: "declined"
740
816
  });
741
- c.screen = "confirm";
817
+ const second = c.round === 1 && c.reason ? c.flow.second?.[c.reason] : void 0;
818
+ if (second) {
819
+ c.offer = second;
820
+ c.rule = void 0;
821
+ c.round = 2;
822
+ } else c.screen = "confirm";
742
823
  c.error = void 0;
743
824
  emit();
744
825
  },
@@ -988,7 +1069,7 @@ var createRecorder = (deps, st) => {
988
1069
  const batch = frames.splice(0, frames.length);
989
1070
  const ms = marks.splice(0, marks.length);
990
1071
  const size = deps.screen();
991
- const { distinctId, identified } = st.who();
1072
+ const { distinctId, identified, userHash } = st.who();
992
1073
  const body = JSON.stringify({
993
1074
  key: st.apiKey,
994
1075
  consent: { replay: true },
@@ -1000,11 +1081,12 @@ var createRecorder = (deps, st) => {
1000
1081
  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 },
1001
1082
  distinctId,
1002
1083
  identified,
1084
+ ...userHash ? { userHash } : {},
1003
1085
  trigger: r.trigger,
1004
1086
  app: st.app
1005
1087
  });
1006
1088
  const status = await deps.send(body);
1007
- if (status === 403 || status === 409 || status === 413) r.stopped = true;
1089
+ if (status === 401 || status === 403 || status === 409 || status === 413) r.stopped = true;
1008
1090
  };
1009
1091
  const tick = async () => {
1010
1092
  if (busy || !st.allowed() || !st.config()) return;
@@ -1164,6 +1246,7 @@ var createClient = (options, deps) => {
1164
1246
  let installId = null;
1165
1247
  let memoryId = newDistinctId();
1166
1248
  let identified;
1249
+ let signed = null;
1167
1250
  let person;
1168
1251
  let config = null;
1169
1252
  let queue = [];
@@ -1198,6 +1281,13 @@ var createClient = (options, deps) => {
1198
1281
  const errorQueue = [];
1199
1282
  const errorsSeen = /* @__PURE__ */ new Set();
1200
1283
  let errorsSent = 0;
1284
+ const fetchInApp = async (id) => {
1285
+ if (!apiKey) return null;
1286
+ const hash = hashOf(id);
1287
+ const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}${hash ? `&userHash=${hash}` : ""}`, { method: "GET" });
1288
+ warnUnsigned(r.status, deps.log);
1289
+ return r.ok ? await r.json() : null;
1290
+ };
1201
1291
  const paywall = createPaywallHost({ config: () => config, distinctId: () => distinctId(), track: (event, props) => record(event, props) });
1202
1292
  const flows = createFlows({
1203
1293
  track: (event, props) => record(event, props),
@@ -1208,11 +1298,7 @@ var createClient = (options, deps) => {
1208
1298
  set: (k, v) => void set(k, v)
1209
1299
  } : null,
1210
1300
  openUrl: deps.openUrl,
1211
- fetchInApp: async (id) => {
1212
- if (!apiKey) return null;
1213
- const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}`, { method: "GET" });
1214
- return r.ok ? await r.json() : null;
1215
- }
1301
+ fetchInApp: (id) => fetchInApp(id)
1216
1302
  });
1217
1303
  const refreshFlows = () => {
1218
1304
  if (!isReady || closed) return;
@@ -1275,6 +1361,7 @@ var createClient = (options, deps) => {
1275
1361
  };
1276
1362
  const distinctId = () => identified ?? (consent === "persistent" && installId ? installId : memoryId);
1277
1363
  const sendId = () => consent === "persistent" ? distinctId() : identified;
1364
+ const hashOf = (id) => hashFor(signed, id);
1278
1365
  const touchSession = () => {
1279
1366
  const t = now();
1280
1367
  if (!session || t - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t };
@@ -1312,6 +1399,7 @@ var createClient = (options, deps) => {
1312
1399
  const timer = ctl ? setTimeoutFn(() => ctl.abort(), REQUEST_TIMEOUT_MS) : null;
1313
1400
  try {
1314
1401
  const r = await deps.fetch(`${host}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body, signal: ctl?.signal });
1402
+ warnUnsigned(r.status, deps.log);
1315
1403
  return r.status;
1316
1404
  } catch {
1317
1405
  return 0;
@@ -1323,7 +1411,7 @@ var createClient = (options, deps) => {
1323
1411
  if (!queue.length || consent === "off" || !apiKey) return;
1324
1412
  if (now() < backoffUntil) return;
1325
1413
  const snapshot = queue.slice();
1326
- const requests = batchRequests(apiKey, snapshot, sdk);
1414
+ const requests = batchRequests(apiKey, snapshot, sdk, hashOf(sendId()));
1327
1415
  const included = new Set(requests.flatMap((r) => r.events));
1328
1416
  const done = /* @__PURE__ */ new Set();
1329
1417
  for (const r of requests) {
@@ -1334,7 +1422,7 @@ var createClient = (options, deps) => {
1334
1422
  backoffUntil = 0;
1335
1423
  } else if (status >= 400 && status < 500 && status !== 408 && status !== 429) {
1336
1424
  r.events.forEach((e) => done.add(e));
1337
- warn(`Events were rejected with ${status}. Check the public key (MRRocket \u203A Settings \u203A Products) and the host.`);
1425
+ if (status !== 401) warn(`Events were rejected with ${status}. Check the public key (MRRocket \u203A Settings \u203A Products) and the host.`);
1338
1426
  } else {
1339
1427
  failures++;
1340
1428
  backoffUntil = now() + Math.min(MAX_BACKOFF_MS, 1e3 * 2 ** failures);
@@ -1373,8 +1461,9 @@ var createClient = (options, deps) => {
1373
1461
  }
1374
1462
  while (errorQueue.length) {
1375
1463
  const batch = errorQueue.splice(0, 4);
1376
- const status = await post("/api/sdk/errors", JSON.stringify({ key: apiKey, sdk, errors: batch }));
1377
- if (status >= 400 && status < 500 && status !== 429) warn(`Error reports were rejected with ${status}. Check the public key and the host.`);
1464
+ const userHash = hashForAll(signed, batch.map((r) => r.distinctId));
1465
+ const status = await post("/api/sdk/errors", JSON.stringify({ key: apiKey, ...userHash ? { userHash } : {}, sdk, errors: batch }));
1466
+ if (status >= 400 && status < 500 && status !== 429 && status !== 401) warn(`Error reports were rejected with ${status}. Check the public key and the host.`);
1378
1467
  }
1379
1468
  };
1380
1469
  const capture = (error, mechanism, fatal = false) => {
@@ -1432,7 +1521,7 @@ var createClient = (options, deps) => {
1432
1521
  allowed: () => replayOn && loaded && !closed && consent !== "off" && !!apiKey,
1433
1522
  config: () => config?.replay,
1434
1523
  session: () => session?.id ?? touchSession(),
1435
- who: () => ({ distinctId: sendId(), identified: !!identified }),
1524
+ who: () => ({ distinctId: sendId(), identified: !!identified, userHash: hashOf(sendId()) }),
1436
1525
  apiKey,
1437
1526
  app: context
1438
1527
  }
@@ -1482,6 +1571,7 @@ var createClient = (options, deps) => {
1482
1571
  };
1483
1572
  let pushToken = null;
1484
1573
  let pushFor;
1574
+ let pushHash;
1485
1575
  let pushOff = null;
1486
1576
  const opened = /* @__PURE__ */ new Set();
1487
1577
  const onPushOpen = (data) => {
@@ -1509,18 +1599,20 @@ var createClient = (options, deps) => {
1509
1599
  if (!apiKey || consent === "off") return { status: "error", reason: !apiKey ? "No public key." : "The consent is off." };
1510
1600
  if (!identified) return { status: "not-signed-in" };
1511
1601
  const who = identified;
1602
+ const userHash = hashOf(who);
1512
1603
  try {
1513
1604
  const permission = await deps.push.permission(o.ask !== false);
1514
1605
  if (permission !== "granted") return { status: permission === "denied" ? "denied" : "not-asked" };
1515
1606
  const token = await deps.push.token(o.projectId);
1516
1607
  if (!token) return { status: "unavailable", reason: "This device has no push token (a simulator, or no EAS project id)." };
1517
- const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: who, platform: "expo", token, app: context }));
1608
+ const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: who, userHash, platform: "expo", token, app: context }));
1518
1609
  if (status < 200 || status >= 300) {
1519
1610
  warn(`Push registration was rejected with ${status || "no answer"}. Check the public key and the host.`);
1520
1611
  return { status: "error", reason: `MRRocket answered ${status || "nothing"}.` };
1521
1612
  }
1522
1613
  pushToken = token;
1523
1614
  pushFor = who;
1615
+ pushHash = userHash;
1524
1616
  listenForOpens();
1525
1617
  debug("Registered for push.");
1526
1618
  return { status: "registered", token };
@@ -1532,9 +1624,28 @@ var createClient = (options, deps) => {
1532
1624
  const token = pushToken;
1533
1625
  if (!token || !apiKey) return false;
1534
1626
  pushToken = null;
1535
- const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: pushFor, platform: "expo", token, remove: true }));
1627
+ const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: pushFor, userHash: pushHash, platform: "expo", token, remove: true }));
1536
1628
  return status >= 200 && status < 300;
1537
1629
  };
1630
+ let reviewing = false;
1631
+ const requestReview = async () => {
1632
+ if (!deps.storeReview) return "unavailable";
1633
+ await loadedNow();
1634
+ if (!apiKey || consent === "off") return "not-now";
1635
+ if (!identified) return "not-signed-in";
1636
+ if (reviewing) return "not-now";
1637
+ reviewing = true;
1638
+ try {
1639
+ const ask = (await fetchInApp(identified).catch(() => null))?.review;
1640
+ if (!ask) return "not-now";
1641
+ const asked = await deps.storeReview.request().catch(() => false);
1642
+ record(REVIEW_EVENT, { campaign: ask.campaign, store: ask.store, action: asked ? "requested" : "unavailable" });
1643
+ void flush();
1644
+ return asked ? "requested" : "unavailable";
1645
+ } finally {
1646
+ reviewing = false;
1647
+ }
1648
+ };
1538
1649
  let resolveReady = () => {
1539
1650
  };
1540
1651
  const ready = new Promise((r) => {
@@ -1664,13 +1775,14 @@ var createClient = (options, deps) => {
1664
1775
  record("$screen", { ...props, path, screen: name, ...previous ? { previous } : {} });
1665
1776
  recorder?.mark({ kind: "pageview", path });
1666
1777
  },
1667
- identify: (id, props) => {
1778
+ identify: (id, props, options2) => {
1668
1779
  if (typeof id !== "string" || !id) return;
1669
1780
  if (!loaded) {
1670
- early.push(() => client.identify(id, props));
1781
+ early.push(() => client.identify(id, props, options2));
1671
1782
  return;
1672
1783
  }
1673
1784
  const previous = sendId();
1785
+ signed = nextSigned(signed, id, options2?.hash, deps.log);
1674
1786
  identified = id;
1675
1787
  person = { ...props, id };
1676
1788
  record("$identify", { ...props, id, ...previous && previous !== id ? { previousId: previous } : {} });
@@ -1684,7 +1796,9 @@ var createClient = (options, deps) => {
1684
1796
  return;
1685
1797
  }
1686
1798
  if (pushToken) void unregisterForPush();
1799
+ void flush();
1687
1800
  identified = void 0;
1801
+ signed = null;
1688
1802
  person = void 0;
1689
1803
  session = null;
1690
1804
  utm = null;
@@ -1763,6 +1877,7 @@ var createClient = (options, deps) => {
1763
1877
  paywall,
1764
1878
  registerForPush,
1765
1879
  unregisterForPush,
1880
+ requestReview,
1766
1881
  flows,
1767
1882
  flush,
1768
1883
  ready,
@@ -1778,7 +1893,7 @@ var createClient = (options, deps) => {
1778
1893
  if (!apiKey || consent === "off") return null;
1779
1894
  const known = client.getUser();
1780
1895
  const user = as?.name || as?.email ? { ...known, ...as.name ? { name: as.name } : {}, ...as.email ? { email: as.email } : {} } : known;
1781
- return createApi({ host, apiKey, distinctId: distinctId(), user });
1896
+ return createApi({ host, apiKey, distinctId: distinctId(), user, userHash: hashOf(distinctId()) });
1782
1897
  },
1783
1898
  isSurveyDone: (id) => surveysDone.has(id),
1784
1899
  markSurveyDone: (id) => {
@@ -1899,9 +2014,9 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
1899
2014
  card(children, { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, paddingBottom: 34 })
1900
2015
  )
1901
2016
  );
1902
- const input = (placeholder) => (0, import_react.createElement)(RN.TextInput, {
1903
- value: comment,
1904
- onChangeText: setComment,
2017
+ const input = (placeholder, value = comment, onChange = setComment) => (0, import_react.createElement)(RN.TextInput, {
2018
+ value,
2019
+ onChangeText: onChange,
1905
2020
  placeholder,
1906
2021
  placeholderTextColor: p.muted,
1907
2022
  multiline: true,
@@ -1986,38 +2101,49 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
1986
2101
  );
1987
2102
  const error = v.error ? text2(v.error, { color: p.danger, fontSize: 13 }) : null;
1988
2103
  let body = [];
2104
+ const choices = (items) => (0, import_react.createElement)(
2105
+ RN.ScrollView,
2106
+ { style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
2107
+ ...items.map(
2108
+ (c) => (0, import_react.createElement)(
2109
+ RN.Pressable,
2110
+ {
2111
+ key: c.key,
2112
+ onPress: c.press,
2113
+ accessibilityRole: "radio",
2114
+ accessibilityState: { checked: c.on },
2115
+ style: {
2116
+ minHeight: 44,
2117
+ paddingHorizontal: 12,
2118
+ borderRadius: 10,
2119
+ borderWidth: 1,
2120
+ borderColor: c.on ? p.accent : p.line,
2121
+ backgroundColor: c.on ? p.soft : p.bg,
2122
+ justifyContent: "center"
2123
+ }
2124
+ },
2125
+ text2(c.label, { fontWeight: "500" })
2126
+ )
2127
+ )
2128
+ );
1989
2129
  if (v.screen === "survey")
1990
2130
  body = [
1991
2131
  title(v.flow.question || "Why are you leaving?"),
1992
- (0, import_react.createElement)(
1993
- RN.ScrollView,
1994
- { style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
1995
- ...v.flow.reasons.map(
1996
- (r) => (0, import_react.createElement)(
1997
- RN.Pressable,
1998
- {
1999
- key: r.id,
2000
- onPress: () => flows.cancel.pick(r.id),
2001
- accessibilityRole: "radio",
2002
- accessibilityState: { checked: v.reason === r.id },
2003
- style: {
2004
- minHeight: 44,
2005
- paddingHorizontal: 12,
2006
- borderRadius: 10,
2007
- borderWidth: 1,
2008
- borderColor: v.reason === r.id ? p.accent : p.line,
2009
- backgroundColor: v.reason === r.id ? p.soft : p.bg,
2010
- justifyContent: "center"
2011
- }
2012
- },
2013
- text2(r.label, { fontWeight: "500" })
2014
- )
2015
- )
2016
- ),
2132
+ choices(v.flow.reasons.map((r) => ({ key: r.id, label: r.label, on: v.reason === r.id, press: () => flows.cancel.pick(r.id) }))),
2017
2133
  btn("Continue", "primary", flows.cancel.next, !v.reason),
2018
2134
  btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2019
2135
  ];
2020
- else if (v.screen === "offer" && v.offer) {
2136
+ else if (v.screen === "followup" && v.followUp) {
2137
+ const f = v.followUp;
2138
+ const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2139
+ body = [
2140
+ label ? muted(`You said: ${label}`) : null,
2141
+ title(f.question),
2142
+ f.answers.length ? choices(f.answers.map((a, i) => ({ key: String(i), label: a, on: v.answer === a, press: () => flows.cancel.answer(a) }))) : input("Your answer (optional)", v.answer ?? "", flows.cancel.answer),
2143
+ btn("Continue", "primary", flows.cancel.next),
2144
+ btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2145
+ ];
2146
+ } else if (v.screen === "offer" && v.offer) {
2021
2147
  const o = v.offer;
2022
2148
  const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2023
2149
  const detail = o.kind === "discount" && o.percent ? v.price ? `${money(discounted(v.price.amount, o.percent), v.price.currency)} instead of ${money(v.price.amount, v.price.currency)}${o.months ? ` for ${months(o.months)}` : ""}` : `${o.percent}% off${o.months ? ` for ${months(o.months)}` : ""}` : o.kind === "pause" && o.months ? `Pause for up to ${months(o.months)}. Your data stays.` : o.kind === "downgrade" && o.planName ? o.planName : null;
@@ -2157,6 +2283,12 @@ try {
2157
2283
  } catch {
2158
2284
  expoLocalization = null;
2159
2285
  }
2286
+ var expoStoreReview = null;
2287
+ try {
2288
+ expoStoreReview = require("expo-store-review");
2289
+ } catch {
2290
+ expoStoreReview = null;
2291
+ }
2160
2292
  var viewShot = null;
2161
2293
  try {
2162
2294
  viewShot = require("react-native-view-shot");
@@ -2260,6 +2392,17 @@ var recorderDeps = () => {
2260
2392
  masks: measureMasks
2261
2393
  };
2262
2394
  };
2395
+ var storeReview = () => {
2396
+ const m = expoStoreReview;
2397
+ if (!m?.requestReview) return null;
2398
+ return {
2399
+ request: async () => {
2400
+ if (m.isAvailableAsync && !await m.isAvailableAsync()) return false;
2401
+ await m.requestReview();
2402
+ return true;
2403
+ }
2404
+ };
2405
+ };
2263
2406
  var nativeDeps = (overrides = {}) => ({
2264
2407
  storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
2265
2408
  fetch: ((url, init) => fetch(url, init)),
@@ -2274,6 +2417,7 @@ var nativeDeps = (overrides = {}) => ({
2274
2417
  initialUrl: () => RN2.Linking.getInitialURL(),
2275
2418
  openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
2276
2419
  push: expoPush(),
2420
+ storeReview: storeReview(),
2277
2421
  recorder: recorderDeps()
2278
2422
  });
2279
2423
  var optionalModules = () => ({
@@ -2282,6 +2426,7 @@ var optionalModules = () => ({
2282
2426
  expoApplication: !!expoApplication,
2283
2427
  expoLocalization: !!expoLocalization,
2284
2428
  expoNotifications: !!expoNotifications,
2429
+ expoStoreReview: !!expoStoreReview?.requestReview,
2285
2430
  viewShot: !!viewShot?.captureScreen
2286
2431
  });
2287
2432
 
@@ -2693,6 +2838,13 @@ var buzz = () => {
2693
2838
  } catch {
2694
2839
  }
2695
2840
  };
2841
+ var imagePicker = null;
2842
+ try {
2843
+ imagePicker = require("expo-image-picker");
2844
+ } catch {
2845
+ imagePicker = null;
2846
+ }
2847
+ var PHOTO_TYPES = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif" };
2696
2848
  var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
2697
2849
  var svg = null;
2698
2850
  try {
@@ -2708,9 +2860,10 @@ var GLYPH = {
2708
2860
  up: "M8 13V3M3.5 7.5 8 3l4.5 4.5",
2709
2861
  next: "M6 3.5 10.5 8 6 12.5",
2710
2862
  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",
2711
- cross: "m4 4 8 8M12 4l-8 8"
2863
+ cross: "m4 4 8 8M12 4l-8 8",
2864
+ clip: "m14.3 7.4-6.1 6.1a4 4 0 0 1-5.7-5.7l5.7-5.7a2.7 2.7 0 0 1 3.8 3.8L6.3 11.6a1.3 1.3 0 0 1-1.9-1.9l5.7-5.6"
2712
2865
  };
2713
- var FALLBACK = { chat: "\u{1F4AC}", home: "\u2302", help: "?", send: "\u2192", up: "\u2191", next: "\u203A", search: "\u2315", cross: "\xD7" };
2866
+ var FALLBACK = { chat: "\u{1F4AC}", home: "\u2302", help: "?", send: "\u2192", up: "\u2191", next: "\u203A", search: "\u2315", cross: "\xD7", clip: "\u{1F4CE}" };
2714
2867
  var glyph = (name, color, size = 20) => {
2715
2868
  const Svg = svg?.Svg ?? svg?.default;
2716
2869
  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]);
@@ -2722,7 +2875,11 @@ var teamMark = (title, bg, fg, size = 36) => (0, import_react2.createElement)(
2722
2875
  { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: bg, alignItems: "center", justifyContent: "center" } },
2723
2876
  (0, import_react2.createElement)(RN3.Text, { style: { color: fg, fontSize: Math.round(size * 0.42), fontWeight: "700" } }, (title.trim()[0] ?? "?").toUpperCase())
2724
2877
  );
2725
- var lift = { shadowColor: "#101218", shadowOpacity: 0.07, shadowRadius: 14, shadowOffset: { width: 0, height: 4 }, elevation: 2 };
2878
+ var HU = {
2879
+ light: { bg: "#ffffff", surface: "#f4f4f5", def: "#ececee", segment: "#ffffff", fg: "#18181b", muted: "#71717a" },
2880
+ dark: { bg: "#111113", surface: "#1c1c1f", def: "#26262a", segment: "#3a3a40", fg: "#fafafa", muted: "#a1a1aa" }
2881
+ };
2882
+ var hairline = { shadowColor: "#000", shadowOpacity: 0.07, shadowRadius: 3, shadowOffset: { width: 0, height: 1 }, elevation: 1 };
2726
2883
  var useChat = (client, o, open, view) => {
2727
2884
  const [thread, setThread] = (0, import_react2.useState)(null);
2728
2885
  const [loaded, setLoaded] = (0, import_react2.useState)(false);
@@ -2889,12 +3046,14 @@ var MessengerSheet = ({
2889
3046
  o,
2890
3047
  chat,
2891
3048
  view,
2892
- setView
3049
+ setView,
3050
+ brand
2893
3051
  }) => {
2894
3052
  const k = kit(p);
2895
3053
  const [body, setBody] = (0, import_react2.useState)("");
2896
3054
  const [email, setEmail] = (0, import_react2.useState)("");
2897
3055
  const [pending, setPending] = (0, import_react2.useState)(null);
3056
+ const [photos, setPhotos] = (0, import_react2.useState)([]);
2898
3057
  const [query, setQuery] = (0, import_react2.useState)("");
2899
3058
  const helpOn = o.help !== false;
2900
3059
  const arts = useArticles(client, helpOn, query);
@@ -2908,52 +3067,135 @@ var MessengerSheet = ({
2908
3067
  }, [count, view]);
2909
3068
  const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
2910
3069
  const emailOk = !email.trim() || EMAIL.test(email.trim());
3070
+ const pickPhotos = async () => {
3071
+ const api = client.api();
3072
+ if (!api || !imagePicker?.launchImageLibraryAsync || pending) return;
3073
+ const room = CHAT_FILES_PER_MESSAGE - photos.length;
3074
+ if (room <= 0) return chat.setError(`Up to ${CHAT_FILES_PER_MESSAGE} files per message.`);
3075
+ let res;
3076
+ try {
3077
+ res = await imagePicker.launchImageLibraryAsync({ allowsMultipleSelection: room > 1, selectionLimit: room, quality: 0.8, exif: false });
3078
+ } catch (e) {
3079
+ return chat.setError(e.message || "The photos could not be opened.");
3080
+ }
3081
+ if (res.canceled || res.cancelled || !res.assets?.length) return;
3082
+ chat.setError("");
3083
+ const added = [];
3084
+ for (const a of res.assets.slice(0, room)) {
3085
+ const name = a.fileName || a.uri.split("/").pop() || "photo.jpg";
3086
+ const type = a.mimeType && Object.values(PHOTO_TYPES).includes(a.mimeType) ? a.mimeType : PHOTO_TYPES[name.split(".").pop()?.toLowerCase() ?? ""];
3087
+ if (!type) {
3088
+ chat.setError("Only pictures (PNG, JPEG, WebP, GIF) can be sent.");
3089
+ continue;
3090
+ }
3091
+ if (a.fileSize && a.fileSize > CHAT_FILE_MAX_BYTES) {
3092
+ chat.setError(`${name} is larger than 10 MB.`);
3093
+ continue;
3094
+ }
3095
+ const ph = { key: `local-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`, uri: a.uri, name, type, size: a.fileSize ?? 0, up: Promise.resolve() };
3096
+ ph.up = api.uploadChatFile({ uri: a.uri, name, type }).then(
3097
+ (r2) => {
3098
+ ph.id = r2.id;
3099
+ setPhotos((list) => list.map((x) => x.key === ph.key ? { ...ph } : x));
3100
+ },
3101
+ (e) => {
3102
+ setPhotos((list) => list.filter((x) => x.key !== ph.key));
3103
+ chat.setError(`${name}: ${e.message}`);
3104
+ }
3105
+ );
3106
+ added.push(ph);
3107
+ }
3108
+ setPhotos((list) => [...list, ...added]);
3109
+ };
3110
+ const canSend = (!!body.trim() || photos.length > 0) && !pending && emailOk;
2911
3111
  const send = async () => {
2912
3112
  const text2 = body.trim();
2913
3113
  const api = client.api();
2914
- if (!text2 || pending || !emailOk || !api) return;
2915
- setPending({ id: "pending", from: "visitor", body: text2, at: (/* @__PURE__ */ new Date()).toISOString() });
3114
+ const files = photos;
3115
+ if (!text2 && !files.length || pending || !emailOk || !api) return;
3116
+ setPending({
3117
+ id: "pending",
3118
+ from: "visitor",
3119
+ body: text2,
3120
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3121
+ ...files.length ? { attachments: files.map((f) => ({ id: f.key, name: f.name, type: f.type, size: f.size })) } : {}
3122
+ });
2916
3123
  setBody("");
3124
+ setPhotos([]);
2917
3125
  chat.setError("");
2918
3126
  try {
2919
- chat.take(await api.sendChat({ body: text2, email: needEmail && email.trim() ? email.trim() : void 0 }));
3127
+ await Promise.all(files.map((f) => f.up));
3128
+ const ids = files.filter((f) => f.id).map((f) => f.id);
3129
+ if (!text2 && !ids.length) throw new Error("The photo could not be sent.");
3130
+ const win = RN3.Dimensions.get("screen");
3131
+ chat.take(
3132
+ await api.sendChat({
3133
+ body: text2,
3134
+ email: needEmail && email.trim() ? email.trim() : void 0,
3135
+ ...ids.length ? { attachments: ids } : {},
3136
+ // A little context for a bug report: the system and the screen size.
3137
+ context: { os: `${RN3.Platform.OS === "ios" ? "iOS" : RN3.Platform.OS === "android" ? "Android" : RN3.Platform.OS} ${RN3.Platform.Version ?? ""}`.trim(), browser: "App", viewport: `${Math.round(win.width)}\xD7${Math.round(win.height)}` }
3138
+ })
3139
+ );
2920
3140
  } catch (e) {
2921
3141
  setBody(text2);
3142
+ setPhotos(files.filter((f) => f.id));
2922
3143
  chat.setError(e.message);
2923
3144
  } finally {
2924
3145
  setPending(null);
2925
3146
  }
2926
3147
  };
3148
+ const localUris = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3149
+ for (const f of photos) localUris.current.set(f.key, f.uri);
3150
+ const fileUrl = (id) => localUris.current.get(id) ?? client.api()?.attachmentUrl(id) ?? "";
2927
3151
  const ink = inkOn(p.accent);
2928
3152
  const dark = p.bg === DARK.bg;
2929
- const canvas = dark ? p.bg : p.soft;
2930
- const raised = dark ? p.soft : p.bg;
2931
- const title = o.title ?? "Chat with us";
2932
- const reply = replyLine(o.replyTime);
3153
+ const r = CORNERS[o.corners ?? "round"];
3154
+ const plain = o.header !== "color";
3155
+ const hu = dark ? HU.dark : HU.light;
3156
+ const surf = {};
3157
+ const segLift = dark ? {} : hairline;
3158
+ const canvas = hu.bg;
3159
+ const raised = hu.surface;
3160
+ const title = o.title?.trim() ?? "";
2933
3161
  const bubble = (m, isPending = false) => {
2934
3162
  const mine = m.from === "visitor";
2935
3163
  return (0, import_react2.createElement)(
2936
3164
  RN3.View,
2937
3165
  { key: m.id, style: { alignSelf: mine ? "flex-end" : "flex-start", maxWidth: "82%", gap: 4 } },
2938
- (0, import_react2.createElement)(
3166
+ ...(m.attachments ?? []).map(
3167
+ (f) => (0, import_react2.createElement)(
3168
+ RN3.Pressable,
3169
+ {
3170
+ key: f.id,
3171
+ onPress: () => void RN3.Linking.openURL(fileUrl(f.id)).catch(() => void 0),
3172
+ accessibilityRole: "imagebutton",
3173
+ accessibilityLabel: f.name,
3174
+ style: { alignSelf: mine ? "flex-end" : "flex-start", opacity: isPending ? 0.7 : 1 }
3175
+ },
3176
+ f.type.startsWith("image/") ? (0, import_react2.createElement)(RN3.Image, { source: { uri: fileUrl(f.id) }, resizeMode: "cover", style: { width: 200, height: 150, borderRadius: r.bubble, backgroundColor: p.soft } }) : (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.soft, borderRadius: r.card, paddingHorizontal: 10, paddingVertical: 7 } }, k.text(`\u{1F4C4} ${f.name}`, { fontSize: 13 }))
3177
+ )
3178
+ ),
3179
+ !m.body && m.attachments?.length ? null : (0, import_react2.createElement)(
2939
3180
  RN3.View,
2940
3181
  {
2941
3182
  style: {
2942
- backgroundColor: mine ? p.accent : dark ? p.soft : "#eeeff1",
2943
- borderRadius: 18,
2944
- borderBottomRightRadius: mine ? 6 : 18,
2945
- borderBottomLeftRadius: mine ? 18 : 6,
3183
+ backgroundColor: mine ? p.accent : hu.surface,
3184
+ ...mine ? {} : surf,
3185
+ borderRadius: r.bubble,
3186
+ borderBottomRightRadius: mine ? r.tail : r.bubble,
3187
+ borderBottomLeftRadius: mine ? r.bubble : r.tail,
2946
3188
  paddingHorizontal: 14,
2947
3189
  paddingVertical: 9,
2948
3190
  opacity: isPending ? 0.7 : 1
2949
3191
  }
2950
3192
  },
2951
- k.text(m.body, { color: mine ? ink : p.fg })
3193
+ k.text(m.body, { color: mine ? ink : hu.fg })
2952
3194
  ),
2953
3195
  ...(m.articles ?? []).map(
2954
3196
  (a) => (0, import_react2.createElement)(
2955
3197
  RN3.Pressable,
2956
- { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { borderWidth: 1, borderColor: p.line, borderRadius: 12, paddingHorizontal: 10, paddingVertical: 7 } },
3198
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { backgroundColor: p.soft, borderRadius: r.card, paddingHorizontal: 10, paddingVertical: 7 } },
2957
3199
  k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
2958
3200
  )
2959
3201
  ),
@@ -2966,60 +3208,80 @@ var MessengerSheet = ({
2966
3208
  k.text(a.title, { flex: 1 }),
2967
3209
  glyph("next", p.accent, 14)
2968
3210
  );
2969
- 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);
3211
+ const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: raised, borderRadius: r.card, padding: 16, ...surf, ...style } }, ...children);
2970
3212
  const searchRow = (onPress) => (0, import_react2.createElement)(
2971
3213
  RN3.Pressable,
2972
- { key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: 12, paddingHorizontal: 14, backgroundColor: canvas } },
3214
+ { key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: r.field, paddingHorizontal: 14, backgroundColor: hu.def } },
2973
3215
  k.muted("Search for help", { flex: 1, fontSize: 15 }),
2974
3216
  glyph("search", p.accent, 16)
2975
3217
  );
2976
- const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: 12, backgroundColor: raised, ...dark ? {} : lift } });
3218
+ const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: r.field, backgroundColor: raised, ...surf } });
3219
+ const first = client.getUser()?.name?.trim().split(/\s+/)[0];
3220
+ const rest = greeting.replace(/^(hi|hello|hey|hallo)( there)?\s*[!,.]*\s*/i, "").trim() || "How can we help you today?";
3221
+ const hero = (fg, sub) => (0, import_react2.createElement)(
3222
+ RN3.Text,
3223
+ { key: "hero", accessibilityRole: "header", style: { color: fg, fontSize: 25, fontWeight: "600", letterSpacing: -0.6, lineHeight: 30 } },
3224
+ first ? `Hi, ${first} \u{1F44B}
3225
+ ` : "Hi there \u{1F44B}\n",
3226
+ (0, import_react2.createElement)(RN3.Text, { style: { color: sub, fontWeight: "500" } }, rest)
3227
+ );
2977
3228
  const last = thread?.messages.at(-1);
2978
3229
  const showHelp = helpOn && arts.any !== false;
2979
3230
  let content;
2980
- if (view === "home")
2981
- content = (0, import_react2.createElement)(
2982
- RN3.ScrollView,
2983
- { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingBottom: 24 }, keyboardShouldPersistTaps: "handled" },
2984
- (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)),
3231
+ if (view === "home") {
3232
+ const cards = [
3233
+ // "Ask a question": the words go into the chat's field and are sent as the chat opens.
2985
3234
  (0, import_react2.createElement)(
2986
3235
  RN3.View,
2987
- { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } },
2988
- last ? (0, import_react2.createElement)(
2989
- RN3.Pressable,
2990
- { key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
2991
- card([
2992
- k.muted("Recent message", { fontSize: 12, fontWeight: "600", marginBottom: 10 }),
2993
- (0, import_react2.createElement)(
2994
- RN3.View,
2995
- { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 12 } },
2996
- teamMark(title, p.accent, ink),
2997
- (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 })),
2998
- 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)
2999
- )
3000
- ])
3001
- ) : null,
3236
+ { key: "ask", style: { flexDirection: "row", alignItems: "center", gap: 8, height: 48, borderRadius: 24, paddingLeft: 18, paddingRight: 6, backgroundColor: hu.surface, ...surf } },
3237
+ k.field({ value: body, onChange: setBody, placeholder: "Ask a question", max: 4e3, style: { flex: 1, borderWidth: 0, paddingHorizontal: 0, backgroundColor: "transparent" } }),
3002
3238
  (0, import_react2.createElement)(
3003
3239
  RN3.Pressable,
3004
- { key: "send", onPress: () => setView("messages"), accessibilityRole: "button" },
3005
- card(
3006
- [
3007
- (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)),
3008
- (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))
3009
- ],
3010
- { flexDirection: "row", alignItems: "center", gap: 12 }
3240
+ {
3241
+ onPress: () => {
3242
+ setView("messages");
3243
+ if (body.trim()) void send();
3244
+ },
3245
+ accessibilityRole: "button",
3246
+ accessibilityLabel: "Send",
3247
+ style: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: body.trim() ? p.accent : hu.def }
3248
+ },
3249
+ glyph("up", body.trim() ? ink : hu.muted, 18)
3250
+ )
3251
+ ),
3252
+ last ? (0, import_react2.createElement)(
3253
+ RN3.Pressable,
3254
+ { key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
3255
+ card([
3256
+ (0, import_react2.createElement)(
3257
+ RN3.View,
3258
+ { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 10 } },
3259
+ title ? teamMark(title, alpha(p.accent, 0.16), p.accent, 26) : null,
3260
+ (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 })),
3261
+ chat.unseen.length ? (0, import_react2.createElement)(RN3.View, { style: { width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : glyph("next", p.muted, 14)
3011
3262
  )
3012
- ),
3013
- showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
3014
- )
3263
+ ], { paddingVertical: 10, paddingHorizontal: 12 })
3264
+ ) : null,
3265
+ showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
3266
+ ];
3267
+ content = plain ? (0, import_react2.createElement)(
3268
+ RN3.ScrollView,
3269
+ { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 24, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3270
+ (0, import_react2.createElement)(RN3.View, { key: "top", style: { paddingHorizontal: 6, paddingBottom: 10 } }, hero(p.fg, p.muted)),
3271
+ ...cards
3272
+ ) : (0, import_react2.createElement)(
3273
+ RN3.ScrollView,
3274
+ { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingBottom: 24 }, keyboardShouldPersistTaps: "handled" },
3275
+ (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, hero(ink, alpha(ink, 0.8))),
3276
+ (0, import_react2.createElement)(RN3.View, { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } }, ...cards)
3015
3277
  );
3016
- else if (view === "messages")
3278
+ } else if (view === "messages")
3017
3279
  content = (0, import_react2.createElement)(
3018
3280
  RN3.View,
3019
- { key: "messages", style: { flex: 1, backgroundColor: p.bg } },
3281
+ { key: "messages", style: { flex: 1, backgroundColor: hu.bg } },
3020
3282
  (0, import_react2.createElement)(
3021
3283
  RN3.ScrollView,
3022
- { ref: scroll, style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 12 } },
3284
+ { ref: scroll, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 } },
3023
3285
  bubble({ id: "greeting", from: "team", body: greeting, at: "" }),
3024
3286
  !chat.loaded ? k.muted("Loading\u2026") : null,
3025
3287
  ...(thread?.messages ?? []).map((m) => bubble(m)),
@@ -3028,23 +3290,57 @@ var MessengerSheet = ({
3028
3290
  ),
3029
3291
  (0, import_react2.createElement)(
3030
3292
  RN3.View,
3031
- { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 10, gap: 8 } },
3293
+ { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 6, gap: 8 } },
3032
3294
  chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
3033
- 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,
3295
+ 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,
3296
+ // Photos picked for the next message, each with a way to take it out again.
3297
+ photos.length ? (0, import_react2.createElement)(
3298
+ RN3.View,
3299
+ { style: { flexDirection: "row", flexWrap: "wrap", gap: 10, paddingTop: 6, paddingHorizontal: 2 } },
3300
+ ...photos.map(
3301
+ (f) => (0, import_react2.createElement)(
3302
+ RN3.View,
3303
+ { key: f.key, style: { width: 56, height: 56 } },
3304
+ (0, import_react2.createElement)(RN3.Image, { source: { uri: f.uri }, resizeMode: "cover", accessibilityLabel: f.id ? f.name : `${f.name}, uploading`, style: { width: 56, height: 56, borderRadius: Math.max(4, r.card - 4), backgroundColor: p.soft, opacity: f.id ? 1 : 0.5 } }),
3305
+ (0, import_react2.createElement)(
3306
+ RN3.Pressable,
3307
+ {
3308
+ onPress: () => setPhotos((list) => list.filter((x) => x.key !== f.key)),
3309
+ accessibilityRole: "button",
3310
+ accessibilityLabel: `Remove ${f.name}`,
3311
+ hitSlop: 8,
3312
+ style: { position: "absolute", top: -7, right: -7, width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: p.bg, backgroundColor: p.fg, alignItems: "center", justifyContent: "center" }
3313
+ },
3314
+ glyph("cross", p.bg, 10)
3315
+ )
3316
+ )
3317
+ )
3318
+ ) : null,
3319
+ // The message field on top, the paperclip (when the app has expo-image-picker) and the send button under it.
3034
3320
  (0, import_react2.createElement)(
3035
3321
  RN3.View,
3036
- { style: { flexDirection: "row", alignItems: "flex-end", gap: 6, borderWidth: 1, borderColor: p.line, borderRadius: 22, paddingVertical: 5, paddingRight: 5, paddingLeft: 16, backgroundColor: p.bg } },
3037
- 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" } }),
3322
+ { style: { gap: 2, borderRadius: 20, paddingTop: 10, paddingBottom: 6, paddingRight: 6, paddingLeft: 16, backgroundColor: hu.surface, ...surf } },
3323
+ 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" } }),
3038
3324
  (0, import_react2.createElement)(
3039
- RN3.Pressable,
3040
- {
3041
- onPress: () => void send(),
3042
- disabled: !body.trim() || !!pending || !emailOk,
3043
- accessibilityRole: "button",
3044
- accessibilityLabel: "Send",
3045
- style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: !body.trim() || !!pending || !emailOk ? p.line : p.accent }
3046
- },
3047
- glyph("up", !body.trim() || !!pending || !emailOk ? p.muted : ink, 18)
3325
+ RN3.View,
3326
+ { style: { flexDirection: "row", alignItems: "center", marginLeft: -8 } },
3327
+ imagePicker?.launchImageLibraryAsync ? (0, import_react2.createElement)(
3328
+ RN3.Pressable,
3329
+ { onPress: () => void pickPhotos(), disabled: !!pending, accessibilityRole: "button", accessibilityLabel: "Attach a photo or screenshot", hitSlop: 6, style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center" } },
3330
+ glyph("clip", hu.muted, 19)
3331
+ ) : null,
3332
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
3333
+ (0, import_react2.createElement)(
3334
+ RN3.Pressable,
3335
+ {
3336
+ onPress: () => void send(),
3337
+ disabled: !canSend,
3338
+ accessibilityRole: "button",
3339
+ accessibilityLabel: "Send",
3340
+ style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: canSend ? p.accent : hu.def }
3341
+ },
3342
+ glyph("up", canSend ? ink : hu.muted, 18)
3343
+ )
3048
3344
  )
3049
3345
  )
3050
3346
  )
@@ -3054,7 +3350,7 @@ var MessengerSheet = ({
3054
3350
  for (const a of arts.list ?? []) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
3055
3351
  content = (0, import_react2.createElement)(
3056
3352
  RN3.ScrollView,
3057
- { key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3353
+ { key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3058
3354
  search,
3059
3355
  arts.list === null ? k.muted("Loading\u2026") : null,
3060
3356
  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,
@@ -3063,46 +3359,73 @@ var MessengerSheet = ({
3063
3359
  } else
3064
3360
  content = (0, import_react2.createElement)(
3065
3361
  RN3.ScrollView,
3066
- { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { padding: 16, paddingBottom: 32 } },
3362
+ { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 32 } },
3067
3363
  (0, import_react2.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
3068
3364
  );
3069
3365
  const current = typeof view === "string" ? view : "help";
3070
- const tab = (v, name, icon, badge = 0) => (0, import_react2.createElement)(
3366
+ const tab = (v, name, icon, badge = 0) => {
3367
+ const on = current === v;
3368
+ return (0, import_react2.createElement)(
3369
+ RN3.Pressable,
3370
+ {
3371
+ key: v,
3372
+ onPress: () => setView(v),
3373
+ accessibilityRole: "tab",
3374
+ accessibilityState: { selected: on },
3375
+ style: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 6, height: 36, borderRadius: 18, ...on ? { backgroundColor: hu.segment, ...segLift } : {} }
3376
+ },
3377
+ glyph(icon, on ? hu.fg : hu.muted, 17),
3378
+ k.text(name, { fontSize: 13.5, fontWeight: "500", color: on ? hu.fg : hu.muted }),
3379
+ badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 7, right: 12, width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : null
3380
+ );
3381
+ };
3382
+ const onAccent = view === "home" && !plain;
3383
+ const ground = onAccent ? p.accent : hu.bg;
3384
+ const back = (to, label) => (0, import_react2.createElement)(
3071
3385
  RN3.Pressable,
3072
- { key: v, onPress: () => setView(v), accessibilityRole: "tab", accessibilityState: { selected: current === v }, style: { flex: 1, alignItems: "center", paddingTop: 8, paddingBottom: 6, gap: 3 } },
3073
- glyph(icon, current === v ? p.accent : p.muted, 22),
3074
- k.text(name, { fontSize: 12, fontWeight: "600", color: current === v ? p.accent : p.muted }),
3075
- 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
3386
+ { key: "back", onPress: () => setView(to), accessibilityRole: "button", accessibilityLabel: label, hitSlop: 10, style: { width: 32, height: 32, alignItems: "center", justifyContent: "center", transform: [{ rotate: "180deg" }] } },
3387
+ glyph("next", hu.fg, 18)
3076
3388
  );
3077
- const onHome = view === "home";
3078
- const headFg = onHome ? ink : p.fg;
3079
3389
  const head = (0, import_react2.createElement)(
3080
3390
  RN3.View,
3081
- { 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 } },
3082
- 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),
3083
- (0, import_react2.createElement)(
3391
+ { key: "head", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingLeft: 12, paddingRight: 12, paddingTop: 12, paddingBottom: 6, backgroundColor: ground } },
3392
+ 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,
3393
+ // In the chat: who answers, in the middle.
3394
+ view === "messages" && title ? (0, import_react2.createElement)(
3084
3395
  RN3.View,
3085
- { style: { flex: 1, minWidth: 0 } },
3086
- 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),
3087
- view === "messages" ? k.muted(reply, { fontSize: 12 }) : null
3088
- ),
3089
- (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))
3396
+ { style: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8 } },
3397
+ teamMark(title, alpha(p.accent, 0.16), p.accent, 28),
3398
+ (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { flexShrink: 1, color: hu.fg, fontSize: 15, fontWeight: "600" } }, title)
3399
+ ) : (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
3400
+ (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", onAccent ? ink : p.muted, 18))
3090
3401
  );
3402
+ const powered = brand ? (0, import_react2.createElement)(
3403
+ RN3.View,
3404
+ { key: "brand", style: { paddingTop: 2, paddingBottom: 6, alignItems: "center", backgroundColor: hu.bg } },
3405
+ (0, import_react2.createElement)(
3406
+ RN3.Text,
3407
+ { onPress: () => void RN3.Linking.openURL(BRAND_URL).catch(() => void 0), accessibilityRole: "link", hitSlop: 8, style: { color: p.muted, fontSize: 11.5 } },
3408
+ "Powered by ",
3409
+ (0, import_react2.createElement)(RN3.Text, { style: { fontWeight: "600" } }, "MRRocket")
3410
+ )
3411
+ ) : null;
3091
3412
  return (0, import_react2.createElement)(
3092
3413
  RN3.Modal,
3093
3414
  { visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: close },
3094
3415
  (0, import_react2.createElement)(
3095
3416
  RN3.KeyboardAvoidingView,
3096
- { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
3417
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: hu.bg } },
3097
3418
  head,
3098
3419
  content,
3099
- (0, import_react2.createElement)(
3420
+ view === "messages" ? null : (0, import_react2.createElement)(
3100
3421
  RN3.View,
3101
- { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", borderTopWidth: 1, borderTopColor: p.line, paddingHorizontal: 8, paddingBottom: 22, backgroundColor: p.bg } },
3422
+ { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", marginHorizontal: 12, marginTop: 6, marginBottom: 6, padding: 4, borderRadius: 22, backgroundColor: hu.surface } },
3102
3423
  tab("home", "Home", "home"),
3103
- tab("messages", "Messages", "chat", view === "messages" ? 0 : chat.unseen.length),
3424
+ tab("messages", "Messages", "chat", chat.unseen.length),
3104
3425
  showHelp ? tab("help", "Help", "help") : null
3105
- )
3426
+ ),
3427
+ powered,
3428
+ (0, import_react2.createElement)(RN3.View, { style: { height: 16 } })
3106
3429
  )
3107
3430
  );
3108
3431
  };
@@ -3127,6 +3450,9 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3127
3450
  preview: look.preview,
3128
3451
  askEmail: look.askEmail,
3129
3452
  haptics: look.sound,
3453
+ corners: look.corners,
3454
+ header: look.header,
3455
+ logo: look.logo,
3130
3456
  ...defined
3131
3457
  } : given;
3132
3458
  const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
@@ -3155,37 +3481,42 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3155
3481
  const unseen = chat.unseen.length;
3156
3482
  const ink = inkOn(p.accent);
3157
3483
  const label = options.label?.trim() ?? "";
3158
- const side = options.position === "bottom-left" ? { left: 20 } : { right: 20 };
3484
+ const r = CORNERS[options.corners ?? "round"];
3485
+ const [edge, side] = (options.position ?? "bottom-right").split("-");
3486
+ const place = {
3487
+ ...edge === "top" ? { top: 60 } : edge === "bottom" ? { bottom: 34 } : { top: 0, bottom: 0, justifyContent: "center" },
3488
+ ...side === "left" ? { left: 20 } : side === "right" ? { right: 20 } : { left: 0, right: 0, alignItems: "center" }
3489
+ };
3159
3490
  const launcher = options.launcher ? (0, import_react2.createElement)(
3160
- RN3.Pressable,
3161
- {
3162
- key: "launcher",
3163
- onPress: () => openAt(unseen ? "messages" : "home"),
3164
- accessibilityRole: "button",
3165
- accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
3166
- style: {
3167
- position: "absolute",
3168
- ...side,
3169
- bottom: 34,
3170
- height: label ? 50 : 56,
3171
- minWidth: label ? 0 : 56,
3172
- paddingHorizontal: label ? 18 : 0,
3173
- borderRadius: 28,
3174
- flexDirection: "row",
3175
- gap: 8,
3176
- backgroundColor: p.accent,
3177
- alignItems: "center",
3178
- justifyContent: "center",
3179
- shadowColor: p.accent,
3180
- shadowOpacity: 0.4,
3181
- shadowRadius: 12,
3182
- shadowOffset: { width: 0, height: 6 },
3183
- elevation: 6
3184
- }
3185
- },
3186
- glyph("chat", ink, label ? 20 : 26),
3187
- label ? k.text(label, { color: ink, fontWeight: "700" }) : null,
3188
- 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
3491
+ RN3.View,
3492
+ { key: "launcher", pointerEvents: "box-none", style: { position: "absolute", ...place } },
3493
+ (0, import_react2.createElement)(
3494
+ RN3.Pressable,
3495
+ {
3496
+ onPress: () => openAt(unseen ? "messages" : "home"),
3497
+ accessibilityRole: "button",
3498
+ accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
3499
+ style: {
3500
+ height: label ? 50 : 56,
3501
+ minWidth: label ? 0 : 56,
3502
+ paddingHorizontal: label ? 18 : 0,
3503
+ borderRadius: 28,
3504
+ flexDirection: "row",
3505
+ gap: 8,
3506
+ backgroundColor: p.accent,
3507
+ alignItems: "center",
3508
+ justifyContent: "center",
3509
+ shadowColor: p.accent,
3510
+ shadowOpacity: 0.4,
3511
+ shadowRadius: 12,
3512
+ shadowOffset: { width: 0, height: 6 },
3513
+ elevation: 6
3514
+ }
3515
+ },
3516
+ glyph("chat", ink, label ? 20 : 26),
3517
+ label ? k.text(label, { color: ink, fontWeight: "700" }) : null,
3518
+ 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
3519
+ )
3189
3520
  ) : null;
3190
3521
  const toast = banner ? (0, import_react2.createElement)(
3191
3522
  RN3.Pressable,
@@ -3194,15 +3525,25 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3194
3525
  onPress: () => openAt("messages"),
3195
3526
  accessibilityRole: "button",
3196
3527
  accessibilityLiveRegion: "polite",
3197
- 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 }
3528
+ style: {
3529
+ position: "absolute",
3530
+ top: 54,
3531
+ left: 16,
3532
+ right: 16,
3533
+ gap: 3,
3534
+ backgroundColor: p.bg === DARK.bg ? p.soft : p.bg,
3535
+ borderRadius: r.card + 2,
3536
+ paddingHorizontal: 16,
3537
+ paddingVertical: 13,
3538
+ shadowColor: "#101218",
3539
+ shadowOpacity: 0.14,
3540
+ shadowRadius: 18,
3541
+ shadowOffset: { width: 0, height: 8 },
3542
+ elevation: 6
3543
+ }
3198
3544
  },
3199
- teamMark(options.title ?? "Chat with us", p.accent, ink),
3200
- (0, import_react2.createElement)(
3201
- RN3.View,
3202
- { style: { flex: 1, gap: 2 } },
3203
- k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
3204
- (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15 } }, banner.body)
3205
- )
3545
+ k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
3546
+ (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15, lineHeight: 20 } }, banner.body)
3206
3547
  ) : null;
3207
3548
  if (!launcher && !toast && !open) return null;
3208
3549
  return (0, import_react2.createElement)(
@@ -3210,7 +3551,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3210
3551
  { pointerEvents: "box-none", style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 } },
3211
3552
  launcher,
3212
3553
  toast,
3213
- open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView }) : null
3554
+ open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView, brand: look?.branding !== false }) : null
3214
3555
  );
3215
3556
  };
3216
3557
 
@@ -3304,6 +3645,10 @@ var usePaywall = () => {
3304
3645
  const client = useClient();
3305
3646
  return { open: client.openPaywall };
3306
3647
  };
3648
+ var useReview = () => {
3649
+ const client = useClient();
3650
+ return { request: client.requestReview };
3651
+ };
3307
3652
  var useOnboarding = () => {
3308
3653
  const client = useClient();
3309
3654
  return { start: client.startOnboarding };