@mrrocket/sdk 0.18.0 → 0.22.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 (63) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +36 -1
  3. package/dist/api-client.d.ts +28 -4
  4. package/dist/api-client.js +30 -13
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/browser/mrrocket-flows.js +8 -3
  7. package/dist/browser/mrrocket-messenger.js +38 -5
  8. package/dist/browser/mrrocket.js +1 -1
  9. package/dist/browser.d.ts +2 -2
  10. package/dist/browser.js +2 -1
  11. package/dist/browser.js.map +1 -1
  12. package/dist/client.d.ts +17 -1
  13. package/dist/client.js +25 -5
  14. package/dist/client.js.map +1 -1
  15. package/dist/core.d.ts +5 -1
  16. package/dist/core.js +1 -1
  17. package/dist/core.js.map +1 -1
  18. package/dist/feedback.js +161 -119
  19. package/dist/feedback.js.map +1 -1
  20. package/dist/flows-ui.d.ts +1 -1
  21. package/dist/flows-ui.js +100 -72
  22. package/dist/flows-ui.js.map +1 -1
  23. package/dist/flows-web.js +2 -1
  24. package/dist/flows-web.js.map +1 -1
  25. package/dist/flows.d.ts +38 -6
  26. package/dist/flows.js +51 -17
  27. package/dist/flows.js.map +1 -1
  28. package/dist/i18n.d.ts +22 -0
  29. package/dist/i18n.js +74 -0
  30. package/dist/i18n.js.map +1 -0
  31. package/dist/index.d.ts +4 -1
  32. package/dist/index.js +5 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/messenger-ui.d.ts +9 -3
  35. package/dist/messenger-ui.js +324 -152
  36. package/dist/messenger-ui.js.map +1 -1
  37. package/dist/messenger.d.ts +3 -1
  38. package/dist/messenger.js +3 -2
  39. package/dist/messenger.js.map +1 -1
  40. package/dist/onboarding-report.d.ts +19 -0
  41. package/dist/onboarding-report.js +23 -0
  42. package/dist/onboarding-report.js.map +1 -0
  43. package/dist/onboarding-state.d.ts +3 -0
  44. package/dist/onboarding-state.js +7 -1
  45. package/dist/onboarding-state.js.map +1 -1
  46. package/dist/paywall.js +2 -1
  47. package/dist/paywall.js.map +1 -1
  48. package/dist/react-native/index.d.cts +93 -8
  49. package/dist/react-native/index.d.ts +93 -8
  50. package/dist/react-native/index.js +819 -511
  51. package/dist/react-native/index.js.map +1 -1
  52. package/dist/server.d.ts +4 -2
  53. package/dist/server.js +2 -2
  54. package/dist/server.js.map +1 -1
  55. package/dist/styles-css.d.ts +1 -1
  56. package/dist/styles-css.js +35 -2
  57. package/dist/styles-css.js.map +1 -1
  58. package/dist/survey.js +18 -14
  59. package/dist/survey.js.map +1 -1
  60. package/dist/ui.d.ts +2 -0
  61. package/dist/ui.js +15 -12
  62. package/dist/ui.js.map +1 -1
  63. package/package.json +6 -3
@@ -31,7 +31,9 @@ __export(native_exports, {
31
31
  SDK_VERSION: () => SDK_VERSION,
32
32
  activeRouteName: () => activeRouteName,
33
33
  createMRRocket: () => createMRRocket,
34
+ currentLocale: () => currentLocale,
34
35
  getMRRocket: () => getMRRocket,
36
+ setLocale: () => setLocale,
35
37
  useCancelFlow: () => useCancelFlow,
36
38
  useExpoRouterTracking: () => useExpoRouterTracking,
37
39
  useFeedback: () => useFeedback,
@@ -41,6 +43,7 @@ __export(native_exports, {
41
43
  useMessenger: () => useMessenger,
42
44
  useNavigationTracking: () => useNavigationTracking,
43
45
  useOnboarding: () => useOnboarding,
46
+ useOnboardingReport: () => useOnboardingReport,
44
47
  usePaywall: () => usePaywall,
45
48
  usePush: () => usePush,
46
49
  useReview: () => useReview,
@@ -50,8 +53,68 @@ __export(native_exports, {
50
53
  module.exports = __toCommonJS(native_exports);
51
54
  var import_react4 = require("react");
52
55
 
56
+ // src/i18n.ts
57
+ var g = globalThis;
58
+ var s = g.__mrrocketI18n ?? (g.__mrrocketI18n = { version: 0, onWords: /* @__PURE__ */ new Set(), onLocale: /* @__PURE__ */ new Set() });
59
+ var tell = (listeners) => {
60
+ for (const fn of [...listeners]) {
61
+ try {
62
+ fn();
63
+ } catch {
64
+ }
65
+ }
66
+ };
67
+ var t = (word) => s.words?.[word] || word;
68
+ var tf = (word, vars) => t(word).replace(/\{(\w+)\}/g, (all, k) => k in vars ? String(vars[k]) : all);
69
+ var setWords = (words, lang) => {
70
+ const same2 = s.lang === lang && JSON.stringify(s.words ?? {}) === JSON.stringify(words ?? {});
71
+ s.words = words;
72
+ s.lang = lang;
73
+ if (same2) return;
74
+ s.version++;
75
+ tell(s.onWords);
76
+ };
77
+ var tidy = (locale) => {
78
+ const tag = locale?.trim().replace(/_/g, "-");
79
+ if (!tag) return void 0;
80
+ try {
81
+ return Intl.getCanonicalLocales(tag)[0];
82
+ } catch (e) {
83
+ return e instanceof RangeError ? void 0 : tag;
84
+ }
85
+ };
86
+ var setLocale = (locale) => {
87
+ const next = tidy(locale);
88
+ if (next === s.locale) return;
89
+ s.locale = next;
90
+ s.version++;
91
+ tell(s.onWords);
92
+ tell(s.onLocale);
93
+ };
94
+ var setDeviceLocale = (locale) => {
95
+ s.device = tidy(locale);
96
+ };
97
+ var currentLocale = () => s.locale ?? s.device ?? (typeof navigator !== "undefined" && navigator.language || void 0);
98
+ var langParam = () => {
99
+ const l = currentLocale();
100
+ return l ? `&lang=${encodeURIComponent(l)}` : "";
101
+ };
102
+ var wordsVersion = () => s.version;
103
+ var onWords = (fn) => {
104
+ s.onWords.add(fn);
105
+ return () => {
106
+ s.onWords.delete(fn);
107
+ };
108
+ };
109
+ var onLocale = (fn) => {
110
+ s.onLocale.add(fn);
111
+ return () => {
112
+ s.onLocale.delete(fn);
113
+ };
114
+ };
115
+
53
116
  // src/core.ts
54
- var SDK_VERSION = "0.18.0";
117
+ var SDK_VERSION = "0.22.0";
55
118
  var propOf = (person, name) => {
56
119
  const want = name.trim().toLowerCase();
57
120
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -77,7 +140,7 @@ var matchesCondition = (c, person) => {
77
140
  var evaluateFlag = (flag, distinctId, person = {}) => {
78
141
  if (!flag.on) return false;
79
142
  const subject = { id: distinctId, ...person };
80
- const set = flag.sets.length ? flag.sets.find((s) => s.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };
143
+ const set = flag.sets.length ? flag.sets.find((s2) => s2.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };
81
144
  if (!set) return false;
82
145
  if (flagUnit(`${flag.key}:rollout:${distinctId}`) * 100 >= set.rollout) return false;
83
146
  if (!flag.variants?.length) return true;
@@ -108,11 +171,11 @@ var parseQuery = (url) => {
108
171
  const q = url.split("#")[0];
109
172
  const at = q.indexOf("?");
110
173
  if (at < 0) return out;
111
- const dec = (s) => {
174
+ const dec = (s2) => {
112
175
  try {
113
- return decodeURIComponent(s.replace(/\+/g, " "));
176
+ return decodeURIComponent(s2.replace(/\+/g, " "));
114
177
  } catch {
115
- return s;
178
+ return s2;
116
179
  }
117
180
  };
118
181
  for (const pair of q.slice(at + 1).split("&")) {
@@ -175,204 +238,6 @@ var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypt
175
238
 
176
239
  // src/onboarding-state.ts
177
240
  var import_react = require("react");
178
- var none = [];
179
- var useOnboardingItem = (flows, id, start) => {
180
- const [state, setState] = (0, import_react.useState)(() => flows && id ? flows.onboarding.item(id) : null);
181
- (0, import_react.useEffect)(() => {
182
- if (!flows || !id) {
183
- setState(null);
184
- return;
185
- }
186
- const up = () => {
187
- const s = flows.onboarding.item(id);
188
- setState(s);
189
- if (s && !s.done && !s.dismissed) flows.onboarding.seen(id);
190
- };
191
- up();
192
- return flows.subscribe(up);
193
- }, [flows, id]);
194
- return (0, import_react.useMemo)(
195
- () => ({
196
- item: state?.item ?? null,
197
- steps: state?.steps ?? none,
198
- done: !!state?.done,
199
- dismissed: !!state?.dismissed,
200
- eligible: !!state?.eligible,
201
- complete: (stepId) => {
202
- if (flows && id) flows.onboarding.complete(id, stepId);
203
- },
204
- dismiss: () => {
205
- if (flows && id) flows.onboarding.dismissItem(id);
206
- },
207
- answer: (question, value) => {
208
- if (flows && id) flows.onboarding.answerItem(id, question, value);
209
- },
210
- start
211
- }),
212
- [state, flows, id, start]
213
- );
214
- };
215
-
216
- // src/identity.ts
217
- 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 }).";
218
- var warnUnsigned = (status, log = (m) => console.warn(m)) => {
219
- const g = globalThis;
220
- if (status !== 401 || g.__mrrocketUnsigned) return;
221
- g.__mrrocketUnsigned = true;
222
- log(`[MRRocket] ${UNSIGNED_HELP}`);
223
- };
224
- var nextSigned = (signed, id, hash, log = (m) => console.warn(m)) => {
225
- const h3 = typeof hash === "string" ? hash.trim().toLowerCase() : "";
226
- if (/^[0-9a-f]{64}$/.test(h3)) return { id, hash: h3 };
227
- if (h3) log("[MRRocket] identify: that hash is no userHash(id); not sent.");
228
- return signed && signed.id === id ? signed : null;
229
- };
230
- var hashFor = (signed, id) => signed && id && signed.id === id ? signed.hash : void 0;
231
- var hashForAll = (signed, ids) => signed && ids.some((id) => id === signed.id) && ids.every((id) => !id || id === signed.id) ? signed.hash : void 0;
232
-
233
- // src/api-client.ts
234
- var CHAT_FILE_MAX_BYTES = 10 * 1024 * 1024;
235
- var CHAT_FILES_PER_MESSAGE = 4;
236
- var CORNERS = {
237
- round: { panel: 22, bubble: 18, tail: 6, card: 16, field: 20 },
238
- soft: { panel: 14, bubble: 12, tail: 4, card: 10, field: 10 },
239
- square: { panel: 6, bubble: 6, tail: 2, card: 4, field: 4 }
240
- };
241
- var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-by";
242
- var inkOn = (hex) => {
243
- const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
244
- if (!m) return "#ffffff";
245
- const [r, g, b] = [m[1], m[2], m[3]].map((x) => {
246
- const c = parseInt(x, 16) / 255;
247
- return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
248
- });
249
- return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
250
- };
251
- var ApiError = class extends Error {
252
- constructor(message, status) {
253
- super(message);
254
- __publicField(this, "status");
255
- this.status = status;
256
- }
257
- };
258
- var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
259
- var call = async (url, init, signal) => {
260
- let res;
261
- try {
262
- res = await fetch(url, {
263
- ...init,
264
- signal,
265
- // A form (a file upload) sets its own multipart type with the boundary.
266
- headers: typeof init?.body === "string" ? { "content-type": "application/json" } : void 0
267
- });
268
- } catch (e) {
269
- if (e.name === "AbortError") throw e;
270
- throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
271
- }
272
- if (!res.ok) {
273
- warnUnsigned(res.status);
274
- let detail = "";
275
- try {
276
- detail = (await res.json()).error ?? "";
277
- } catch {
278
- }
279
- throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
280
- }
281
- return await res.json();
282
- };
283
- var createApi = (c) => {
284
- const base = `${c.host}/api/sdk`;
285
- const who2 = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
286
- const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
287
- const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
288
- return {
289
- list: (o, signal) => call(`${base}/feedback?${qs({ ...me, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
290
- post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
291
- roadmap: (signal) => call(`${base}/feedback/roadmap?${qs(me)}`, void 0, signal),
292
- changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
293
- submit: (p) => post("/feedback", p),
294
- vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
295
- comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
296
- comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
297
- survey: (url, signal) => call(`${base}/surveys?${qs({ ...me, url })}`, void 0, signal),
298
- seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
299
- respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
300
- chat: (signal) => call(`${base}/messages?${qs(me)}`, void 0, signal),
301
- /** A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). */
302
- sendChat: (m) => post("/messages", m),
303
- /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
304
- uploadChatFile: (file, signal) => {
305
- const form = new FormData();
306
- form.append("key", c.apiKey);
307
- form.append("distinctId", c.distinctId);
308
- if (c.userHash) form.append("userHash", c.userHash);
309
- if ("uri" in file) form.append("file", file);
310
- else form.append("file", file, file.name || "file");
311
- return call(`${base}/messages/attachments`, { method: "POST", body: form }, signal);
312
- },
313
- /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
314
- attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me)}`,
315
- articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
316
- article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
317
- messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
318
- rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
319
- };
320
- };
321
-
322
- // src/batch.ts
323
- var MAX_BATCH = 100;
324
- var MAX_BYTES = 6e4;
325
- var byteLength = (s) => {
326
- let n = 0;
327
- for (let i = 0; i < s.length; i++) {
328
- const c = s.charCodeAt(i);
329
- if (c < 128) n += 1;
330
- else if (c < 2048) n += 2;
331
- else if (c >= 55296 && c <= 56319 && i + 1 < s.length && (s.charCodeAt(i + 1) & 64512) === 56320) {
332
- n += 4;
333
- i++;
334
- } else n += 3;
335
- }
336
- return n;
337
- };
338
- var batchRequests = (key, events, sdk, userHash) => {
339
- const head = `{"key":${JSON.stringify(key)},${userHash ? `"userHash":${JSON.stringify(userHash)},` : ""}"sdk":${JSON.stringify(sdk)},"events":[`;
340
- const empty = byteLength(head) + 2;
341
- const out = [];
342
- let chunk = [];
343
- let items = [];
344
- let size = empty;
345
- const close = () => {
346
- if (chunk.length) out.push({ body: `${head}${chunk.join(",")}]}`, events: items });
347
- chunk = [];
348
- items = [];
349
- size = empty;
350
- };
351
- for (const event of events) {
352
- const json = JSON.stringify(event);
353
- const n = byteLength(json) + 1;
354
- if (empty + n > MAX_BYTES) continue;
355
- if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();
356
- chunk.push(json);
357
- items.push(event);
358
- size += n;
359
- }
360
- close();
361
- return out;
362
- };
363
-
364
- // src/errors.ts
365
- var MAX_STACK = 8 * 1024;
366
- var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
367
- var describeError = (error) => {
368
- if (error instanceof Error) return { type: error.name || "Error", message: clip(error.message || String(error), 1e3), stack: error.stack ? clip(error.stack, MAX_STACK) : void 0 };
369
- if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
370
- try {
371
- return { type: "Error", message: clip(JSON.stringify(error) ?? String(error), 1e3) };
372
- } catch {
373
- return { type: "Error", message: String(error) };
374
- }
375
- };
376
241
 
377
242
  // src/flows.ts
378
243
  var CANCEL_EVENT = "$cancel_flow";
@@ -384,18 +249,20 @@ var PLAIN_CANCEL = {
384
249
  question: "",
385
250
  reasons: [],
386
251
  offers: {},
387
- confirm: { title: "Cancel your subscription?", body: "You keep access until the end of the billing period.", button: "Cancel subscription" },
252
+ // Read when it is shown, so it follows the visitor's language.
253
+ get confirm() {
254
+ return { title: t("Cancel your subscription?"), body: t("You keep access until the end of the billing period."), button: t("Cancel subscription") };
255
+ },
388
256
  offersFor: "business"
389
257
  };
390
- var FAILED = "Something went wrong. Please try again.";
391
258
  var PROGRESS_KEY = "mrrocket:onboarding";
392
- var eventSlug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
259
+ var eventSlug = (s2) => s2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
393
260
  var SIGNUP = /* @__PURE__ */ new Set(["signed_up", "signup", "sign_up", "registered", "account_created", "user_signed_up"]);
394
261
  var matchesTrigger = (trigger, event) => {
395
- const t = eventSlug(trigger);
262
+ const t2 = eventSlug(trigger);
396
263
  const e = eventSlug(event);
397
- if (!t || !e) return false;
398
- return t === e || SIGNUP.has(t) && SIGNUP.has(e);
264
+ if (!t2 || !e) return false;
265
+ return t2 === e || SIGNUP.has(t2) && SIGNUP.has(e);
399
266
  };
400
267
  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());
401
268
  var isPageTrigger = (trigger) => trigger.trim().startsWith("/");
@@ -404,16 +271,30 @@ var momentOf = (f) => f.when ?? f.trigger;
404
271
  var manual = (trigger) => eventSlug(trigger) === "from_a_checklist" || eventSlug(trigger) === "from_your_code" || eventSlug(trigger) === eventSlug(NEEDS_NEWER);
405
272
  var AFTER_PREFIX = "after:";
406
273
  var afterOf = (trigger) => {
407
- const t = (trigger ?? "").trim();
408
- return t.toLowerCase().startsWith(AFTER_PREFIX) ? t.slice(AFTER_PREFIX.length).trim() || null : null;
274
+ const t2 = (trigger ?? "").trim();
275
+ return t2.toLowerCase().startsWith(AFTER_PREFIX) ? t2.slice(AFTER_PREFIX.length).trim() || null : null;
409
276
  };
410
277
  var opensOf = (step) => step.opens ?? (step.startsTour ? { kind: "tour", id: step.startsTour } : void 0);
278
+ var OTHER_PREFIX = "other:";
279
+ var scaleOf = (q) => {
280
+ const min = q?.min ?? 0;
281
+ const max = q?.max ?? 10;
282
+ return Array.from({ length: Math.max(0, Math.min(max - min, 20) + 1) }, (_, i) => min + i);
283
+ };
411
284
  var answerOk = (q, value) => {
412
285
  if (!q || value == null) return false;
413
- if (q.type === "choice") return (q.options ?? []).some((o) => o.id === value);
286
+ if (q.type === "choice") {
287
+ if (q.other && value.startsWith(OTHER_PREFIX)) return value.slice(OTHER_PREFIX.length).trim().length > 0;
288
+ return (q.options ?? []).some((o) => o.id === value);
289
+ }
290
+ if (q.type === "scale") return scaleOf(q).some((n) => String(n) === value);
414
291
  return value.trim().length > 0 && value.length <= 500;
415
292
  };
416
- var answerLabel = (q, value) => q?.type === "choice" ? q.options?.find((o) => o.id === value)?.label ?? value : value.trim();
293
+ var answerLabel = (q, value) => {
294
+ if (q?.type !== "choice") return value.trim();
295
+ if (q.other && value.startsWith(OTHER_PREFIX)) return value.slice(OTHER_PREFIX.length).trim();
296
+ return q.options?.find((o) => o.id === value)?.label ?? value;
297
+ };
417
298
  var videoEmbed = (url) => {
418
299
  const u = (url ?? "").trim();
419
300
  if (!/^https?:\/\//i.test(u)) return null;
@@ -435,12 +316,14 @@ var runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toS
435
316
  var discounted = (amount, percent) => Math.round(amount * (1 - percent / 100) * 100) / 100;
436
317
  var money = (amount, currency) => {
437
318
  try {
438
- return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
319
+ return new Intl.NumberFormat(currentLocale(), { style: "currency", currency }).format(amount);
439
320
  } catch {
440
321
  return `${amount.toFixed(2)} ${currency}`;
441
322
  }
442
323
  };
443
- var offerButton = (o) => o.kind === "contact" ? "Send to the team" : o.kind === "pause" ? "Pause instead" : o.kind === "downgrade" ? "Switch plan" : "Accept offer";
324
+ var offerButton = (o) => o.kind === "contact" ? t("Send to the team") : o.kind === "pause" ? t("Pause instead") : o.kind === "downgrade" ? t("Switch plan") : t("Accept offer");
325
+ var monthsWord = (n) => n === 1 ? t("1 month") : tf("{n} months", { n });
326
+ var forMonths = (n) => tf("for {months}", { months: monthsWord(n) });
444
327
  var safeUrl = (url) => {
445
328
  const u = (url ?? "").trim();
446
329
  return /^https?:\/\//i.test(u) || /^\/(?!\/)/.test(u) ? u : void 0;
@@ -587,13 +470,13 @@ var createFlows = (env) => {
587
470
  }
588
471
  };
589
472
  const merge = (state) => {
590
- for (const [id, s] of Object.entries(state ?? {})) {
473
+ for (const [id, s2] of Object.entries(state ?? {})) {
591
474
  const p = prog(id);
592
- for (const d of s.done ?? []) p.done.add(d);
593
- if (s.dismissed) p.dismissed = true;
594
- for (const [k, v] of Object.entries(s.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === "yes");
595
- for (const [k, v] of Object.entries(s.at ?? {})) if (typeof v === "number" && !p.at.has(k)) p.at.set(k, v);
596
- for (const [k, v] of Object.entries(s.reached ?? {})) if (typeof v === "number" && (!p.reached.has(k) || v < p.reached.get(k))) p.reached.set(k, v);
475
+ for (const d of s2.done ?? []) p.done.add(d);
476
+ if (s2.dismissed) p.dismissed = true;
477
+ for (const [k, v] of Object.entries(s2.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === "yes");
478
+ for (const [k, v] of Object.entries(s2.at ?? {})) if (typeof v === "number" && !p.at.has(k)) p.at.set(k, v);
479
+ for (const [k, v] of Object.entries(s2.reached ?? {})) if (typeof v === "number" && (!p.reached.has(k) || v < p.reached.get(k))) p.reached.set(k, v);
597
480
  }
598
481
  };
599
482
  const wakeAt = (until) => {
@@ -633,14 +516,14 @@ var createFlows = (env) => {
633
516
  return {
634
517
  type: "checklist",
635
518
  flow,
636
- items: items.map((s) => {
637
- const o = opensOf(s);
638
- const t = o && (o.kind === "tour" || o.kind === "modal") && o.id !== flow.id ? flowById(o.id) : void 0;
639
- const target = t && t.shownBy !== "code" && nodesOf(t).length ? t : void 0;
519
+ items: items.map((s2) => {
520
+ const o = opensOf(s2);
521
+ const t2 = o && (o.kind === "tour" || o.kind === "modal") && o.id !== flow.id ? flowById(o.id) : void 0;
522
+ const target = t2 && t2.shownBy !== "code" && nodesOf(t2).length ? t2 : void 0;
640
523
  const opens = o && (o.kind === "page" ? !!o.target.trim() : o.kind === "code" || !!target) ? o : void 0;
641
524
  return {
642
- step: s,
643
- done: p.done.has(s.id),
525
+ step: s2,
526
+ done: p.done.has(s2.id),
644
527
  ...target && o?.kind === "tour" ? { tour: target } : {},
645
528
  ...opens ? { opens } : {},
646
529
  ...target ? { target } : {}
@@ -696,7 +579,7 @@ var createFlows = (env) => {
696
579
  active = null;
697
580
  const list = back ? flowById(back.flow) : void 0;
698
581
  if (back && list) {
699
- const item = stepsIn(nodesOf(list)).find((s) => s.id === back.step);
582
+ const item = stepsIn(nodesOf(list)).find((s2) => s2.id === back.step);
700
583
  const p = prog(list.id);
701
584
  if (item && !item.event && !progress.get(ended2)?.dismissed && !p.done.has(item.id)) {
702
585
  markDone(p, item.id);
@@ -739,8 +622,8 @@ var createFlows = (env) => {
739
622
  };
740
623
  const allowed = (f) => f.audience === "answered" ? !!f.answered && answers.get(f.answered.question) === f.answered.option : f.audience !== "signed_in" || !!env.personId();
741
624
  const predecessorEnded = (id) => {
742
- const g = flowById(id);
743
- return !g || finished(g);
625
+ const g2 = flowById(id);
626
+ return !g2 || finished(g2);
744
627
  };
745
628
  const due = (f, trigger = momentOf(f)) => {
746
629
  const path = env.path?.();
@@ -775,7 +658,7 @@ var createFlows = (env) => {
775
658
  };
776
659
  const answerStep = (flow, step, raw) => {
777
660
  const q = step.question;
778
- const value = q.type === "text" ? raw.trim().slice(0, 500) : raw;
661
+ const value = q.type === "text" ? raw.trim().slice(0, 500) : raw.startsWith(OTHER_PREFIX) ? OTHER_PREFIX + raw.slice(OTHER_PREFIX.length).trim().slice(0, 490) : raw;
779
662
  answers.set(q.key, value);
780
663
  drafts.delete(step.id);
781
664
  markDone(prog(flow.id), step.id);
@@ -886,7 +769,7 @@ var createFlows = (env) => {
886
769
  () => finish(c, r),
887
770
  () => {
888
771
  c.busy = false;
889
- c.error = FAILED;
772
+ c.error = t("Something went wrong. Please try again.");
890
773
  emit();
891
774
  }
892
775
  );
@@ -923,11 +806,11 @@ var createFlows = (env) => {
923
806
  for (const l of lists) {
924
807
  const p = prog(l.id);
925
808
  let here = false;
926
- for (const s of stepsIn(nodesOf(l)))
927
- if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
928
- markDone(p, s.id);
809
+ for (const s2 of stepsIn(nodesOf(l)))
810
+ if (s2.kind === "checklist" && s2.event && !p.done.has(s2.id) && matchesTrigger(s2.event, name)) {
811
+ markDone(p, s2.id);
929
812
  here = true;
930
- env.track(ONBOARDING_EVENT, { flow: l.id, step: s.id, kind: s.kind, action: "completed" });
813
+ env.track(ONBOARDING_EVENT, { flow: l.id, step: s2.id, kind: s2.kind, action: "completed" });
931
814
  }
932
815
  if (here) {
933
816
  ticked = true;
@@ -966,8 +849,8 @@ var createFlows = (env) => {
966
849
  answer: (text2) => {
967
850
  const c = cancel;
968
851
  if (!c || c.busy || c.screen !== "followup") return;
969
- const t = text2.slice(0, 500);
970
- c.answer = t.trim() ? t : void 0;
852
+ const t2 = text2.slice(0, 500);
853
+ c.answer = t2.trim() ? t2 : void 0;
971
854
  emit();
972
855
  },
973
856
  next: () => {
@@ -1109,11 +992,22 @@ var createFlows = (env) => {
1109
992
  drafts.set(v.step.id, String(value).slice(0, 500));
1110
993
  emit();
1111
994
  },
995
+ skip: () => {
996
+ const v = onboardingView();
997
+ const q = v?.type === "step" && v.step.kind === "question" ? v.step.question : void 0;
998
+ if (v?.type !== "step" || !q?.optional) return;
999
+ drafts.delete(v.step.id);
1000
+ markDone(prog(v.flow.id), v.step.id);
1001
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: "question", action: "completed", question: q.key, skipped: true });
1002
+ celebrate(v.flow);
1003
+ save();
1004
+ emit();
1005
+ },
1112
1006
  back: () => {
1113
1007
  const v = onboardingView();
1114
1008
  if (v?.type !== "step" || v.step.kind !== "question" || !v.back) return;
1115
1009
  const line = stepsIn(nodesOf(v.flow));
1116
- const at = line.findIndex((s) => s.id === v.step.id);
1010
+ const at = line.findIndex((s2) => s2.id === v.step.id);
1117
1011
  const p = prog(v.flow.id);
1118
1012
  for (let i = at - 1; i >= 0; i--)
1119
1013
  if (p.done.has(line[i].id)) {
@@ -1128,11 +1022,11 @@ var createFlows = (env) => {
1128
1022
  const f = flowById(id);
1129
1023
  if (!f) return null;
1130
1024
  const p = progress.get(f.id);
1131
- const steps = stepsIn(nodesOf(f)).map((s) => {
1132
- const a = s.question ? answers.get(s.question.key) : void 0;
1133
- return { ...s, done: !!p?.done.has(s.id), ...a !== void 0 ? { answer: a } : {} };
1025
+ const steps = stepsIn(nodesOf(f)).map((s2) => {
1026
+ const a = s2.question ? answers.get(s2.question.key) : void 0;
1027
+ return { ...s2, done: !!p?.done.has(s2.id), ...a !== void 0 ? { answer: a } : {} };
1134
1028
  });
1135
- const done = steps.length > 0 && steps.every((s) => s.done);
1029
+ const done = steps.length > 0 && steps.every((s2) => s2.done);
1136
1030
  const dismissed = !!p?.dismissed;
1137
1031
  return { item: f, steps, done, dismissed, eligible: !done && !dismissed && allowed(f) && due(f) };
1138
1032
  },
@@ -1144,21 +1038,21 @@ var createFlows = (env) => {
1144
1038
  env.track(ONBOARDING_EVENT, { flow: f.id, action: "started" });
1145
1039
  }
1146
1040
  const p = progress.get(f.id);
1147
- for (const s of stepsIn(nodesOf(f)))
1148
- if (!p?.done.has(s.id) && !shown.has(`s:${s.id}`)) {
1149
- shown.add(`s:${s.id}`);
1150
- env.track(ONBOARDING_EVENT, { flow: f.id, step: s.id, kind: s.kind, action: "shown" });
1151
- if (s.kind !== "checklist") break;
1041
+ for (const s2 of stepsIn(nodesOf(f)))
1042
+ if (!p?.done.has(s2.id) && !shown.has(`s:${s2.id}`)) {
1043
+ shown.add(`s:${s2.id}`);
1044
+ env.track(ONBOARDING_EVENT, { flow: f.id, step: s2.id, kind: s2.kind, action: "shown" });
1045
+ if (s2.kind !== "checklist") break;
1152
1046
  }
1153
1047
  },
1154
1048
  complete: (id, stepId) => {
1155
1049
  const f = flowById(id);
1156
- const s = f && stepsIn(nodesOf(f)).find((x) => x.id === stepId);
1157
- if (!f || !s || s.kind === "question") return;
1050
+ const s2 = f && stepsIn(nodesOf(f)).find((x) => x.id === stepId);
1051
+ if (!f || !s2 || s2.kind === "question") return;
1158
1052
  const p = prog(f.id);
1159
- if (p.done.has(s.id)) return;
1160
- markDone(p, s.id);
1161
- env.track(ONBOARDING_EVENT, { flow: f.id, step: s.id, kind: s.kind, action: "completed" });
1053
+ if (p.done.has(s2.id)) return;
1054
+ markDone(p, s2.id);
1055
+ env.track(ONBOARDING_EVENT, { flow: f.id, step: s2.id, kind: s2.kind, action: "completed" });
1162
1056
  ended(f);
1163
1057
  },
1164
1058
  dismissItem: (id) => {
@@ -1173,9 +1067,9 @@ var createFlows = (env) => {
1173
1067
  },
1174
1068
  answerItem: (id, question, value) => {
1175
1069
  const f = flowById(id);
1176
- const s = f && stepsIn(nodesOf(f)).find((x) => x.kind === "question" && (x.id === question || x.question?.key === question));
1177
- if (!f || !s?.question || !answerOk(s.question, value)) return;
1178
- answerStep(f, s, value);
1070
+ const s2 = f && stepsIn(nodesOf(f)).find((x) => x.kind === "question" && (x.id === question || x.question?.key === question));
1071
+ if (!f || !s2?.question || !answerOk(s2.question, value)) return;
1072
+ answerStep(f, s2, value);
1179
1073
  ended(f);
1180
1074
  }
1181
1075
  },
@@ -1204,6 +1098,237 @@ var createFlows = (env) => {
1204
1098
  return controller;
1205
1099
  };
1206
1100
 
1101
+ // src/onboarding-report.ts
1102
+ var KIND = { questions: "question", checklist: "checklist", tour: "tour" };
1103
+ var createOnboardingReport = (track, name, options = {}) => {
1104
+ const type = options.type ?? "questions";
1105
+ const kind = KIND[type] ?? "question";
1106
+ const seen = /* @__PURE__ */ new Set();
1107
+ const send = (props) => track(ONBOARDING_EVENT, { code: eventSlug(name), name, type, ...options.order?.length ? { order: options.order } : {}, ...props });
1108
+ return {
1109
+ saw: (step, title) => {
1110
+ if (seen.has(step)) return;
1111
+ seen.add(step);
1112
+ send({ step, title, kind, action: "shown" });
1113
+ },
1114
+ answered: (question, answer, label) => send({ step: question, kind: "question", action: "completed", question, answer, label: label ?? answer }),
1115
+ done: (step) => send({ step, kind, action: "completed" }),
1116
+ closed: (step) => send({ ...step ? { step } : {}, kind, action: "dismissed" })
1117
+ };
1118
+ };
1119
+
1120
+ // src/onboarding-state.ts
1121
+ var none = [];
1122
+ var useOnboardingItem = (flows, id, start) => {
1123
+ const [state, setState] = (0, import_react.useState)(() => flows && id ? flows.onboarding.item(id) : null);
1124
+ (0, import_react.useEffect)(() => {
1125
+ if (!flows || !id) {
1126
+ setState(null);
1127
+ return;
1128
+ }
1129
+ const up = () => {
1130
+ const s2 = flows.onboarding.item(id);
1131
+ setState(s2);
1132
+ if (s2 && !s2.done && !s2.dismissed) flows.onboarding.seen(id);
1133
+ };
1134
+ up();
1135
+ return flows.subscribe(up);
1136
+ }, [flows, id]);
1137
+ return (0, import_react.useMemo)(
1138
+ () => ({
1139
+ item: state?.item ?? null,
1140
+ steps: state?.steps ?? none,
1141
+ done: !!state?.done,
1142
+ dismissed: !!state?.dismissed,
1143
+ eligible: !!state?.eligible,
1144
+ complete: (stepId) => {
1145
+ if (flows && id) flows.onboarding.complete(id, stepId);
1146
+ },
1147
+ dismiss: () => {
1148
+ if (flows && id) flows.onboarding.dismissItem(id);
1149
+ },
1150
+ answer: (question, value) => {
1151
+ if (flows && id) flows.onboarding.answerItem(id, question, value);
1152
+ },
1153
+ start
1154
+ }),
1155
+ [state, flows, id, start]
1156
+ );
1157
+ };
1158
+ var useReportWith = (track, name, options) => {
1159
+ const key = JSON.stringify(options ?? null);
1160
+ return (0, import_react.useMemo)(() => createOnboardingReport(track, name, options), [track, name, key]);
1161
+ };
1162
+
1163
+ // src/identity.ts
1164
+ 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 }).";
1165
+ var warnUnsigned = (status, log = (m) => console.warn(m)) => {
1166
+ const g2 = globalThis;
1167
+ if (status !== 401 || g2.__mrrocketUnsigned) return;
1168
+ g2.__mrrocketUnsigned = true;
1169
+ log(`[MRRocket] ${UNSIGNED_HELP}`);
1170
+ };
1171
+ var nextSigned = (signed, id, hash, log = (m) => console.warn(m)) => {
1172
+ const h3 = typeof hash === "string" ? hash.trim().toLowerCase() : "";
1173
+ if (/^[0-9a-f]{64}$/.test(h3)) return { id, hash: h3 };
1174
+ if (h3) log("[MRRocket] identify: that hash is no userHash(id); not sent.");
1175
+ return signed && signed.id === id ? signed : null;
1176
+ };
1177
+ var hashFor = (signed, id) => signed && id && signed.id === id ? signed.hash : void 0;
1178
+ var hashForAll = (signed, ids) => signed && ids.some((id) => id === signed.id) && ids.every((id) => !id || id === signed.id) ? signed.hash : void 0;
1179
+
1180
+ // src/api-client.ts
1181
+ var CHAT_FILE_MAX_BYTES = 10 * 1024 * 1024;
1182
+ var CHAT_FILES_PER_MESSAGE = 4;
1183
+ var CORNERS = {
1184
+ round: { panel: 22, bubble: 18, tail: 6, card: 16, field: 20 },
1185
+ soft: { panel: 14, bubble: 12, tail: 4, card: 10, field: 10 },
1186
+ square: { panel: 6, bubble: 6, tail: 2, card: 4, field: 4 }
1187
+ };
1188
+ var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-by";
1189
+ var inkOn = (hex) => {
1190
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
1191
+ if (!m) return "#ffffff";
1192
+ const [r, g2, b] = [m[1], m[2], m[3]].map((x) => {
1193
+ const c = parseInt(x, 16) / 255;
1194
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
1195
+ });
1196
+ return 0.2126 * r + 0.7152 * g2 + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
1197
+ };
1198
+ var ApiError = class extends Error {
1199
+ constructor(message, status) {
1200
+ super(message);
1201
+ __publicField(this, "status");
1202
+ this.status = status;
1203
+ }
1204
+ };
1205
+ var qs = (params, lang = currentLocale()) => Object.entries({ ...params, lang }).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
1206
+ var withLang = (url) => {
1207
+ const q = qs({});
1208
+ return q ? `${url}?${q}` : url;
1209
+ };
1210
+ var call = async (url, init, signal) => {
1211
+ let res;
1212
+ try {
1213
+ res = await fetch(url, {
1214
+ ...init,
1215
+ signal,
1216
+ // A form (a file upload) sets its own multipart type with the boundary.
1217
+ headers: typeof init?.body === "string" ? { "content-type": "application/json" } : void 0
1218
+ });
1219
+ } catch (e) {
1220
+ if (e.name === "AbortError") throw e;
1221
+ throw new ApiError(t("Could not reach the server. Check your connection and try again."), 0);
1222
+ }
1223
+ if (!res.ok) {
1224
+ warnUnsigned(res.status);
1225
+ let detail = "";
1226
+ try {
1227
+ detail = (await res.json()).error ?? "";
1228
+ } catch {
1229
+ }
1230
+ throw new ApiError(detail || (res.status === 429 ? t("Too many requests. Try again in a minute.") : t("Something went wrong. Please try again.")), res.status);
1231
+ }
1232
+ return await res.json();
1233
+ };
1234
+ var createApi = (c) => {
1235
+ const base = `${c.host}/api/sdk`;
1236
+ const who2 = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
1237
+ const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
1238
+ const post = (path, body, signal) => call(withLang(`${base}${path}`), { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
1239
+ return {
1240
+ list: (o, signal) => call(`${base}/feedback?${qs({ ...me, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
1241
+ post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
1242
+ roadmap: (signal) => call(`${base}/feedback/roadmap?${qs(me)}`, void 0, signal),
1243
+ changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
1244
+ submit: (p) => post("/feedback", p),
1245
+ vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
1246
+ comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
1247
+ comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
1248
+ survey: (url, signal) => call(`${base}/surveys?${qs({ ...me, url })}`, void 0, signal),
1249
+ seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
1250
+ respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
1251
+ /** One chat by id, else the visitor's newest; `conversations` lists them all. */
1252
+ chat: (signal, conversation) => call(`${base}/messages?${qs({ ...me, conversation })}`, void 0, signal),
1253
+ /**
1254
+ * A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). `conversation` is
1255
+ * the chat it answers, or "new" to start one; without it, the running chat or else a new one.
1256
+ */
1257
+ sendChat: (m) => post("/messages", m),
1258
+ /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
1259
+ uploadChatFile: (file, signal) => {
1260
+ const form = new FormData();
1261
+ form.append("key", c.apiKey);
1262
+ form.append("distinctId", c.distinctId);
1263
+ if (c.userHash) form.append("userHash", c.userHash);
1264
+ if ("uri" in file) form.append("file", file);
1265
+ else form.append("file", file, file.name || "file");
1266
+ return call(withLang(`${base}/messages/attachments`), { method: "POST", body: form }, signal);
1267
+ },
1268
+ /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
1269
+ attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me, "")}`,
1270
+ articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
1271
+ article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
1272
+ messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
1273
+ rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
1274
+ };
1275
+ };
1276
+
1277
+ // src/batch.ts
1278
+ var MAX_BATCH = 100;
1279
+ var MAX_BYTES = 6e4;
1280
+ var byteLength = (s2) => {
1281
+ let n = 0;
1282
+ for (let i = 0; i < s2.length; i++) {
1283
+ const c = s2.charCodeAt(i);
1284
+ if (c < 128) n += 1;
1285
+ else if (c < 2048) n += 2;
1286
+ else if (c >= 55296 && c <= 56319 && i + 1 < s2.length && (s2.charCodeAt(i + 1) & 64512) === 56320) {
1287
+ n += 4;
1288
+ i++;
1289
+ } else n += 3;
1290
+ }
1291
+ return n;
1292
+ };
1293
+ var batchRequests = (key, events, sdk, userHash) => {
1294
+ const head = `{"key":${JSON.stringify(key)},${userHash ? `"userHash":${JSON.stringify(userHash)},` : ""}"sdk":${JSON.stringify(sdk)},"events":[`;
1295
+ const empty = byteLength(head) + 2;
1296
+ const out = [];
1297
+ let chunk = [];
1298
+ let items = [];
1299
+ let size = empty;
1300
+ const close = () => {
1301
+ if (chunk.length) out.push({ body: `${head}${chunk.join(",")}]}`, events: items });
1302
+ chunk = [];
1303
+ items = [];
1304
+ size = empty;
1305
+ };
1306
+ for (const event of events) {
1307
+ const json = JSON.stringify(event);
1308
+ const n = byteLength(json) + 1;
1309
+ if (empty + n > MAX_BYTES) continue;
1310
+ if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();
1311
+ chunk.push(json);
1312
+ items.push(event);
1313
+ size += n;
1314
+ }
1315
+ close();
1316
+ return out;
1317
+ };
1318
+
1319
+ // src/errors.ts
1320
+ var MAX_STACK = 8 * 1024;
1321
+ var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
1322
+ var describeError = (error) => {
1323
+ if (error instanceof Error) return { type: error.name || "Error", message: clip(error.message || String(error), 1e3), stack: error.stack ? clip(error.stack, MAX_STACK) : void 0 };
1324
+ if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
1325
+ try {
1326
+ return { type: "Error", message: clip(JSON.stringify(error) ?? String(error), 1e3) };
1327
+ } catch {
1328
+ return { type: "Error", message: String(error) };
1329
+ }
1330
+ };
1331
+
1207
1332
  // src/paywall.ts
1208
1333
  var PAYWALL_EVENT = "$paywall";
1209
1334
  var paywallPages = (config, w, distinctId) => {
@@ -1263,7 +1388,7 @@ var createPaywallHost = (env) => {
1263
1388
  },
1264
1389
  () => {
1265
1390
  c.busy = false;
1266
- c.error = "The purchase did not go through. Please try again.";
1391
+ c.error = t("The purchase did not go through. Please try again.");
1267
1392
  emit();
1268
1393
  }
1269
1394
  );
@@ -1338,10 +1463,10 @@ var RageDetector = class {
1338
1463
  __publicField(this, "clicks", []);
1339
1464
  __publicField(this, "fired", false);
1340
1465
  }
1341
- push(x, y, t) {
1342
- const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
1466
+ push(x, y, t2) {
1467
+ const near = this.clicks.filter((c) => t2 - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
1343
1468
  if (near.length !== this.clicks.length) this.fired = false;
1344
- this.clicks = [...near, { x, y, t }];
1469
+ this.clicks = [...near, { x, y, t: t2 }];
1345
1470
  if (this.clicks.length >= this.count && !this.fired) {
1346
1471
  this.fired = true;
1347
1472
  return true;
@@ -1357,9 +1482,9 @@ var BUFFER_MS = 6e4;
1357
1482
  var CHUNK_FRAMES = 12;
1358
1483
  var CHUNK_CHARS = 7e5;
1359
1484
  var MAX_MS = 30 * 6e4;
1360
- var shareOf = (s) => {
1485
+ var shareOf = (s2) => {
1361
1486
  let h3 = 2166136261;
1362
- for (let i = 0; i < s.length; i++) h3 = Math.imul(h3 ^ s.charCodeAt(i), 16777619);
1487
+ for (let i = 0; i < s2.length; i++) h3 = Math.imul(h3 ^ s2.charCodeAt(i), 16777619);
1363
1488
  return (h3 >>> 0) / 4294967296;
1364
1489
  };
1365
1490
  var createRecorder = (deps, st) => {
@@ -1484,14 +1609,14 @@ var measureMasks = () => Promise.all(
1484
1609
  (ref) => new Promise((resolve) => {
1485
1610
  const v = ref.current;
1486
1611
  if (!v) return resolve("none");
1487
- const t = setTimeout(() => resolve("failed"), 100);
1612
+ const t2 = setTimeout(() => resolve("failed"), 100);
1488
1613
  try {
1489
1614
  v.measureInWindow((x, y, w, h3) => {
1490
- clearTimeout(t);
1615
+ clearTimeout(t2);
1491
1616
  resolve(w > 0 && h3 > 0 ? [x, y, w, h3] : "none");
1492
1617
  });
1493
1618
  } catch {
1494
- clearTimeout(t);
1619
+ clearTimeout(t2);
1495
1620
  resolve("failed");
1496
1621
  }
1497
1622
  })
@@ -1528,7 +1653,7 @@ var MAX_CRUMBS = 20;
1528
1653
  var MAX_BACKOFF_MS = 5 * 60 * 1e3;
1529
1654
  var REQUEST_TIMEOUT_MS = 15e3;
1530
1655
  var EAGER_COUNT = 20;
1531
- var clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1656
+ var clip2 = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
1532
1657
  var createClient = (options, deps) => {
1533
1658
  const apiKey = (options.apiKey ?? "").trim();
1534
1659
  const host = (options.host ?? DEFAULT_HOST).trim().replace(/\/+$/, "");
@@ -1556,6 +1681,8 @@ var createClient = (options, deps) => {
1556
1681
  context = deps.context();
1557
1682
  } catch {
1558
1683
  }
1684
+ setDeviceLocale(context.locale);
1685
+ if (options.locale) setLocale(options.locale);
1559
1686
  const appId = (deps.appId || "app").replace(/[^A-Za-z0-9._-]/g, "") || "app";
1560
1687
  const release = options.release ?? (context.appVersion ? `${context.appVersion}${context.build ? ` (${context.build})` : ""}` : void 0);
1561
1688
  const sdk = { v: SDK_VERSION, errors: errorsOn, platform: context.platform };
@@ -1601,7 +1728,7 @@ var createClient = (options, deps) => {
1601
1728
  const fetchInApp = async (id) => {
1602
1729
  if (!apiKey) return null;
1603
1730
  const hash = hashOf(id);
1604
- const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}${hash ? `&userHash=${hash}` : ""}`, { method: "GET" });
1731
+ const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}${hash ? `&userHash=${hash}` : ""}${langParam()}`, { method: "GET" });
1605
1732
  warnUnsigned(r.status, deps.log);
1606
1733
  return r.ok ? await r.json() : null;
1607
1734
  };
@@ -1687,9 +1814,9 @@ var createClient = (options, deps) => {
1687
1814
  const sendId = () => consent === "persistent" ? distinctId() : identified;
1688
1815
  const hashOf = (id) => hashFor(signed, id);
1689
1816
  const touchSession = () => {
1690
- const t = now();
1691
- if (!session || t - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t };
1692
- else session.last = t;
1817
+ const t2 = now();
1818
+ if (!session || t2 - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t2 };
1819
+ else session.last = t2;
1693
1820
  return session.id;
1694
1821
  };
1695
1822
  const urlOf = (path = currentScreen) => `app://${appId}${path ?? "/"}`;
@@ -1878,7 +2005,7 @@ var createClient = (options, deps) => {
1878
2005
  const fetchConfig = async () => {
1879
2006
  if (!apiKey) return;
1880
2007
  try {
1881
- const r = await deps.fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}`, { method: "GET" });
2008
+ const r = await deps.fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}${langParam()}`, { method: "GET" });
1882
2009
  if (!r.ok) {
1883
2010
  warn(`Config request answered ${r.status}. Check the public key and the host.`);
1884
2011
  return;
@@ -1886,6 +2013,7 @@ var createClient = (options, deps) => {
1886
2013
  const c = await r.json();
1887
2014
  if (c && Array.isArray(c.experiments)) {
1888
2015
  config = c;
2016
+ setWords(c.words, c.lang);
1889
2017
  if (consent !== "off") await set(KEYS.config, JSON.stringify(c));
1890
2018
  changed();
1891
2019
  }
@@ -2004,7 +2132,10 @@ var createClient = (options, deps) => {
2004
2132
  }
2005
2133
  try {
2006
2134
  const c = cached ? JSON.parse(cached) : null;
2007
- if (c && Array.isArray(c.experiments) && !config) config = c;
2135
+ if (c && Array.isArray(c.experiments) && !config) {
2136
+ config = c;
2137
+ setWords(c.words, c.lang);
2138
+ }
2008
2139
  } catch {
2009
2140
  await remove(KEYS.config);
2010
2141
  }
@@ -2060,6 +2191,9 @@ var createClient = (options, deps) => {
2060
2191
  await fetchConfig();
2061
2192
  markReady();
2062
2193
  refreshFlows();
2194
+ onLocale(() => {
2195
+ void fetchConfig().then(refreshFlows);
2196
+ });
2063
2197
  };
2064
2198
  void start().catch(() => {
2065
2199
  loaded = true;
@@ -2248,13 +2382,13 @@ var createClient = (options, deps) => {
2248
2382
  return client;
2249
2383
  };
2250
2384
  var activeRouteName = (state) => {
2251
- let s = state;
2385
+ let s2 = state;
2252
2386
  let name;
2253
- for (let depth = 0; s && Array.isArray(s.routes) && s.routes.length && depth < 20; depth++) {
2254
- const route = s.routes[typeof s.index === "number" ? s.index : s.routes.length - 1];
2387
+ for (let depth = 0; s2 && Array.isArray(s2.routes) && s2.routes.length && depth < 20; depth++) {
2388
+ const route = s2.routes[typeof s2.index === "number" ? s2.index : s2.routes.length - 1];
2255
2389
  if (!route) break;
2256
2390
  name = route.name ?? name;
2257
- s = route.state;
2391
+ s2 = route.state;
2258
2392
  }
2259
2393
  return name;
2260
2394
  };
@@ -2410,9 +2544,9 @@ var Finale = ({ kind, accent }) => {
2410
2544
  RN.View,
2411
2545
  { pointerEvents: "none", accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 } },
2412
2546
  ...bits.map((b, i) => {
2413
- const t = values[i];
2414
- const fade = t.interpolate({ inputRange: [0, 0.7, 1], outputRange: [1, 1, 0] });
2415
- const scale = b.shape === "star" ? t.interpolate({ inputRange: [0, 0.25, 1], outputRange: [0.2, 1.15, 0.6] }) : 1;
2547
+ const t2 = values[i];
2548
+ const fade = t2.interpolate({ inputRange: [0, 0.7, 1], outputRange: [1, 1, 0] });
2549
+ const scale = b.shape === "star" ? t2.interpolate({ inputRange: [0, 0.25, 1], outputRange: [0.2, 1.15, 0.6] }) : 1;
2416
2550
  return (0, import_react2.createElement)(
2417
2551
  A.View,
2418
2552
  {
@@ -2429,9 +2563,9 @@ var Finale = ({ kind, accent }) => {
2429
2563
  justifyContent: "center",
2430
2564
  opacity: fade,
2431
2565
  transform: [
2432
- { translateX: t.interpolate({ inputRange: [0, 1], outputRange: [0, b.dx] }) },
2433
- { translateY: t.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, b.mid, b.dy] }) },
2434
- { rotate: t.interpolate({ inputRange: [0, 1], outputRange: ["0deg", `${b.rot}deg`] }) },
2566
+ { translateX: t2.interpolate({ inputRange: [0, 1], outputRange: [0, b.dx] }) },
2567
+ { translateY: t2.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, b.mid, b.dy] }) },
2568
+ { rotate: t2.interpolate({ inputRange: [0, 1], outputRange: ["0deg", `${b.rot}deg`] }) },
2435
2569
  { scale }
2436
2570
  ]
2437
2571
  }
@@ -2443,10 +2577,9 @@ var Finale = ({ kind, accent }) => {
2443
2577
  };
2444
2578
  var LIGHT = { bg: "#ffffff", fg: "#1c1d20", muted: "#5d6068", soft: "#f4f5f7", line: "#e4e5e8" };
2445
2579
  var DARK = { bg: "#17181b", fg: "#f1f2f4", muted: "#a9acb3", soft: "#232428", line: "#2e2f35" };
2446
- var months = (n) => `${n} ${n === 1 ? "month" : "months"}`;
2447
2580
  var MRRocketFlows = (props) => {
2448
2581
  const [, setTick] = (0, import_react2.useState)(0);
2449
- (0, import_react2.useEffect)(() => props.flows.subscribe(() => setTick((t) => t + 1)), [props.flows]);
2582
+ (0, import_react2.useEffect)(() => props.flows.subscribe(() => setTick((t2) => t2 + 1)), [props.flows]);
2450
2583
  const fin = props.flows.finale?.() ?? null;
2451
2584
  const accent = props.appearance?.accent ?? props.flows.accent?.() ?? "#3f76ff";
2452
2585
  return (0, import_react2.createElement)(import_react2.Fragment, null, (0, import_react2.createElement)(FlowsBody, props), fin ? (0, import_react2.createElement)(Finale, { key: fin.key, kind: fin.kind, accent }) : null);
@@ -2456,8 +2589,8 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2456
2589
  const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
2457
2590
  const p = { ...dark ? DARK : LIGHT, accent: appearance.accent ?? flows.accent?.() ?? "#3f76ff", danger: "#d92d20" };
2458
2591
  const [, setTick] = (0, import_react2.useState)(0);
2459
- (0, import_react2.useEffect)(() => flows.subscribe(() => setTick((t) => t + 1)), [flows]);
2460
- (0, import_react2.useEffect)(() => paywall?.subscribe(() => setTick((t) => t + 1)), [paywall]);
2592
+ (0, import_react2.useEffect)(() => flows.subscribe(() => setTick((t2) => t2 + 1)), [flows]);
2593
+ (0, import_react2.useEffect)(() => paywall?.subscribe(() => setTick((t2) => t2 + 1)), [paywall]);
2461
2594
  const pw = paywall?.view() ?? null;
2462
2595
  const v = flows.view();
2463
2596
  const [comment, setComment] = (0, import_react2.useState)("");
@@ -2546,7 +2679,7 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2546
2679
  { length: w.total },
2547
2680
  (_, i) => (0, import_react2.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= w.index ? p.accent : p.line } })
2548
2681
  ) : [],
2549
- close("Close", host.close)
2682
+ close(t("Close"), host.close)
2550
2683
  ),
2551
2684
  title(pg.title),
2552
2685
  pg.body ? muted(pg.body, { fontSize: 15 }) : null,
@@ -2591,8 +2724,8 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2591
2724
  )
2592
2725
  ) : null,
2593
2726
  w.error ? text2(w.error, { color: p.danger, fontSize: 13 }) : null,
2594
- btn(w.busy ? "One moment\u2026" : pg.button || "Continue", "primary", host.next, w.busy || pg.kind === "plans" && !w.plan),
2595
- w.index ? btn("Back", "quiet", host.back, w.busy) : null
2727
+ btn(w.busy ? t("One moment\u2026") : pg.button || t("Continue"), "primary", host.next, w.busy || pg.kind === "plans" && !w.plan),
2728
+ w.index ? btn(t("Back"), "quiet", host.back, w.busy) : null
2596
2729
  ],
2597
2730
  host.close
2598
2731
  );
@@ -2607,7 +2740,7 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2607
2740
  { length: v.steps },
2608
2741
  (_, i) => (0, import_react2.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i < v.step ? p.accent : p.line } })
2609
2742
  ) : [],
2610
- close("Close", flows.cancel.close)
2743
+ close(t("Close"), flows.cancel.close)
2611
2744
  );
2612
2745
  const error = v.error ? text2(v.error, { color: p.danger, fontSize: 13 }) : null;
2613
2746
  let body = [];
@@ -2638,45 +2771,45 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2638
2771
  );
2639
2772
  if (v.screen === "survey")
2640
2773
  body = [
2641
- title(v.flow.question || "Why are you leaving?"),
2774
+ title(v.flow.question || t("Why are you leaving?")),
2642
2775
  choices(v.flow.reasons.map((r) => ({ key: r.id, label: r.label, on: v.reason === r.id, press: () => flows.cancel.pick(r.id) }))),
2643
- btn("Continue", "primary", flows.cancel.next, !v.reason),
2644
- btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2776
+ btn(t("Continue"), "primary", flows.cancel.next, !v.reason),
2777
+ btn(t("Never mind, keep my plan"), "quiet", flows.cancel.close)
2645
2778
  ];
2646
2779
  else if (v.screen === "followup" && v.followUp) {
2647
2780
  const f = v.followUp;
2648
2781
  const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2649
2782
  body = [
2650
- label ? muted(`You said: ${label}`) : null,
2783
+ label ? muted(tf("You said: {label}", { label })) : null,
2651
2784
  title(f.question),
2652
- 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),
2653
- btn("Continue", "primary", flows.cancel.next),
2654
- btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2785
+ f.answers.length ? choices(f.answers.map((a, i) => ({ key: String(i), label: f.answerLabels?.[i] || a, on: v.answer === a, press: () => flows.cancel.answer(a) }))) : input(t("Your answer (optional)"), v.answer ?? "", flows.cancel.answer),
2786
+ btn(t("Continue"), "primary", flows.cancel.next),
2787
+ btn(t("Never mind, keep my plan"), "quiet", flows.cancel.close)
2655
2788
  ];
2656
2789
  } else if (v.screen === "offer" && v.offer) {
2657
2790
  const o = v.offer;
2658
2791
  const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2659
- 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;
2792
+ const detail = o.kind === "discount" && o.percent ? v.price ? `${tf("{price} instead of {old}", { price: money(discounted(v.price.amount, o.percent), v.price.currency), old: money(v.price.amount, v.price.currency) })}${o.months ? ` ${forMonths(o.months)}` : ""}` : `${tf("{percent}% off", { percent: o.percent })}${o.months ? ` ${forMonths(o.months)}` : ""}` : o.kind === "pause" && o.months ? tf("Pause for up to {months}. Your data stays.", { months: monthsWord(o.months) }) : o.kind === "downgrade" && o.planName ? o.planName : null;
2660
2793
  body = [
2661
- label ? muted(`You said: ${label}`) : null,
2794
+ label ? muted(tf("You said: {label}", { label })) : null,
2662
2795
  title(o.headline),
2663
2796
  detail ? (0, import_react2.createElement)(RN.View, { style: { backgroundColor: p.soft, borderRadius: 10, padding: 12 } }, text2(detail, { fontWeight: "600" })) : null,
2664
- o.kind === "contact" ? input("What would make you stay?") : null,
2665
- btn(v.busy ? "One moment\u2026" : offerButton(o), "primary", () => flows.cancel.accept(comment), v.busy),
2666
- btn("No thanks, continue cancelling", "quiet", flows.cancel.decline, v.busy)
2797
+ o.kind === "contact" ? input(t("What would make you stay?")) : null,
2798
+ btn(v.busy ? t("One moment\u2026") : offerButton(o), "primary", () => flows.cancel.accept(comment), v.busy),
2799
+ btn(t("No thanks, continue cancelling"), "quiet", flows.cancel.decline, v.busy)
2667
2800
  ];
2668
2801
  } else
2669
2802
  body = [
2670
2803
  title(v.flow.confirm.title),
2671
2804
  v.flow.confirm.body ? muted(v.flow.confirm.body, { fontSize: 15 }) : null,
2672
- v.steps > 1 ? input("Anything else we should know? (optional)") : null,
2673
- btn(v.busy ? "One moment\u2026" : v.flow.confirm.button, "danger", () => flows.cancel.confirm(comment), v.busy),
2674
- btn("Keep my plan", "secondary", flows.cancel.close, v.busy)
2805
+ v.steps > 1 ? input(t("Anything else we should know? (optional)")) : null,
2806
+ btn(v.busy ? t("One moment\u2026") : v.flow.confirm.button, "danger", () => flows.cancel.confirm(comment), v.busy),
2807
+ btn(t("Keep my plan"), "secondary", flows.cancel.close, v.busy)
2675
2808
  ];
2676
2809
  return sheet([top, ...body, error], flows.cancel.close);
2677
2810
  }
2678
- const media = (s) => {
2679
- const m2 = mediaOf(s);
2811
+ const media = (s2) => {
2812
+ const m2 = mediaOf(s2);
2680
2813
  if (!m2) return null;
2681
2814
  if (m2.kind === "image")
2682
2815
  return RN.Image ? (0, import_react2.createElement)(RN.Image, {
@@ -2688,12 +2821,12 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2688
2821
  }) : null;
2689
2822
  const v2 = videoEmbed(m2.url);
2690
2823
  const open = v2?.kind === "file" ? v2.src : m2.url;
2691
- return RN.Linking ? btn("\u25B6 Watch the video", "secondary", () => void RN.Linking?.openURL(open).catch(() => {
2824
+ return RN.Linking ? btn(`\u25B6 ${t("Watch the video")}`, "secondary", () => void RN.Linking?.openURL(open).catch(() => {
2692
2825
  })) : null;
2693
2826
  };
2694
2827
  if (v.type === "step" && v.step.kind === "question") {
2695
- const s = v.step;
2696
- const q = s.question;
2828
+ const s2 = v.step;
2829
+ const q = s2.question;
2697
2830
  const ok = answerOk(q, v.answer);
2698
2831
  return sheet(
2699
2832
  [
@@ -2704,48 +2837,84 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2704
2837
  { length: v.total },
2705
2838
  (_, i) => (0, import_react2.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= v.index ? p.accent : p.line } })
2706
2839
  ) : [],
2707
- close("Close", flows.onboarding.dismiss)
2840
+ close(t("Close"), flows.onboarding.dismiss)
2708
2841
  ),
2709
2842
  v.index === 0 && v.flow.icon ? (0, import_react2.createElement)(RN.View, { key: "icon" }, iconNode(v.flow.icon, 36, p.accent)) : null,
2710
- title(s.title),
2711
- s.body ? muted(s.body, { fontSize: 15 }) : null,
2843
+ title(s2.title),
2844
+ s2.body ? muted(s2.body, { fontSize: 15 }) : null,
2712
2845
  q?.type === "choice" ? (0, import_react2.createElement)(
2713
2846
  RN.ScrollView,
2714
2847
  { key: "opts", style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
2715
- ...(q.options ?? []).map(
2716
- (o) => (0, import_react2.createElement)(
2848
+ ...[...q.options ?? [], ...q.other ? [{ id: OTHER_PREFIX, label: q.otherLabel || t("Other") }] : []].map((o) => {
2849
+ const isOther = o.id === OTHER_PREFIX;
2850
+ const on = isOther ? !!v.answer?.startsWith(OTHER_PREFIX) : v.answer === o.id;
2851
+ return (0, import_react2.createElement)(
2717
2852
  RN.Pressable,
2718
2853
  {
2719
2854
  key: o.id,
2720
- onPress: () => flows.onboarding.answer(o.id),
2855
+ onPress: () => isOther && on ? void 0 : flows.onboarding.answer(o.id),
2721
2856
  accessibilityRole: "radio",
2722
- accessibilityState: { checked: v.answer === o.id },
2857
+ accessibilityState: { checked: on },
2723
2858
  style: {
2724
2859
  minHeight: 44,
2725
2860
  paddingHorizontal: 12,
2726
2861
  borderRadius: 10,
2727
2862
  borderWidth: 1,
2728
- borderColor: v.answer === o.id ? p.accent : p.line,
2729
- backgroundColor: v.answer === o.id ? p.soft : p.bg,
2863
+ borderColor: on ? p.accent : p.line,
2864
+ backgroundColor: on ? p.soft : p.bg,
2730
2865
  justifyContent: "center"
2731
2866
  }
2732
2867
  },
2733
2868
  text2(o.label, { fontWeight: "500" })
2734
- )
2735
- )
2869
+ );
2870
+ }),
2871
+ q.other && v.answer?.startsWith(OTHER_PREFIX) ? (0, import_react2.createElement)(RN.TextInput, {
2872
+ key: "other",
2873
+ value: v.answer.slice(OTHER_PREFIX.length),
2874
+ onChangeText: (t2) => flows.onboarding.answer(OTHER_PREFIX + t2),
2875
+ onSubmitEditing: flows.onboarding.next,
2876
+ placeholder: t("Your answer"),
2877
+ placeholderTextColor: p.muted,
2878
+ maxLength: 490,
2879
+ accessibilityLabel: q.otherLabel || t("Other"),
2880
+ style: { height: 44, borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 12, color: p.fg }
2881
+ }) : null
2882
+ ) : q?.type === "scale" ? (0, import_react2.createElement)(
2883
+ RN.View,
2884
+ { key: "scale", style: { gap: 6 } },
2885
+ (0, import_react2.createElement)(
2886
+ RN.View,
2887
+ { style: { flexDirection: "row", gap: 4 }, accessibilityRole: "radiogroup" },
2888
+ ...scaleOf(q).map((n) => {
2889
+ const on = v.answer === String(n);
2890
+ return (0, import_react2.createElement)(
2891
+ RN.Pressable,
2892
+ {
2893
+ key: n,
2894
+ onPress: () => flows.onboarding.answer(String(n)),
2895
+ accessibilityRole: "radio",
2896
+ accessibilityState: { checked: on },
2897
+ style: { flex: 1, height: 44, borderRadius: 8, borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.accent : p.bg, alignItems: "center", justifyContent: "center" }
2898
+ },
2899
+ text2(String(n), { fontWeight: "500", color: on ? "#fff" : p.fg })
2900
+ );
2901
+ })
2902
+ ),
2903
+ q.lowLabel || q.highLabel ? (0, import_react2.createElement)(RN.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, muted(q.lowLabel || ""), muted(q.highLabel || "")) : null
2736
2904
  ) : q ? (0, import_react2.createElement)(RN.TextInput, {
2737
2905
  key: "answer",
2738
2906
  value: v.answer ?? "",
2739
- onChangeText: (t) => flows.onboarding.answer(t),
2907
+ onChangeText: (t2) => flows.onboarding.answer(t2),
2740
2908
  onSubmitEditing: flows.onboarding.next,
2741
- placeholder: q.placeholder || "Your answer",
2909
+ placeholder: q.placeholder || t("Your answer"),
2742
2910
  placeholderTextColor: p.muted,
2743
2911
  maxLength: 500,
2744
- accessibilityLabel: s.title,
2912
+ accessibilityLabel: s2.title,
2745
2913
  style: { height: 44, borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 12, color: p.fg }
2746
2914
  }) : null,
2747
- btn(s.button || (v.last ? "Done" : "Continue"), "primary", flows.onboarding.next, !ok),
2748
- v.back ? btn("Back", "quiet", flows.onboarding.back) : null
2915
+ btn(s2.button || (v.last ? t("Done") : t("Continue")), "primary", flows.onboarding.next, !ok),
2916
+ q?.optional ? btn(q.skipLabel || t("Skip"), "quiet", flows.onboarding.skip) : null,
2917
+ v.back ? btn(t("Back"), "quiet", flows.onboarding.back) : null
2749
2918
  ],
2750
2919
  flows.onboarding.dismiss
2751
2920
  );
@@ -2756,14 +2925,14 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2756
2925
  (0, import_react2.createElement)(
2757
2926
  RN.View,
2758
2927
  { style: { flexDirection: "row", alignItems: "center" } },
2759
- v.total > 1 ? muted(`Step ${v.index + 1} of ${v.total}`) : null,
2760
- close("Close the tour", flows.onboarding.dismiss)
2928
+ v.total > 1 ? muted(tf("Step {n} of {total}", { n: v.index + 1, total: v.total })) : null,
2929
+ close(t("Close the tour"), flows.onboarding.dismiss)
2761
2930
  ),
2762
2931
  v.step.kind === "modal" && v.flow.icon ? (0, import_react2.createElement)(RN.View, { key: "icon" }, iconNode(v.flow.icon, 36, p.accent)) : null,
2763
2932
  media(v.step),
2764
2933
  title(v.step.title),
2765
2934
  v.step.body ? muted(v.step.body, { fontSize: 15 }) : null,
2766
- btn(v.step.button || (v.last ? "Done" : "Next"), "primary", flows.onboarding.next)
2935
+ btn(v.step.button || (v.last ? t("Done") : t("Next")), "primary", flows.onboarding.next)
2767
2936
  ],
2768
2937
  flows.onboarding.dismiss
2769
2938
  );
@@ -2777,9 +2946,9 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2777
2946
  RN.View,
2778
2947
  { key: "t", style: { flexDirection: "row", alignItems: "center" } },
2779
2948
  title(v.flow.name),
2780
- close("Hide the checklist", flows.onboarding.dismiss)
2949
+ close(t("Hide the checklist"), flows.onboarding.dismiss)
2781
2950
  ),
2782
- muted(`${done} of ${v.items.length} done`),
2951
+ muted(tf("{done} of {total} done", { done, total: v.items.length })),
2783
2952
  ...v.items.map(
2784
2953
  ({ step, done: d, opens }) => (0, import_react2.createElement)(
2785
2954
  RN.Pressable,
@@ -2831,14 +3000,14 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2831
3000
  RN.View,
2832
3001
  { key: "t", style: { flexDirection: "row", alignItems: "center", gap: 8 } },
2833
3002
  (0, import_react2.createElement)(RN.View, { style: { flex: 1 } }, text2(m.title, { fontWeight: "700" }), m.body ? muted(m.body) : null),
2834
- close("Dismiss", flows.message.dismiss)
3003
+ close(t("Dismiss"), flows.message.dismiss)
2835
3004
  ),
2836
3005
  m.button ? btn(m.button, "primary", flows.message.click) : null
2837
3006
  ])
2838
3007
  );
2839
3008
  return sheet(
2840
3009
  [
2841
- (0, import_react2.createElement)(RN.View, { key: "t", style: { flexDirection: "row" } }, close("Dismiss", flows.message.dismiss)),
3010
+ (0, import_react2.createElement)(RN.View, { key: "t", style: { flexDirection: "row" } }, close(t("Dismiss"), flows.message.dismiss)),
2842
3011
  title(m.title),
2843
3012
  m.body ? muted(m.body, { fontSize: 15 }) : null,
2844
3013
  m.button ? btn(m.button, "primary", flows.message.click) : null
@@ -2912,10 +3081,10 @@ var expoPush = () => {
2912
3081
  },
2913
3082
  token: async (projectId) => {
2914
3083
  if (RN2.Platform.OS === "android" && n.setNotificationChannelAsync)
2915
- await n.setNotificationChannelAsync("default", { name: "Default", importance: n.AndroidImportance?.HIGH ?? 4 });
3084
+ await n.setNotificationChannelAsync("default", { name: t("Default"), importance: n.AndroidImportance?.HIGH ?? 4 });
2916
3085
  const id = projectId ?? constants.expoConfig?.extra?.eas?.projectId ?? constants.easConfig?.projectId;
2917
- const t = await n.getExpoPushTokenAsync(id ? { projectId: id } : void 0);
2918
- return typeof t?.data === "string" && t.data ? t.data : null;
3086
+ const push = await n.getExpoPushTokenAsync(id ? { projectId: id } : void 0);
3087
+ return typeof push?.data === "string" && push.data ? push.data : null;
2919
3088
  },
2920
3089
  onOpen: (listener) => {
2921
3090
  const sub = n.addNotificationResponseReceivedListener((r) => listener(dataOf(r) ?? {}));
@@ -2926,8 +3095,8 @@ var expoPush = () => {
2926
3095
  };
2927
3096
  var asyncStorage = () => {
2928
3097
  const m = asyncStorageModule;
2929
- const s = m?.default ?? m;
2930
- return s && typeof s.getItem === "function" && typeof s.setItem === "function" && typeof s.removeItem === "function" ? s : null;
3098
+ const s2 = m?.default ?? m;
3099
+ return s2 && typeof s2.getItem === "function" && typeof s2.setItem === "function" && typeof s2.removeItem === "function" ? s2 : null;
2931
3100
  };
2932
3101
  var DEVICE_TYPES = { 1: "phone", 2: "tablet", 3: "desktop", 4: "tv" };
2933
3102
  var text = (v, max = 80) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : typeof v === "number" ? String(v) : void 0;
@@ -2971,10 +3140,10 @@ var recorderDeps = () => {
2971
3140
  }
2972
3141
  return {
2973
3142
  capture: async (width) => {
2974
- const s = RN2.Dimensions.get("screen");
2975
- if (!s?.width) return null;
3143
+ const s2 = RN2.Dimensions.get("screen");
3144
+ if (!s2?.width) return null;
2976
3145
  const w = Math.round(width);
2977
- const h3 = Math.round(s.height / s.width * width);
3146
+ const h3 = Math.round(s2.height / s2.width * width);
2978
3147
  const img = await capture({ format: "jpg", quality: 0.5, result: "base64", width: w, height: h3 });
2979
3148
  return img ? { img: img.replace(/^data:image\/\w+;base64,/, ""), w, h: h3 } : null;
2980
3149
  },
@@ -3001,8 +3170,8 @@ var nativeDeps = (overrides = {}) => ({
3001
3170
  context: readContext,
3002
3171
  appId: text(expoApplication?.applicationId, 150),
3003
3172
  screen: () => {
3004
- const s = RN2.Dimensions.get("screen");
3005
- return s ? { w: s.width, h: s.height } : void 0;
3173
+ const s2 = RN2.Dimensions.get("screen");
3174
+ return s2 ? { w: s2.width, h: s2.height } : void 0;
3006
3175
  },
3007
3176
  initialUrl: () => RN2.Linking.getInitialURL(),
3008
3177
  openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
@@ -3100,7 +3269,7 @@ var kit = (p) => {
3100
3269
  RN3.View,
3101
3270
  { style: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: p.line } },
3102
3271
  title(heading),
3103
- closeX("Close", onClose)
3272
+ closeX(t("Close"), onClose)
3104
3273
  ),
3105
3274
  ...children
3106
3275
  )
@@ -3108,13 +3277,13 @@ var kit = (p) => {
3108
3277
  return { text: text2, muted, title, btn, closeX, field, chip, page };
3109
3278
  };
3110
3279
  var timeAgo = (iso) => {
3111
- const t = new Date(iso).getTime();
3112
- if (Number.isNaN(t)) return "";
3113
- const d = Math.max(0, Date.now() - t) / 1e3;
3114
- if (d < 60) return "just now";
3115
- if (d < 3600) return `${Math.floor(d / 60)}m ago`;
3116
- if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
3117
- return `${Math.floor(d / 86400)}d ago`;
3280
+ const at = new Date(iso).getTime();
3281
+ if (Number.isNaN(at)) return "";
3282
+ const d = Math.max(0, Date.now() - at) / 1e3;
3283
+ if (d < 60) return t("just now");
3284
+ if (d < 3600) return tf("{n}m ago", { n: Math.floor(d / 60) });
3285
+ if (d < 86400) return tf("{n}h ago", { n: Math.floor(d / 3600) });
3286
+ return tf("{n}d ago", { n: Math.floor(d / 86400) });
3118
3287
  };
3119
3288
  var opener = () => {
3120
3289
  let open = false;
@@ -3153,8 +3322,8 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3153
3322
  }, [api, survey.id]);
3154
3323
  (0, import_react3.useEffect)(() => {
3155
3324
  if (!done) return;
3156
- const t = setTimeout(onDone, 2500);
3157
- return () => clearTimeout(t);
3325
+ const t2 = setTimeout(onDone, 2500);
3326
+ return () => clearTimeout(t2);
3158
3327
  }, [done, onDone]);
3159
3328
  const finish = (completed, all = answers) => {
3160
3329
  if (finished.current) return;
@@ -3166,7 +3335,7 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3166
3335
  onDone();
3167
3336
  };
3168
3337
  const next = () => {
3169
- if (q.required && !isAnswered(value)) return setError("Please answer this question.");
3338
+ if (q.required && !isAnswered(value)) return setError(t("Please answer this question."));
3170
3339
  setError("");
3171
3340
  if (!last) return setStep(step + 1);
3172
3341
  finish(true);
@@ -3196,14 +3365,14 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3196
3365
  onPress: () => pick(n),
3197
3366
  accessibilityRole: "radio",
3198
3367
  accessibilityState: { checked: on },
3199
- accessibilityLabel: `${n} of ${scale - 1 + start}`,
3368
+ accessibilityLabel: tf("{n} of {total}", { n, total: scale - 1 + start }),
3200
3369
  style: { width: 40, height: 40, borderRadius: 8, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.accent : p.bg }
3201
3370
  },
3202
3371
  k.text(String(n), { color: on ? "#ffffff" : p.fg, fontWeight: "600" })
3203
3372
  );
3204
3373
  })
3205
3374
  ),
3206
- q.low || q.high || q.kind === "nps" ? (0, import_react3.createElement)(RN3.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, k.muted(q.low ?? (q.kind === "nps" ? "Not likely" : "")), k.muted(q.high ?? (q.kind === "nps" ? "Very likely" : ""))) : null
3375
+ q.low || q.high || q.kind === "nps" ? (0, import_react3.createElement)(RN3.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, k.muted(q.low ?? (q.kind === "nps" ? t("Not likely") : "")), k.muted(q.high ?? (q.kind === "nps" ? t("Very likely") : ""))) : null
3207
3376
  );
3208
3377
  } else if (q.kind === "single" || q.kind === "multi") {
3209
3378
  const multi = q.kind === "multi";
@@ -3211,7 +3380,7 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3211
3380
  input = (0, import_react3.createElement)(
3212
3381
  RN3.View,
3213
3382
  { style: { gap: 6 } },
3214
- ...(q.options ?? []).map((o) => {
3383
+ ...(q.options ?? []).map((o, i) => {
3215
3384
  const on = multi ? picked.includes(o) : value === o;
3216
3385
  return (0, import_react3.createElement)(
3217
3386
  RN3.Pressable,
@@ -3222,24 +3391,25 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3222
3391
  accessibilityState: { checked: on },
3223
3392
  style: { minHeight: 44, paddingHorizontal: 12, borderRadius: 10, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
3224
3393
  },
3225
- k.text(o, { fontWeight: "500" })
3394
+ // The label in the person's language; what is recorded stays the option itself.
3395
+ k.text(q.labels?.[i] || o, { fontWeight: "500" })
3226
3396
  );
3227
3397
  })
3228
3398
  );
3229
- } else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? "Type your answer", multiline: true });
3230
- const body = done ? [k.text(survey.thanks || "Thanks for your feedback.", { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
3399
+ } else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? t("Type your answer"), multiline: true });
3400
+ const body = done ? [k.text(survey.thanks || t("Thanks for your feedback."), { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
3231
3401
  (0, import_react3.createElement)(
3232
3402
  RN3.View,
3233
3403
  { key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
3234
3404
  ...survey.questions.map((x, i) => (0, import_react3.createElement)(RN3.View, { key: x.key, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= step ? p.accent : p.line } })),
3235
3405
  (0, import_react3.createElement)(RN3.View, { style: { flex: 1 } }),
3236
- k.closeX("Close survey", dismiss)
3406
+ k.closeX(t("Close survey"), dismiss)
3237
3407
  ),
3238
- (0, import_react3.createElement)(RN3.Text, { key: "q", accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700" } }, q.title, q.required ? "" : (0, import_react3.createElement)(RN3.Text, { style: { color: p.muted, fontSize: 13, fontWeight: "400" } }, " optional")),
3408
+ (0, import_react3.createElement)(RN3.Text, { key: "q", accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700" } }, q.title, q.required ? "" : (0, import_react3.createElement)(RN3.Text, { style: { color: p.muted, fontSize: 13, fontWeight: "400" } }, ` ${t("optional")}`)),
3239
3409
  q.description ? k.muted(q.description) : null,
3240
3410
  input,
3241
3411
  error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
3242
- k.btn(last ? "Send" : "Next", "primary", next)
3412
+ k.btn(last ? t("Send") : t("Next"), "primary", next)
3243
3413
  ];
3244
3414
  return (0, import_react3.createElement)(
3245
3415
  RN3.Modal,
@@ -3272,9 +3442,9 @@ var MRRocketSurveys = ({ client, appearance }) => {
3272
3442
  const api = client.api();
3273
3443
  if (stop || !api) return;
3274
3444
  api.survey("", ctrl.signal).then((r) => {
3275
- const s = r.survey;
3276
- if (stop || !s || !s.questions.length || client.isSurveyDone(s.id)) return;
3277
- setSurvey({ s, api });
3445
+ const s2 = r.survey;
3446
+ if (stop || !s2 || !s2.questions.length || client.isSurveyDone(s2.id)) return;
3447
+ setSurvey({ s: s2, api });
3278
3448
  }).catch(() => void 0);
3279
3449
  });
3280
3450
  }, 3e3);
@@ -3312,7 +3482,7 @@ var FeedbackSheet = ({ client, p, title }) => {
3312
3482
  const user = client.getUser();
3313
3483
  (0, import_react3.useEffect)(() => {
3314
3484
  const api = client.api();
3315
- if (!api) return setError("Feedback is off while tracking is off.");
3485
+ if (!api) return setError(t("Feedback is off while tracking is off."));
3316
3486
  const ctrl = new AbortController();
3317
3487
  api.list({ sort: "top" }, ctrl.signal).then((l) => {
3318
3488
  setPosts(l.posts);
@@ -3345,8 +3515,8 @@ var FeedbackSheet = ({ client, p, title }) => {
3345
3515
  const tabs = (0, import_react3.createElement)(
3346
3516
  RN3.View,
3347
3517
  { style: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingTop: 12 } },
3348
- k.chip("Top requests", tab === "top", () => setTab("top")),
3349
- k.chip("Suggest", tab === "new", () => {
3518
+ k.chip(t("Top requests"), tab === "top", () => setTab("top")),
3519
+ k.chip(t("Suggest"), tab === "new", () => {
3350
3520
  setTab("new");
3351
3521
  setSent(false);
3352
3522
  })
@@ -3355,8 +3525,8 @@ var FeedbackSheet = ({ client, p, title }) => {
3355
3525
  RN3.ScrollView,
3356
3526
  { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
3357
3527
  error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
3358
- posts === null && !error ? k.muted("Loading\u2026") : null,
3359
- posts?.length === 0 ? k.muted("No requests yet. Be the first to suggest one.") : null,
3528
+ posts === null && !error ? k.muted(t("Loading\u2026")) : null,
3529
+ posts?.length === 0 ? k.muted(t("No requests yet. Be the first to suggest one.")) : null,
3360
3530
  ...(posts ?? []).map(
3361
3531
  (post) => (0, import_react3.createElement)(
3362
3532
  RN3.View,
@@ -3367,7 +3537,7 @@ var FeedbackSheet = ({ client, p, title }) => {
3367
3537
  onPress: () => vote(post),
3368
3538
  accessibilityRole: "button",
3369
3539
  accessibilityState: { selected: post.voted },
3370
- accessibilityLabel: `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
3540
+ accessibilityLabel: post.voted ? tf("Remove your vote from {title}. {votes}", { title: post.title, votes: post.votes === 1 ? t("1 vote") : tf("{n} votes", { n: post.votes }) }) : tf("Upvote {title}. {votes}", { title: post.title, votes: post.votes === 1 ? t("1 vote") : tf("{n} votes", { n: post.votes }) }),
3371
3541
  style: { width: 44, minHeight: 48, borderRadius: 10, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: post.voted ? p.accent : p.line }
3372
3542
  },
3373
3543
  k.text("\u25B2", { fontSize: 11, color: post.voted ? p.accent : p.muted }),
@@ -3380,36 +3550,39 @@ var FeedbackSheet = ({ client, p, title }) => {
3380
3550
  const form = (0, import_react3.createElement)(
3381
3551
  RN3.ScrollView,
3382
3552
  { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 }, keyboardShouldPersistTaps: "handled" },
3383
- sent ? k.text("Thanks! Your request is on the board.", { fontWeight: "600" }) : null,
3553
+ sent ? k.text(t("Thanks! Your request is on the board."), { fontWeight: "600" }) : null,
3384
3554
  boards.length > 1 ? (0, import_react3.createElement)(RN3.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 6 } }, ...boards.map((b) => k.chip(b.label, board === b.id, () => setBoard(b.id), b.id))) : null,
3385
- k.field({ value: subject, onChange: setSubject, placeholder: "Short title", max: 120 }),
3386
- k.field({ value: details, onChange: setDetails, placeholder: "Details (optional)", multiline: true }),
3555
+ k.field({ value: subject, onChange: setSubject, placeholder: t("Short title"), max: 120 }),
3556
+ k.field({ value: details, onChange: setDetails, placeholder: t("Details (optional)"), multiline: true }),
3387
3557
  ...user?.email ? [] : [
3388
- k.muted("Your name and email are optional; they let the team follow up."),
3389
- k.field({ value: name, onChange: setName, placeholder: "Your name (optional)", max: 100 }),
3390
- k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional)", max: 200, email: true })
3558
+ k.muted(t("Your name and email are optional; they let the team follow up.")),
3559
+ k.field({ value: name, onChange: setName, placeholder: t("Your name (optional)"), max: 100 }),
3560
+ k.field({ value: email, onChange: setEmail, placeholder: t("Your email (optional)"), max: 200, email: true })
3391
3561
  ],
3392
3562
  error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
3393
- k.btn(sending ? "Sending\u2026" : "Send request", "primary", submit, subject.trim().length < 3 || sending)
3563
+ k.btn(sending ? t("Sending\u2026") : t("Send request"), "primary", submit, subject.trim().length < 3 || sending)
3394
3564
  );
3395
3565
  return k.page(title, close, [tabs, tab === "top" ? list : form]);
3396
3566
  };
3397
3567
  var MRRocketFeedback = ({ client, appearance, options = {} }) => {
3398
3568
  const p = usePalette(appearance);
3399
3569
  const open = feedbackOpen.useOpen();
3400
- return open ? (0, import_react3.createElement)(FeedbackSheet, { client, p, title: options.title ?? "Feedback" }) : null;
3570
+ return open ? (0, import_react3.createElement)(FeedbackSheet, { client, p, title: options.title ?? t("Feedback") }) : null;
3401
3571
  };
3402
3572
  var POLL_OPEN_MS = 4e3;
3403
3573
  var POLL_WAITING_MS = 8e3;
3404
3574
  var POLL_IDLE_MS = 3e4;
3575
+ var POLL_QUIET_MS = 6e4;
3405
3576
  var WAITING_FOR_MS = 10 * 6e4;
3406
3577
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
3407
- var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
3408
- var who = (from, agentName) => from === "ai" && agentName?.trim() || whoLabel[from];
3578
+ var whoLabel = (from) => from === "visitor" ? t("You") : from === "ai" ? t("AI agent") : t("Team");
3579
+ var who = (from, agentName) => from === "ai" && agentName?.trim() || whoLabel(from);
3409
3580
  var msgView = "home";
3581
+ var msgChat = null;
3410
3582
  var messengerOpen = opener();
3411
- var openAt = (v = "home") => {
3583
+ var openAt = (v = "home", chatId = null) => {
3412
3584
  msgView = v;
3585
+ msgChat = chatId;
3413
3586
  messengerOpen.set(true);
3414
3587
  };
3415
3588
  var useMessenger = () => ({
@@ -3436,7 +3609,10 @@ try {
3436
3609
  imagePicker = null;
3437
3610
  }
3438
3611
  var PHOTO_TYPES = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif" };
3439
- var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
3612
+ var samePlace = (a, b) => a.chatId === b.chatId && a.fresh === b.fresh;
3613
+ var shownChat = (at, chats) => at.fresh ? null : at.chatId ?? (chats.length === 1 ? chats[0].id : null);
3614
+ var isAnswer = (m) => m.from === "team" || m.from === "ai";
3615
+ var lineOf = (m) => m.body || (m.attachments?.length === 1 ? tf("Sent {name}", { name: m.attachments[0].name }) : m.attachments?.length ? tf("Sent {n} files", { n: m.attachments.length }) : "");
3440
3616
  var svg2 = null;
3441
3617
  try {
3442
3618
  svg2 = require("react-native-svg");
@@ -3473,42 +3649,80 @@ var HU = {
3473
3649
  var hairline = { shadowColor: "#000", shadowOpacity: 0.07, shadowRadius: 3, shadowOffset: { width: 0, height: 1 }, elevation: 1 };
3474
3650
  var useChat = (client, o, open, view) => {
3475
3651
  const [thread, setThread] = (0, import_react3.useState)(null);
3652
+ const [chats, setChats] = (0, import_react3.useState)([]);
3653
+ const [hasEmail, setHasEmail] = (0, import_react3.useState)(false);
3654
+ const [place, setPlace] = (0, import_react3.useState)({ chatId: null, fresh: false });
3476
3655
  const [loaded, setLoaded] = (0, import_react3.useState)(false);
3477
3656
  const [error, setError] = (0, import_react3.useState)("");
3478
3657
  const [unseen, setUnseen] = (0, import_react3.useState)([]);
3479
- const known = (0, import_react3.useRef)(null);
3658
+ const known = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
3659
+ const seeded = (0, import_react3.useRef)(false);
3480
3660
  const watching = open && view === "messages";
3481
3661
  const watch = (0, import_react3.useRef)(watching);
3482
3662
  watch.current = watching;
3663
+ const at = (0, import_react3.useRef)(place);
3664
+ const list = (0, import_react3.useRef)(chats);
3665
+ const readFailed = (0, import_react3.useRef)(false);
3483
3666
  const [active, setActive] = (0, import_react3.useState)(RN3.AppState.currentState === "active");
3484
3667
  (0, import_react3.useEffect)(() => {
3485
3668
  const sub = RN3.AppState.addEventListener("change", (st) => setActive(st === "active"));
3486
3669
  return () => sub.remove();
3487
3670
  }, []);
3671
+ const enter = (0, import_react3.useCallback)((chatId, fresh = false) => {
3672
+ const next = { chatId, fresh };
3673
+ if (samePlace(at.current, next)) return;
3674
+ at.current = next;
3675
+ setPlace(next);
3676
+ setThread(null);
3677
+ setError("");
3678
+ }, []);
3488
3679
  const take = (0, import_react3.useCallback)(
3489
- (t) => {
3490
- const team = teamOf(t);
3491
- if (known.current !== null) {
3492
- const i = team.findIndex((m) => m.id === known.current);
3493
- const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
3494
- if (fresh.length) {
3495
- if (o.haptics !== false) buzz();
3496
- if (!watch.current) setUnseen((u) => [...u, ...fresh]);
3497
- }
3498
- }
3499
- known.current = team.at(-1)?.id ?? "";
3500
- setThread(t);
3680
+ (t2) => {
3681
+ const read = t2.conversation;
3682
+ const last = t2.messages.at(-1);
3683
+ const all = t2.conversations ?? (read && last ? [{ id: read.id, status: read.status, subject: lineOf(t2.messages[0]), last }] : []);
3684
+ const fresh = [];
3685
+ const learn = (chat, answers, newest) => {
3686
+ const was = known.current.get(chat);
3687
+ const since = was ?? (seeded.current ? "" : void 0);
3688
+ if (since !== void 0) fresh.push(...answers.filter((m) => m.at > since).map((m) => ({ ...m, chat })));
3689
+ known.current.set(chat, newest ?? was ?? "");
3690
+ };
3691
+ for (const c of all) if (c.id !== read?.id) learn(c.id, c.last && isAnswer(c.last) ? [c.last] : [], c.last?.at);
3692
+ if (read) learn(read.id, t2.messages.filter(isAnswer), last?.at);
3693
+ seeded.current = true;
3694
+ if (fresh.length && o.haptics !== false) buzz();
3695
+ const shown2 = shownChat(at.current, all);
3696
+ const counted = fresh.filter((m) => !watch.current || m.chat !== shown2);
3697
+ setUnseen((u) => [...u.filter((m) => all.some((c) => c.id === m.chat)), ...counted]);
3698
+ list.current = all;
3699
+ setChats(all);
3700
+ setHasEmail(t2.hasEmail);
3701
+ if (at.current.chatId === null || read?.id === at.current.chatId) setThread(t2);
3501
3702
  },
3502
3703
  [o.haptics]
3503
3704
  );
3705
+ const arrived = (0, import_react3.useCallback)(
3706
+ (from, t2) => {
3707
+ const went = t2.conversation?.id;
3708
+ if (went && samePlace(at.current, from)) {
3709
+ at.current = { chatId: went, fresh: false };
3710
+ setPlace(at.current);
3711
+ }
3712
+ take(t2);
3713
+ },
3714
+ [take]
3715
+ );
3716
+ const shown = shownChat(place, chats);
3504
3717
  (0, import_react3.useEffect)(() => {
3505
- if (watching) setUnseen([]);
3506
- }, [watching]);
3718
+ if (watching && shown) setUnseen((u) => u.filter((m) => m.chat !== shown));
3719
+ }, [watching, shown]);
3507
3720
  (0, import_react3.useEffect)(() => {
3508
3721
  if (!active) return;
3509
3722
  let stop = false;
3510
3723
  let timer;
3511
3724
  let ctrl = null;
3725
+ let refused = false;
3512
3726
  const tick = async () => {
3513
3727
  const api = client.api();
3514
3728
  if (!api) {
@@ -3516,23 +3730,32 @@ var useChat = (client, o, open, view) => {
3516
3730
  return;
3517
3731
  }
3518
3732
  ctrl = new AbortController();
3519
- let t = null;
3520
3733
  try {
3521
3734
  await client.ready;
3522
- t = await api.chat(ctrl.signal);
3735
+ const t2 = await api.chat(ctrl.signal, place.chatId ?? void 0);
3523
3736
  if (stop) return;
3524
- take(t);
3525
- setError("");
3737
+ take(t2);
3738
+ if (readFailed.current) setError("");
3739
+ readFailed.current = false;
3526
3740
  } catch (e) {
3527
3741
  if (e.name === "AbortError" || stop) return;
3528
- if (watch.current) setError(e.message);
3742
+ refused = [401, 403].includes(e.status ?? 0);
3743
+ if (e.status === 404 && place.chatId) enter(null);
3744
+ else if (watch.current) {
3745
+ readFailed.current = true;
3746
+ setError(e.message);
3747
+ }
3529
3748
  }
3530
3749
  setLoaded(true);
3531
- const waiting = !!t?.messages.some((m) => m.from === "visitor") && t?.conversation?.status !== "solved";
3532
- const wrote = (t?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
3533
- const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
3534
- const ms = watch.current ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : waiting || open ? POLL_IDLE_MS : 0;
3750
+ const running = list.current.filter((c) => c.status !== "solved");
3751
+ const recent = running.some((c) => c.last && Date.now() - new Date(c.last.at).getTime() < WAITING_FOR_MS);
3752
+ const ms = watch.current ? POLL_OPEN_MS : running.length && recent ? POLL_WAITING_MS : running.length || open ? POLL_IDLE_MS : 0;
3535
3753
  if (!stop && ms) timer = setTimeout(tick, ms);
3754
+ else if (!stop) timer = setTimeout(quiet, POLL_QUIET_MS);
3755
+ };
3756
+ const quiet = () => {
3757
+ if (client.getUser() && !refused) void tick();
3758
+ else timer = setTimeout(quiet, POLL_QUIET_MS);
3536
3759
  };
3537
3760
  void tick();
3538
3761
  return () => {
@@ -3540,8 +3763,22 @@ var useChat = (client, o, open, view) => {
3540
3763
  ctrl?.abort();
3541
3764
  clearTimeout(timer);
3542
3765
  };
3543
- }, [client, take, active, open, watching]);
3544
- return { thread, loaded, error, setError, unseen, take };
3766
+ }, [client, take, enter, active, open, watching, place]);
3767
+ return {
3768
+ // A new conversation has nothing to show yet; polls keep reading the newest for the list.
3769
+ thread: place.fresh ? null : thread,
3770
+ chats,
3771
+ hasEmail,
3772
+ place,
3773
+ /** Messages shows the list: several conversations and none chosen. */
3774
+ listing: place.chatId === null && !place.fresh && chats.length > 1,
3775
+ enter,
3776
+ loaded,
3777
+ error,
3778
+ setError,
3779
+ unseen,
3780
+ arrived
3781
+ };
3545
3782
  };
3546
3783
  var useArticles = (client, on, query) => {
3547
3784
  const [list, setList] = (0, import_react3.useState)(null);
@@ -3549,7 +3786,7 @@ var useArticles = (client, on, query) => {
3549
3786
  (0, import_react3.useEffect)(() => {
3550
3787
  if (!on) return;
3551
3788
  let stop = false;
3552
- const t = setTimeout(
3789
+ const t2 = setTimeout(
3553
3790
  () => void client.ready.then(
3554
3791
  () => client.api()?.articles(query.trim() || void 0).then((r) => {
3555
3792
  if (stop) return;
@@ -3561,27 +3798,27 @@ var useArticles = (client, on, query) => {
3561
3798
  );
3562
3799
  return () => {
3563
3800
  stop = true;
3564
- clearTimeout(t);
3801
+ clearTimeout(t2);
3565
3802
  };
3566
3803
  }, [client, on, query]);
3567
3804
  return { list, any };
3568
3805
  };
3569
3806
  var spansOf = (p, spans, base) => spans.map(
3570
- (s, i) => (0, import_react3.createElement)(
3807
+ (s2, i) => (0, import_react3.createElement)(
3571
3808
  RN3.Text,
3572
3809
  {
3573
3810
  key: i,
3574
- onPress: s.href ? () => void RN3.Linking.openURL(s.href).catch(() => void 0) : void 0,
3575
- accessibilityRole: s.href ? "link" : void 0,
3811
+ onPress: s2.href ? () => void RN3.Linking.openURL(s2.href).catch(() => void 0) : void 0,
3812
+ accessibilityRole: s2.href ? "link" : void 0,
3576
3813
  style: {
3577
3814
  ...base,
3578
- ...s.bold ? { fontWeight: "700" } : {},
3579
- ...s.italic ? { fontStyle: "italic" } : {},
3580
- ...s.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
3581
- ...s.href ? { color: p.accent, textDecorationLine: "underline" } : {}
3815
+ ...s2.bold ? { fontWeight: "700" } : {},
3816
+ ...s2.italic ? { fontStyle: "italic" } : {},
3817
+ ...s2.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
3818
+ ...s2.href ? { color: p.accent, textDecorationLine: "underline" } : {}
3582
3819
  }
3583
3820
  },
3584
- s.text
3821
+ s2.text
3585
3822
  )
3586
3823
  );
3587
3824
  var ArticleView = ({ client, p, id, onAsk }) => {
@@ -3595,7 +3832,7 @@ var ArticleView = ({ client, p, id, onAsk }) => {
3595
3832
  return () => ctrl.abort();
3596
3833
  }, [client, id]);
3597
3834
  if (error) return k.text(error, { color: p.danger });
3598
- if (!a) return k.muted("Loading\u2026");
3835
+ if (!a) return k.muted(t("Loading\u2026"));
3599
3836
  const base = { color: p.fg, fontSize: 15, lineHeight: 22 };
3600
3837
  const blocks = a.blocks.map((b, i) => {
3601
3838
  if (b.type === "h2" || b.type === "h3") return (0, import_react3.createElement)(RN3.Text, { key: i, accessibilityRole: "header", style: { ...base, fontSize: b.type === "h2" ? 17 : 16, fontWeight: "700", marginTop: 8 } }, ...spansOf(p, b.spans, {}));
@@ -3624,11 +3861,11 @@ var ArticleView = ({ client, p, id, onAsk }) => {
3624
3861
  (0, import_react3.createElement)(
3625
3862
  RN3.View,
3626
3863
  { style: { flexDirection: "row", alignItems: "center", gap: 8, padding: 12, borderRadius: 10, backgroundColor: p.soft, marginTop: 8 } },
3627
- rated ? k.muted("Thanks for letting us know.") : k.muted("Did this answer your question?", { flex: 1 }),
3628
- rated ? null : k.btn("Yes", "secondary", () => rate(true), false, { height: 34 }),
3629
- rated ? null : k.btn("No", "secondary", () => rate(false), false, { height: 34 })
3864
+ rated ? k.muted(t("Thanks for letting us know.")) : k.muted(t("Did this answer your question?"), { flex: 1 }),
3865
+ rated ? null : k.btn(t("Yes"), "secondary", () => rate(true), false, { height: 34 }),
3866
+ rated ? null : k.btn(t("No"), "secondary", () => rate(false), false, { height: 34 })
3630
3867
  ),
3631
- k.btn("Still need help? Send us a message", "secondary", onAsk)
3868
+ k.btn(t("Still need help? Send us a message"), "secondary", onAsk)
3632
3869
  );
3633
3870
  };
3634
3871
  var MessengerSheet = ({
@@ -3642,14 +3879,15 @@ var MessengerSheet = ({
3642
3879
  }) => {
3643
3880
  const k = kit(p);
3644
3881
  const [body, setBody] = (0, import_react3.useState)("");
3882
+ const [asked, setAsked] = (0, import_react3.useState)("");
3645
3883
  const [email, setEmail] = (0, import_react3.useState)("");
3646
3884
  const [pending, setPending] = (0, import_react3.useState)(null);
3647
3885
  const [slow, setSlow] = (0, import_react3.useState)(false);
3648
3886
  (0, import_react3.useEffect)(() => {
3649
3887
  setSlow(false);
3650
3888
  if (!pending) return;
3651
- const t = setTimeout(() => setSlow(true), 1500);
3652
- return () => clearTimeout(t);
3889
+ const t2 = setTimeout(() => setSlow(true), 1500);
3890
+ return () => clearTimeout(t2);
3653
3891
  }, [pending]);
3654
3892
  const [photos, setPhotos] = (0, import_react3.useState)([]);
3655
3893
  const [query, setQuery] = (0, import_react3.useState)("");
@@ -3657,24 +3895,25 @@ var MessengerSheet = ({
3657
3895
  const arts = useArticles(client, helpOn, query);
3658
3896
  const scroll = (0, import_react3.useRef)(null);
3659
3897
  const close = () => messengerOpen.set(false);
3660
- const greeting = o.greeting ?? "Hi! How can we help?";
3661
- const { thread } = chat;
3662
- const count = (thread?.messages.length ?? 0) + (pending ? 1 : 0);
3898
+ const greeting = o.greeting ?? t("Hi! How can we help?");
3899
+ const { thread, place } = chat;
3900
+ const pendingHere = pending && samePlace(pending.to, place) ? pending : null;
3901
+ const count = (thread?.messages.length ?? 0) + (pendingHere ? 1 : 0);
3663
3902
  (0, import_react3.useEffect)(() => {
3664
3903
  if (view === "messages") setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 50);
3665
- }, [count, view]);
3666
- const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
3904
+ }, [count, view, place]);
3905
+ const needEmail = (o.askEmail ?? true) && !chat.hasEmail && !client.getUser()?.email;
3667
3906
  const emailOk = !email.trim() || EMAIL.test(email.trim());
3668
3907
  const pickPhotos = async () => {
3669
3908
  const api = client.api();
3670
3909
  if (!api || !imagePicker?.launchImageLibraryAsync || pending) return;
3671
3910
  const room = CHAT_FILES_PER_MESSAGE - photos.length;
3672
- if (room <= 0) return chat.setError(`Up to ${CHAT_FILES_PER_MESSAGE} files per message.`);
3911
+ if (room <= 0) return chat.setError(tf("Up to {n} files per message.", { n: CHAT_FILES_PER_MESSAGE }));
3673
3912
  let res;
3674
3913
  try {
3675
3914
  res = await imagePicker.launchImageLibraryAsync({ allowsMultipleSelection: room > 1, selectionLimit: room, quality: 0.8, exif: false });
3676
3915
  } catch (e) {
3677
- return chat.setError(e.message || "The photos could not be opened.");
3916
+ return chat.setError(e.message || t("The photos could not be opened."));
3678
3917
  }
3679
3918
  if (res.canceled || res.cancelled || !res.assets?.length) return;
3680
3919
  chat.setError("");
@@ -3683,11 +3922,11 @@ var MessengerSheet = ({
3683
3922
  const name = a.fileName || a.uri.split("/").pop() || "photo.jpg";
3684
3923
  const type = a.mimeType && Object.values(PHOTO_TYPES).includes(a.mimeType) ? a.mimeType : PHOTO_TYPES[name.split(".").pop()?.toLowerCase() ?? ""];
3685
3924
  if (!type) {
3686
- chat.setError("Only pictures (PNG, JPEG, WebP, GIF) can be sent.");
3925
+ chat.setError(t("Only pictures (PNG, JPEG, WebP, GIF) can be sent."));
3687
3926
  continue;
3688
3927
  }
3689
3928
  if (a.fileSize && a.fileSize > CHAT_FILE_MAX_BYTES) {
3690
- chat.setError(`${name} is larger than 10 MB.`);
3929
+ chat.setError(tf("{name} is larger than 10 MB.", { name }));
3691
3930
  continue;
3692
3931
  }
3693
3932
  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() };
@@ -3706,29 +3945,29 @@ var MessengerSheet = ({
3706
3945
  setPhotos((list) => [...list, ...added]);
3707
3946
  };
3708
3947
  const canSend = (!!body.trim() || photos.length > 0) && !pending && emailOk;
3709
- const send = async () => {
3710
- const text2 = body.trim();
3948
+ const deliver = async (text2, files, to) => {
3711
3949
  const api = client.api();
3712
- const files = photos;
3713
- if (!text2 && !files.length || pending || !emailOk || !api) return;
3950
+ if (!api) return;
3714
3951
  setPending({
3715
3952
  id: "pending",
3716
3953
  from: "visitor",
3717
3954
  body: text2,
3718
3955
  at: (/* @__PURE__ */ new Date()).toISOString(),
3956
+ to,
3719
3957
  ...files.length ? { attachments: files.map((f) => ({ id: f.key, name: f.name, type: f.type, size: f.size })) } : {}
3720
3958
  });
3721
- setBody("");
3722
- setPhotos([]);
3723
3959
  chat.setError("");
3724
3960
  try {
3725
3961
  await Promise.all(files.map((f) => f.up));
3726
3962
  const ids = files.filter((f) => f.id).map((f) => f.id);
3727
- if (!text2 && !ids.length) throw new Error("The photo could not be sent.");
3963
+ if (!text2 && !ids.length) throw new Error(t("The photo could not be sent."));
3728
3964
  const win = RN3.Dimensions.get("screen");
3729
- chat.take(
3965
+ const conversation = to.fresh ? "new" : to.chatId ?? thread?.conversation?.id;
3966
+ chat.arrived(
3967
+ to,
3730
3968
  await api.sendChat({
3731
3969
  body: text2,
3970
+ ...conversation ? { conversation } : {},
3732
3971
  email: needEmail && email.trim() ? email.trim() : void 0,
3733
3972
  ...ids.length ? { attachments: ids } : {},
3734
3973
  // A little context for a bug report: the system and the screen size.
@@ -3736,13 +3975,44 @@ var MessengerSheet = ({
3736
3975
  })
3737
3976
  );
3738
3977
  } catch (e) {
3739
- setBody(text2);
3740
- setPhotos(files.filter((f) => f.id));
3741
- chat.setError(e.message);
3978
+ const gone = e.status === 404;
3979
+ chat.enter(gone ? null : to.chatId, gone || to.fresh);
3980
+ setBody((typed) => [text2, typed.trim()].filter(Boolean).join("\n"));
3981
+ setPhotos((list) => [...files.filter((f) => f.id), ...list]);
3982
+ chat.setError(gone ? t("This conversation is no longer available. Your message is still here to send as a new one.") : e.message);
3742
3983
  } finally {
3743
3984
  setPending(null);
3744
3985
  }
3745
3986
  };
3987
+ const send = () => {
3988
+ const text2 = body.trim();
3989
+ const files = photos;
3990
+ if (!text2 && !files.length || pending || !emailOk || !client.api()) return;
3991
+ setBody("");
3992
+ setPhotos([]);
3993
+ void deliver(text2, files, place);
3994
+ };
3995
+ const openMessages = () => {
3996
+ chat.enter(null);
3997
+ setView("messages");
3998
+ };
3999
+ const openChat = (id) => {
4000
+ chat.enter(id);
4001
+ setView("messages");
4002
+ };
4003
+ const startChat = () => {
4004
+ chat.enter(null, true);
4005
+ setView("messages");
4006
+ };
4007
+ const ask = () => {
4008
+ const text2 = asked.trim();
4009
+ if (!text2) return openMessages();
4010
+ if (pending || !client.api()) return;
4011
+ setAsked("");
4012
+ startChat();
4013
+ if (emailOk) void deliver(text2, [], { chatId: null, fresh: true });
4014
+ else setBody((typed) => [text2, typed.trim()].filter(Boolean).join("\n"));
4015
+ };
3746
4016
  const localUris = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
3747
4017
  for (const f of photos) localUris.current.set(f.key, f.uri);
3748
4018
  const fileUrl = (id) => localUris.current.get(id) ?? client.api()?.attachmentUrl(id) ?? "";
@@ -3797,7 +4067,7 @@ var MessengerSheet = ({
3797
4067
  k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
3798
4068
  )
3799
4069
  ),
3800
- m.at ? k.muted(`${mine ? "" : `${who(m.from, o.agentName)} \xB7 `}${isPending && slow ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
4070
+ m.at ? k.muted(`${mine ? "" : `${who(m.from, o.agentName)} \xB7 `}${isPending && slow ? t("Sending\u2026") : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
3801
4071
  );
3802
4072
  };
3803
4073
  const row = (a) => (0, import_react3.createElement)(
@@ -3810,55 +4080,76 @@ var MessengerSheet = ({
3810
4080
  const searchRow = (onPress) => (0, import_react3.createElement)(
3811
4081
  RN3.Pressable,
3812
4082
  { key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: r.field, paddingHorizontal: 14, backgroundColor: hu.def } },
3813
- k.muted("Search for help", { flex: 1, fontSize: 15 }),
4083
+ k.muted(t("Search for help"), { flex: 1, fontSize: 15 }),
3814
4084
  glyph("search", p.accent, 16)
3815
4085
  );
3816
- const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: r.field, backgroundColor: raised, ...surf } });
4086
+ const search = k.field({ value: query, onChange: setQuery, placeholder: t("Search for help"), max: 200, style: { borderWidth: 0, borderRadius: r.field, backgroundColor: raised, ...surf } });
3817
4087
  const first = client.getUser()?.name?.trim().split(/\s+/)[0];
3818
- const rest = greeting.replace(/^(hi|hello|hey|hallo)( there)?\s*[!,.]*\s*/i, "").trim() || "How can we help you today?";
4088
+ const rest = (o.greeting !== void 0 ? greeting.replace(/^(hi|hello|hey|hallo|moin|servus|bonjour|salut|hola|ciao|salve|hoi|ol[áa]|cze[śs][ćc]|hej|こんにちは)( there)?\s*[!,.!、。]*\s*/i, "").trim() : t("How can we help?")) || t("How can we help you today?");
3819
4089
  const hero = (fg, sub) => (0, import_react3.createElement)(
3820
4090
  RN3.Text,
3821
4091
  { key: "hero", accessibilityRole: "header", style: { color: fg, fontSize: 25, fontWeight: "600", letterSpacing: -0.6, lineHeight: 30 } },
3822
- first ? `Hi, ${first} \u{1F44B}
3823
- ` : "Hi there \u{1F44B}\n",
4092
+ `${first ? tf("Hi, {name}", { name: first }) : t("Hi there")} \u{1F44B}
4093
+ `,
3824
4094
  (0, import_react3.createElement)(RN3.Text, { style: { color: sub, fontWeight: "500" } }, rest)
3825
4095
  );
3826
- const last = thread?.messages.at(-1);
4096
+ const chatRow = (c) => {
4097
+ const m = c.last;
4098
+ const n = chat.unseen.filter((u) => u.chat === c.id).length;
4099
+ const closed = c.status === "solved" ? ` \xB7 ${t("Closed")}` : "";
4100
+ const subject = c.subject || (m ? lineOf(m) : "");
4101
+ const line = m ? lineOf(m) : "";
4102
+ return (0, import_react3.createElement)(
4103
+ RN3.Pressable,
4104
+ { key: c.id, onPress: () => openChat(c.id), accessibilityRole: "button", accessibilityLabel: `${tf("Conversation: {subject}", { subject })}${closed}${n ? `, ${tf("{n} new", { n })}` : ""}` },
4105
+ card(
4106
+ [
4107
+ (0, import_react3.createElement)(
4108
+ RN3.View,
4109
+ { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 10 } },
4110
+ title ? teamMark(title, alpha(p.accent, 0.16), p.accent, 26) : null,
4111
+ (0, import_react3.createElement)(
4112
+ RN3.View,
4113
+ { style: { flex: 1, gap: 1 } },
4114
+ (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.fg, fontSize: 14, fontWeight: "500" } }, subject),
4115
+ m && line !== subject ? (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.muted, fontSize: 13 } }, line) : null,
4116
+ m ? k.muted(`${who(m.from, o.agentName)} \xB7 ${timeAgo(m.at)}${closed}`, { fontSize: 11.5 }) : null
4117
+ ),
4118
+ n ? (0, import_react3.createElement)(RN3.View, { style: { minWidth: 20, height: 20, paddingHorizontal: 5, borderRadius: 10, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(Math.min(n, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : glyph("next", p.muted, 14)
4119
+ )
4120
+ ],
4121
+ { paddingVertical: 10, paddingHorizontal: 12 }
4122
+ )
4123
+ );
4124
+ };
3827
4125
  const showHelp = helpOn && arts.any !== false;
3828
4126
  let content;
3829
4127
  if (view === "home") {
4128
+ const running = chat.chats.filter((c) => c.status !== "solved");
4129
+ const rows = (running.length ? running : chat.chats).slice(0, 2);
3830
4130
  const cards = [
3831
- // "Ask a question": the words go into the chat's field and are sent as the chat opens.
4131
+ // "Ask a question": its own field; the words start a new conversation.
3832
4132
  (0, import_react3.createElement)(
3833
4133
  RN3.View,
3834
4134
  { key: "ask", style: { flexDirection: "row", alignItems: "center", gap: 8, height: 48, borderRadius: 24, paddingLeft: 18, paddingRight: 6, backgroundColor: hu.surface, ...surf } },
3835
- k.field({ value: body, onChange: setBody, placeholder: "Ask a question", max: 4e3, style: { flex: 1, borderWidth: 0, paddingHorizontal: 0, backgroundColor: "transparent" } }),
4135
+ k.field({ value: asked, onChange: setAsked, placeholder: t("Ask a question"), max: 4e3, onSubmit: ask, style: { flex: 1, borderWidth: 0, paddingHorizontal: 0, backgroundColor: "transparent" } }),
3836
4136
  (0, import_react3.createElement)(
3837
4137
  RN3.Pressable,
3838
4138
  {
3839
- onPress: () => {
3840
- setView("messages");
3841
- if (body.trim()) void send();
3842
- },
4139
+ onPress: ask,
3843
4140
  accessibilityRole: "button",
3844
- accessibilityLabel: "Send",
3845
- style: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: body.trim() ? p.accent : hu.def }
4141
+ accessibilityLabel: t("Send"),
4142
+ style: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: asked.trim() ? p.accent : hu.def }
3846
4143
  },
3847
- glyph("up", body.trim() ? ink : hu.muted, 18)
4144
+ glyph("up", asked.trim() ? ink : hu.muted, 18)
3848
4145
  )
3849
4146
  ),
3850
- last ? (0, import_react3.createElement)(
4147
+ ...rows.map(chatRow),
4148
+ chat.chats.length > rows.length ? (0, import_react3.createElement)(
3851
4149
  RN3.Pressable,
3852
- { key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
3853
- card([
3854
- (0, import_react3.createElement)(
3855
- RN3.View,
3856
- { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 10 } },
3857
- title ? teamMark(title, alpha(p.accent, 0.16), p.accent, 26) : null,
3858
- (0, import_react3.createElement)(RN3.View, { style: { flex: 1, gap: 1 } }, (0, import_react3.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 })),
3859
- chat.unseen.length ? (0, import_react3.createElement)(RN3.View, { style: { width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : glyph("next", p.muted, 14)
3860
- )
3861
- ], { paddingVertical: 10, paddingHorizontal: 12 })
4150
+ { key: "all", onPress: openMessages, accessibilityRole: "button", accessibilityLabel: tf("All conversations ({n})", { n: chat.chats.length }), style: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 4, paddingVertical: 6 } },
4151
+ k.muted(tf("All conversations ({n})", { n: chat.chats.length }), { fontSize: 13, fontWeight: "500" }),
4152
+ glyph("next", p.muted, 12)
3862
4153
  ) : null,
3863
4154
  showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
3864
4155
  ];
@@ -3873,7 +4164,14 @@ var MessengerSheet = ({
3873
4164
  (0, import_react3.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, hero(ink, alpha(ink, 0.8))),
3874
4165
  (0, import_react3.createElement)(RN3.View, { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } }, ...cards)
3875
4166
  );
3876
- } else if (view === "messages")
4167
+ } else if (view === "messages" && chat.listing)
4168
+ content = (0, import_react3.createElement)(
4169
+ RN3.ScrollView,
4170
+ { key: "chats", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 } },
4171
+ ...chat.chats.map(chatRow),
4172
+ k.btn(t("New conversation"), "primary", startChat)
4173
+ );
4174
+ else if (view === "messages")
3877
4175
  content = (0, import_react3.createElement)(
3878
4176
  RN3.View,
3879
4177
  { key: "messages", style: { flex: 1, backgroundColor: hu.bg } },
@@ -3881,11 +4179,11 @@ var MessengerSheet = ({
3881
4179
  RN3.ScrollView,
3882
4180
  { ref: scroll, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 } },
3883
4181
  // The greeting opens an empty chat only, never above the visitor's first words (2026-10-30).
3884
- !thread?.messages.length && !pending ? bubble({ id: "greeting", from: "team", body: greeting, at: "" }) : null,
3885
- !chat.loaded ? k.muted("Loading\u2026") : null,
4182
+ !thread?.messages.length && !pendingHere && !place.chatId ? bubble({ id: "greeting", from: "team", body: greeting, at: "" }) : null,
4183
+ !chat.loaded && !place.fresh && !pendingHere || place.chatId && !thread ? k.muted(t("Loading\u2026")) : null,
3886
4184
  ...(thread?.messages ?? []).map((m) => bubble(m)),
3887
- pending ? bubble(pending, true) : null,
3888
- thread?.conversation?.status === "solved" && !pending ? k.muted("This conversation is closed. Write to start a new one.", { textAlign: "center" }) : null
4185
+ pendingHere ? bubble(pendingHere, true) : null,
4186
+ thread?.conversation?.status === "solved" && !pendingHere ? k.muted(t("This conversation is closed. Write to start a new one."), { textAlign: "center" }) : null
3889
4187
  ),
3890
4188
  (0, import_react3.createElement)(
3891
4189
  RN3.View,
@@ -3899,13 +4197,13 @@ var MessengerSheet = ({
3899
4197
  (f) => (0, import_react3.createElement)(
3900
4198
  RN3.View,
3901
4199
  { key: f.key, style: { width: 56, height: 56 } },
3902
- (0, import_react3.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 } }),
4200
+ (0, import_react3.createElement)(RN3.Image, { source: { uri: f.uri }, resizeMode: "cover", accessibilityLabel: f.id ? f.name : tf("{name}, uploading", { name: f.name }), style: { width: 56, height: 56, borderRadius: Math.max(4, r.card - 4), backgroundColor: p.soft, opacity: f.id ? 1 : 0.5 } }),
3903
4201
  (0, import_react3.createElement)(
3904
4202
  RN3.Pressable,
3905
4203
  {
3906
4204
  onPress: () => setPhotos((list) => list.filter((x) => x.key !== f.key)),
3907
4205
  accessibilityRole: "button",
3908
- accessibilityLabel: `Remove ${f.name}`,
4206
+ accessibilityLabel: tf("Remove {name}", { name: f.name }),
3909
4207
  hitSlop: 8,
3910
4208
  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" }
3911
4209
  },
@@ -3922,28 +4220,28 @@ var MessengerSheet = ({
3922
4220
  needEmail ? k.field({
3923
4221
  value: email,
3924
4222
  onChange: setEmail,
3925
- placeholder: "Email for our reply (optional)",
4223
+ placeholder: t("Email for our reply (optional)"),
3926
4224
  email: true,
3927
4225
  max: 200,
3928
4226
  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 }
3929
4227
  }) : null,
3930
- 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" } }),
4228
+ k.field({ value: body, onChange: setBody, placeholder: t("Write a message\u2026"), multiline: true, max: 4e3, style: { minHeight: 28, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 4, backgroundColor: "transparent" } }),
3931
4229
  (0, import_react3.createElement)(
3932
4230
  RN3.View,
3933
4231
  { style: { flexDirection: "row", alignItems: "center", marginLeft: -8 } },
3934
4232
  imagePicker?.launchImageLibraryAsync ? (0, import_react3.createElement)(
3935
4233
  RN3.Pressable,
3936
- { 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" } },
4234
+ { onPress: () => void pickPhotos(), disabled: !!pending, accessibilityRole: "button", accessibilityLabel: t("Attach a photo or screenshot"), hitSlop: 6, style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center" } },
3937
4235
  glyph("clip", hu.muted, 19)
3938
4236
  ) : null,
3939
4237
  (0, import_react3.createElement)(RN3.View, { style: { flex: 1 } }),
3940
4238
  (0, import_react3.createElement)(
3941
4239
  RN3.Pressable,
3942
4240
  {
3943
- onPress: () => void send(),
4241
+ onPress: send,
3944
4242
  disabled: !canSend,
3945
4243
  accessibilityRole: "button",
3946
- accessibilityLabel: "Send",
4244
+ accessibilityLabel: t("Send"),
3947
4245
  style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: canSend ? p.accent : hu.def }
3948
4246
  },
3949
4247
  glyph("up", canSend ? ink : hu.muted, 18)
@@ -3959,15 +4257,15 @@ var MessengerSheet = ({
3959
4257
  RN3.ScrollView,
3960
4258
  { key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3961
4259
  search,
3962
- arts.list === null ? k.muted("Loading\u2026") : null,
3963
- arts.list?.length === 0 ? (0, import_react3.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,
4260
+ arts.list === null ? k.muted(t("Loading\u2026")) : null,
4261
+ arts.list?.length === 0 ? (0, import_react3.createElement)(RN3.View, { style: { gap: 10, alignItems: "center", paddingVertical: 24 } }, k.muted(query.trim() ? tf("Nothing found for \u201C{q}\u201D.", { q: query.trim() }) : t("No help articles yet.")), k.btn(t("Send us a message"), "primary", startChat)) : null,
3964
4262
  ...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row), { padding: 8 })] : [] : [...groups].map(([name, items]) => (0, import_react3.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`, { fontSize: 12, fontWeight: "600", paddingHorizontal: 2 }), card(items.map(row), { padding: 8 })))
3965
4263
  );
3966
4264
  } else
3967
4265
  content = (0, import_react3.createElement)(
3968
4266
  RN3.ScrollView,
3969
4267
  { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 32 } },
3970
- (0, import_react3.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
4268
+ (0, import_react3.createElement)(ArticleView, { client, p, id: view.article, onAsk: startChat })
3971
4269
  );
3972
4270
  const current = typeof view === "string" ? view : "help";
3973
4271
  const tab = (v, name, icon, badge = 0) => {
@@ -3976,7 +4274,7 @@ var MessengerSheet = ({
3976
4274
  RN3.Pressable,
3977
4275
  {
3978
4276
  key: v,
3979
- onPress: () => setView(v),
4277
+ onPress: () => v === "messages" ? openMessages() : setView(v),
3980
4278
  accessibilityRole: "tab",
3981
4279
  accessibilityLabel: name,
3982
4280
  accessibilityState: { selected: on },
@@ -3992,21 +4290,21 @@ var MessengerSheet = ({
3992
4290
  const ground = onAccent ? p.accent : hu.bg;
3993
4291
  const back = (to, label) => (0, import_react3.createElement)(
3994
4292
  RN3.Pressable,
3995
- { key: "back", onPress: () => setView(to), accessibilityRole: "button", accessibilityLabel: label, hitSlop: 10, style: { width: 32, height: 32, alignItems: "center", justifyContent: "center", transform: [{ rotate: "180deg" }] } },
4293
+ { key: "back", onPress: to, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 10, style: { width: 32, height: 32, alignItems: "center", justifyContent: "center", transform: [{ rotate: "180deg" }] } },
3996
4294
  glyph("next", hu.fg, 18)
3997
4295
  );
3998
4296
  const head = (0, import_react3.createElement)(
3999
4297
  RN3.View,
4000
4298
  { key: "head", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingLeft: 12, paddingRight: 12, paddingTop: 12, paddingBottom: 6, backgroundColor: ground } },
4001
- typeof view !== "string" ? back("help", "All articles") : view === "messages" ? back("home", "Back to home") : view === "home" && (o.logo || title) ? o.logo ? (0, import_react3.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,
4002
- // In the chat: who answers, in the middle.
4003
- view === "messages" && title ? (0, import_react3.createElement)(
4299
+ typeof view !== "string" ? back(() => setView("help"), t("All articles")) : view === "messages" && !chat.listing && chat.chats.length > 1 ? back(openMessages, t("All conversations")) : view === "messages" ? back(() => setView("home"), t("Back to home")) : view === "home" && (o.logo || title) ? o.logo ? (0, import_react3.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,
4300
+ // In the chat: who answers, in the middle; over the list its name.
4301
+ view === "messages" && chat.listing ? (0, import_react3.createElement)(RN3.View, { style: { flex: 1, alignItems: "center" } }, k.text(t("Messages"), { color: hu.fg, fontSize: 15, fontWeight: "600" })) : view === "messages" && title ? (0, import_react3.createElement)(
4004
4302
  RN3.View,
4005
4303
  { style: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8 } },
4006
4304
  teamMark(title, alpha(p.accent, 0.16), p.accent, 28),
4007
4305
  (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { flexShrink: 1, color: hu.fg, fontSize: 15, fontWeight: "600" } }, title)
4008
4306
  ) : (0, import_react3.createElement)(RN3.View, { style: { flex: 1 } }),
4009
- (0, import_react3.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))
4307
+ (0, import_react3.createElement)(RN3.Pressable, { onPress: close, accessibilityRole: "button", accessibilityLabel: t("Close"), hitSlop: 12, style: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" } }, glyph("cross", onAccent ? ink : p.muted, 18))
4010
4308
  );
4011
4309
  const powered = brand ? (0, import_react3.createElement)(
4012
4310
  RN3.View,
@@ -4014,8 +4312,9 @@ var MessengerSheet = ({
4014
4312
  (0, import_react3.createElement)(
4015
4313
  RN3.Text,
4016
4314
  { onPress: () => void RN3.Linking.openURL(BRAND_URL).catch(() => void 0), accessibilityRole: "link", hitSlop: 8, style: { color: p.muted, fontSize: 11.5 } },
4017
- "Powered by ",
4018
- (0, import_react3.createElement)(RN3.Text, { style: { fontWeight: "600" } }, "MRRocket")
4315
+ t("Powered by {brand}").split("{brand}")[0],
4316
+ (0, import_react3.createElement)(RN3.Text, { style: { fontWeight: "600" } }, "MRRocket"),
4317
+ t("Powered by {brand}").split("{brand}")[1]
4019
4318
  )
4020
4319
  ) : null;
4021
4320
  return (0, import_react3.createElement)(
@@ -4026,12 +4325,12 @@ var MessengerSheet = ({
4026
4325
  { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: hu.bg } },
4027
4326
  head,
4028
4327
  content,
4029
- view === "messages" ? null : (0, import_react3.createElement)(
4328
+ view === "messages" && !chat.listing ? null : (0, import_react3.createElement)(
4030
4329
  RN3.View,
4031
4330
  { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", marginHorizontal: 12, marginTop: 6, marginBottom: 6, padding: 4, borderRadius: 22, backgroundColor: hu.surface } },
4032
- tab("home", "Home", "home"),
4033
- tab("messages", "Messages", "chat", chat.unseen.length),
4034
- showHelp ? tab("help", "Help", "help") : null
4331
+ tab("home", t("Home"), "home"),
4332
+ tab("messages", t("Messages"), "chat", chat.unseen.length),
4333
+ showHelp ? tab("help", t("Help"), "help") : null
4035
4334
  ),
4036
4335
  powered,
4037
4336
  (0, import_react3.createElement)(RN3.View, { style: { height: 16 } })
@@ -4046,7 +4345,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4046
4345
  () => client.api()?.messengerLook(ctrl.signal).then(setLook).catch(() => void 0)
4047
4346
  );
4048
4347
  return () => ctrl.abort();
4049
- }, [client]);
4348
+ }, [client, wordsVersion()]);
4050
4349
  const defined = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== void 0));
4051
4350
  const options = look ? {
4052
4351
  title: look.title,
@@ -4070,25 +4369,29 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4070
4369
  const open = messengerOpen.useOpen();
4071
4370
  const [view, setViewState] = (0, import_react3.useState)(msgView);
4072
4371
  const [banner, setBanner] = (0, import_react3.useState)(null);
4073
- (0, import_react3.useEffect)(() => {
4074
- if (open) setViewState(msgView);
4075
- }, [open]);
4076
4372
  const setView = (v) => {
4077
4373
  msgView = v;
4078
4374
  setViewState(v);
4079
4375
  };
4080
4376
  const chat = useChat(client, options, open, view);
4377
+ const { enter } = chat;
4378
+ (0, import_react3.useEffect)(() => {
4379
+ if (!open) return;
4380
+ setViewState(msgView);
4381
+ if (msgView === "messages") enter(msgChat);
4382
+ }, [open, enter]);
4081
4383
  const latest = chat.unseen.at(-1);
4082
4384
  (0, import_react3.useEffect)(() => {
4083
4385
  if (!latest || open || options.preview === false) return;
4084
4386
  setBanner(latest);
4085
- const t = setTimeout(() => setBanner(null), 6e3);
4086
- return () => clearTimeout(t);
4387
+ const t2 = setTimeout(() => setBanner(null), 6e3);
4388
+ return () => clearTimeout(t2);
4087
4389
  }, [latest, open, options.preview]);
4088
4390
  (0, import_react3.useEffect)(() => {
4089
4391
  if (open) setBanner(null);
4090
4392
  }, [open]);
4091
4393
  const unseen = chat.unseen.length;
4394
+ const news = latest?.chat ?? null;
4092
4395
  const ink = inkOn(p.accent);
4093
4396
  const label = options.label?.trim() ?? "";
4094
4397
  const r = CORNERS[options.corners ?? "round"];
@@ -4103,9 +4406,9 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4103
4406
  (0, import_react3.createElement)(
4104
4407
  RN3.Pressable,
4105
4408
  {
4106
- onPress: () => openAt(unseen ? "messages" : "home"),
4409
+ onPress: () => openAt(news ? "messages" : "home", news),
4107
4410
  accessibilityRole: "button",
4108
- accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
4411
+ accessibilityLabel: unseen === 1 ? t("Open chat, 1 new reply") : unseen ? tf("Open chat, {n} new replies", { n: unseen }) : t("Open chat"),
4109
4412
  style: {
4110
4413
  height: label ? 50 : 56,
4111
4414
  minWidth: label ? 0 : 56,
@@ -4132,7 +4435,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4132
4435
  RN3.Pressable,
4133
4436
  {
4134
4437
  key: "banner",
4135
- onPress: () => openAt("messages"),
4438
+ onPress: () => openAt("messages", banner.chat),
4136
4439
  accessibilityRole: "button",
4137
4440
  accessibilityLiveRegion: "polite",
4138
4441
  style: {
@@ -4152,7 +4455,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4152
4455
  elevation: 6
4153
4456
  }
4154
4457
  },
4155
- k.muted(`${who(banner.from, options.agentName)}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
4458
+ k.muted(`${who(banner.from, options.agentName)}${unseen > 1 ? ` \xB7 ${tf("{n} new", { n: unseen })}` : ""}`, { fontWeight: "600", fontSize: 12 }),
4156
4459
  (0, import_react3.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15, lineHeight: 20 } }, banner.body)
4157
4460
  ) : null;
4158
4461
  if (!launcher && !toast && !open) return null;
@@ -4195,6 +4498,10 @@ var getMRRocket = () => shared;
4195
4498
  var Ctx = (0, import_react4.createContext)(null);
4196
4499
  var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, surveys = true, messenger = {}, feedback = {}, heatmaps = true, ...options }) => {
4197
4500
  const [client] = (0, import_react4.useState)(() => given ?? createMRRocket({ ...options, consent }));
4501
+ (0, import_react4.useSyncExternalStore)(onWords, wordsVersion, wordsVersion);
4502
+ (0, import_react4.useEffect)(() => {
4503
+ if (options.locale) setLocale(options.locale);
4504
+ }, [options.locale]);
4198
4505
  (0, import_react4.useEffect)(() => {
4199
4506
  if (consent) client.setConsent(consent);
4200
4507
  }, [client, consent]);
@@ -4263,6 +4570,7 @@ var useOnboarding = (id) => {
4263
4570
  const client = useClient();
4264
4571
  return useOnboardingItem(client.flows, id, client.startOnboarding);
4265
4572
  };
4573
+ var useOnboardingReport = (name, options) => useReportWith(useClient().track, name, options);
4266
4574
  var useFlag = (key) => {
4267
4575
  const client = useClient();
4268
4576
  useVersion(client);