@mrrocket/sdk 0.17.0 → 0.21.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 (69) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +24 -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 +15 -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 +28 -3
  13. package/dist/client.js +37 -5
  14. package/dist/client.js.map +1 -1
  15. package/dist/context.d.ts +3 -1
  16. package/dist/context.js.map +1 -1
  17. package/dist/core.d.ts +7 -1
  18. package/dist/core.js +1 -1
  19. package/dist/core.js.map +1 -1
  20. package/dist/feedback.js +161 -119
  21. package/dist/feedback.js.map +1 -1
  22. package/dist/finish-web.d.ts +18 -0
  23. package/dist/finish-web.js +139 -0
  24. package/dist/finish-web.js.map +1 -0
  25. package/dist/flows-ui.d.ts +1 -10
  26. package/dist/flows-ui.js +175 -48
  27. package/dist/flows-ui.js.map +1 -1
  28. package/dist/flows-web.d.ts +8 -1
  29. package/dist/flows-web.js +10 -1
  30. package/dist/flows-web.js.map +1 -1
  31. package/dist/flows.d.ts +193 -9
  32. package/dist/flows.js +284 -38
  33. package/dist/flows.js.map +1 -1
  34. package/dist/i18n.d.ts +22 -0
  35. package/dist/i18n.js +74 -0
  36. package/dist/i18n.js.map +1 -0
  37. package/dist/icons.d.ts +24 -0
  38. package/dist/icons.js +61 -0
  39. package/dist/icons.js.map +1 -0
  40. package/dist/index.d.ts +6 -2
  41. package/dist/index.js +8 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/messenger-ui.d.ts +9 -3
  44. package/dist/messenger-ui.js +388 -191
  45. package/dist/messenger-ui.js.map +1 -1
  46. package/dist/messenger.d.ts +3 -1
  47. package/dist/messenger.js +3 -2
  48. package/dist/messenger.js.map +1 -1
  49. package/dist/onboarding-state.d.ts +23 -0
  50. package/dist/onboarding-state.js +43 -0
  51. package/dist/onboarding-state.js.map +1 -0
  52. package/dist/paywall.js +2 -1
  53. package/dist/paywall.js.map +1 -1
  54. package/dist/react-native/index.d.cts +239 -19
  55. package/dist/react-native/index.d.ts +239 -19
  56. package/dist/react-native/index.js +1305 -518
  57. package/dist/react-native/index.js.map +1 -1
  58. package/dist/server.d.ts +4 -2
  59. package/dist/server.js +2 -2
  60. package/dist/server.js.map +1 -1
  61. package/dist/styles-css.d.ts +1 -1
  62. package/dist/styles-css.js +35 -2
  63. package/dist/styles-css.js.map +1 -1
  64. package/dist/survey.js +18 -14
  65. package/dist/survey.js.map +1 -1
  66. package/dist/ui.d.ts +2 -0
  67. package/dist/ui.js +15 -12
  68. package/dist/ui.js.map +1 -1
  69. 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,
@@ -48,10 +50,70 @@ __export(native_exports, {
48
50
  useVariant: () => useVariant
49
51
  });
50
52
  module.exports = __toCommonJS(native_exports);
51
- var import_react3 = require("react");
53
+ var import_react4 = require("react");
54
+
55
+ // src/i18n.ts
56
+ var g = globalThis;
57
+ var s = g.__mrrocketI18n ?? (g.__mrrocketI18n = { version: 0, onWords: /* @__PURE__ */ new Set(), onLocale: /* @__PURE__ */ new Set() });
58
+ var tell = (listeners) => {
59
+ for (const fn of [...listeners]) {
60
+ try {
61
+ fn();
62
+ } catch {
63
+ }
64
+ }
65
+ };
66
+ var t = (word) => s.words?.[word] || word;
67
+ var tf = (word, vars) => t(word).replace(/\{(\w+)\}/g, (all, k) => k in vars ? String(vars[k]) : all);
68
+ var setWords = (words, lang) => {
69
+ const same2 = s.lang === lang && JSON.stringify(s.words ?? {}) === JSON.stringify(words ?? {});
70
+ s.words = words;
71
+ s.lang = lang;
72
+ if (same2) return;
73
+ s.version++;
74
+ tell(s.onWords);
75
+ };
76
+ var tidy = (locale) => {
77
+ const tag = locale?.trim().replace(/_/g, "-");
78
+ if (!tag) return void 0;
79
+ try {
80
+ return Intl.getCanonicalLocales(tag)[0];
81
+ } catch (e) {
82
+ return e instanceof RangeError ? void 0 : tag;
83
+ }
84
+ };
85
+ var setLocale = (locale) => {
86
+ const next = tidy(locale);
87
+ if (next === s.locale) return;
88
+ s.locale = next;
89
+ s.version++;
90
+ tell(s.onWords);
91
+ tell(s.onLocale);
92
+ };
93
+ var setDeviceLocale = (locale) => {
94
+ s.device = tidy(locale);
95
+ };
96
+ var currentLocale = () => s.locale ?? s.device ?? (typeof navigator !== "undefined" && navigator.language || void 0);
97
+ var langParam = () => {
98
+ const l = currentLocale();
99
+ return l ? `&lang=${encodeURIComponent(l)}` : "";
100
+ };
101
+ var wordsVersion = () => s.version;
102
+ var onWords = (fn) => {
103
+ s.onWords.add(fn);
104
+ return () => {
105
+ s.onWords.delete(fn);
106
+ };
107
+ };
108
+ var onLocale = (fn) => {
109
+ s.onLocale.add(fn);
110
+ return () => {
111
+ s.onLocale.delete(fn);
112
+ };
113
+ };
52
114
 
53
115
  // src/core.ts
54
- var SDK_VERSION = "0.17.0";
116
+ var SDK_VERSION = "0.21.0";
55
117
  var propOf = (person, name) => {
56
118
  const want = name.trim().toLowerCase();
57
119
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -77,7 +139,7 @@ var matchesCondition = (c, person) => {
77
139
  var evaluateFlag = (flag, distinctId, person = {}) => {
78
140
  if (!flag.on) return false;
79
141
  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 };
142
+ const set = flag.sets.length ? flag.sets.find((s2) => s2.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };
81
143
  if (!set) return false;
82
144
  if (flagUnit(`${flag.key}:rollout:${distinctId}`) * 100 >= set.rollout) return false;
83
145
  if (!flag.variants?.length) return true;
@@ -108,11 +170,11 @@ var parseQuery = (url) => {
108
170
  const q = url.split("#")[0];
109
171
  const at = q.indexOf("?");
110
172
  if (at < 0) return out;
111
- const dec = (s) => {
173
+ const dec = (s2) => {
112
174
  try {
113
- return decodeURIComponent(s.replace(/\+/g, " "));
175
+ return decodeURIComponent(s2.replace(/\+/g, " "));
114
176
  } catch {
115
- return s;
177
+ return s2;
116
178
  }
117
179
  };
118
180
  for (const pair of q.slice(at + 1).split("&")) {
@@ -173,12 +235,52 @@ var textChanges = (config, distinctId, preview) => {
173
235
  };
174
236
  var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
175
237
 
238
+ // src/onboarding-state.ts
239
+ var import_react = require("react");
240
+ var none = [];
241
+ var useOnboardingItem = (flows, id, start) => {
242
+ const [state, setState] = (0, import_react.useState)(() => flows && id ? flows.onboarding.item(id) : null);
243
+ (0, import_react.useEffect)(() => {
244
+ if (!flows || !id) {
245
+ setState(null);
246
+ return;
247
+ }
248
+ const up = () => {
249
+ const s2 = flows.onboarding.item(id);
250
+ setState(s2);
251
+ if (s2 && !s2.done && !s2.dismissed) flows.onboarding.seen(id);
252
+ };
253
+ up();
254
+ return flows.subscribe(up);
255
+ }, [flows, id]);
256
+ return (0, import_react.useMemo)(
257
+ () => ({
258
+ item: state?.item ?? null,
259
+ steps: state?.steps ?? none,
260
+ done: !!state?.done,
261
+ dismissed: !!state?.dismissed,
262
+ eligible: !!state?.eligible,
263
+ complete: (stepId) => {
264
+ if (flows && id) flows.onboarding.complete(id, stepId);
265
+ },
266
+ dismiss: () => {
267
+ if (flows && id) flows.onboarding.dismissItem(id);
268
+ },
269
+ answer: (question, value) => {
270
+ if (flows && id) flows.onboarding.answerItem(id, question, value);
271
+ },
272
+ start
273
+ }),
274
+ [state, flows, id, start]
275
+ );
276
+ };
277
+
176
278
  // src/identity.ts
177
279
  var UNSIGNED_HELP = "401: sign the user id on your server with userHash(id) from @mrrocket/sdk/next/server and pass identify(id, props, { hash }).";
178
280
  var warnUnsigned = (status, log = (m) => console.warn(m)) => {
179
- const g = globalThis;
180
- if (status !== 401 || g.__mrrocketUnsigned) return;
181
- g.__mrrocketUnsigned = true;
281
+ const g2 = globalThis;
282
+ if (status !== 401 || g2.__mrrocketUnsigned) return;
283
+ g2.__mrrocketUnsigned = true;
182
284
  log(`[MRRocket] ${UNSIGNED_HELP}`);
183
285
  };
184
286
  var nextSigned = (signed, id, hash, log = (m) => console.warn(m)) => {
@@ -202,11 +304,11 @@ var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-b
202
304
  var inkOn = (hex) => {
203
305
  const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
204
306
  if (!m) return "#ffffff";
205
- const [r, g, b] = [m[1], m[2], m[3]].map((x) => {
307
+ const [r, g2, b] = [m[1], m[2], m[3]].map((x) => {
206
308
  const c = parseInt(x, 16) / 255;
207
309
  return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
208
310
  });
209
- return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
311
+ return 0.2126 * r + 0.7152 * g2 + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
210
312
  };
211
313
  var ApiError = class extends Error {
212
314
  constructor(message, status) {
@@ -215,7 +317,11 @@ var ApiError = class extends Error {
215
317
  this.status = status;
216
318
  }
217
319
  };
218
- var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
320
+ var qs = (params, lang = currentLocale()) => Object.entries({ ...params, lang }).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
321
+ var withLang = (url) => {
322
+ const q = qs({});
323
+ return q ? `${url}?${q}` : url;
324
+ };
219
325
  var call = async (url, init, signal) => {
220
326
  let res;
221
327
  try {
@@ -227,7 +333,7 @@ var call = async (url, init, signal) => {
227
333
  });
228
334
  } catch (e) {
229
335
  if (e.name === "AbortError") throw e;
230
- throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
336
+ throw new ApiError(t("Could not reach the server. Check your connection and try again."), 0);
231
337
  }
232
338
  if (!res.ok) {
233
339
  warnUnsigned(res.status);
@@ -236,7 +342,7 @@ var call = async (url, init, signal) => {
236
342
  detail = (await res.json()).error ?? "";
237
343
  } catch {
238
344
  }
239
- throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
345
+ 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);
240
346
  }
241
347
  return await res.json();
242
348
  };
@@ -244,7 +350,7 @@ var createApi = (c) => {
244
350
  const base = `${c.host}/api/sdk`;
245
351
  const who2 = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
246
352
  const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
247
- const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
353
+ const post = (path, body, signal) => call(withLang(`${base}${path}`), { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
248
354
  return {
249
355
  list: (o, signal) => call(`${base}/feedback?${qs({ ...me, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
250
356
  post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
@@ -257,8 +363,12 @@ var createApi = (c) => {
257
363
  survey: (url, signal) => call(`${base}/surveys?${qs({ ...me, url })}`, void 0, signal),
258
364
  seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
259
365
  respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
260
- chat: (signal) => call(`${base}/messages?${qs(me)}`, void 0, signal),
261
- /** A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). */
366
+ /** One chat by id, else the visitor's newest; `conversations` lists them all. */
367
+ chat: (signal, conversation) => call(`${base}/messages?${qs({ ...me, conversation })}`, void 0, signal),
368
+ /**
369
+ * A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). `conversation` is
370
+ * the chat it answers, or "new" to start one; without it, the running chat or else a new one.
371
+ */
262
372
  sendChat: (m) => post("/messages", m),
263
373
  /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
264
374
  uploadChatFile: (file, signal) => {
@@ -268,10 +378,10 @@ var createApi = (c) => {
268
378
  if (c.userHash) form.append("userHash", c.userHash);
269
379
  if ("uri" in file) form.append("file", file);
270
380
  else form.append("file", file, file.name || "file");
271
- return call(`${base}/messages/attachments`, { method: "POST", body: form }, signal);
381
+ return call(withLang(`${base}/messages/attachments`), { method: "POST", body: form }, signal);
272
382
  },
273
383
  /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
274
- attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me)}`,
384
+ attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me, "")}`,
275
385
  articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
276
386
  article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
277
387
  messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
@@ -282,13 +392,13 @@ var createApi = (c) => {
282
392
  // src/batch.ts
283
393
  var MAX_BATCH = 100;
284
394
  var MAX_BYTES = 6e4;
285
- var byteLength = (s) => {
395
+ var byteLength = (s2) => {
286
396
  let n = 0;
287
- for (let i = 0; i < s.length; i++) {
288
- const c = s.charCodeAt(i);
397
+ for (let i = 0; i < s2.length; i++) {
398
+ const c = s2.charCodeAt(i);
289
399
  if (c < 128) n += 1;
290
400
  else if (c < 2048) n += 2;
291
- else if (c >= 55296 && c <= 56319 && i + 1 < s.length && (s.charCodeAt(i + 1) & 64512) === 56320) {
401
+ else if (c >= 55296 && c <= 56319 && i + 1 < s2.length && (s2.charCodeAt(i + 1) & 64512) === 56320) {
292
402
  n += 4;
293
403
  i++;
294
404
  } else n += 3;
@@ -323,7 +433,7 @@ var batchRequests = (key, events, sdk, userHash) => {
323
433
 
324
434
  // src/errors.ts
325
435
  var MAX_STACK = 8 * 1024;
326
- var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
436
+ var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
327
437
  var describeError = (error) => {
328
438
  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 };
329
439
  if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
@@ -344,22 +454,52 @@ var PLAIN_CANCEL = {
344
454
  question: "",
345
455
  reasons: [],
346
456
  offers: {},
347
- confirm: { title: "Cancel your subscription?", body: "You keep access until the end of the billing period.", button: "Cancel subscription" },
457
+ // Read when it is shown, so it follows the visitor's language.
458
+ get confirm() {
459
+ return { title: t("Cancel your subscription?"), body: t("You keep access until the end of the billing period."), button: t("Cancel subscription") };
460
+ },
348
461
  offersFor: "business"
349
462
  };
350
- var FAILED = "Something went wrong. Please try again.";
351
463
  var PROGRESS_KEY = "mrrocket:onboarding";
352
- var eventSlug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
464
+ var eventSlug = (s2) => s2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
353
465
  var SIGNUP = /* @__PURE__ */ new Set(["signed_up", "signup", "sign_up", "registered", "account_created", "user_signed_up"]);
354
466
  var matchesTrigger = (trigger, event) => {
355
- const t = eventSlug(trigger);
467
+ const t2 = eventSlug(trigger);
356
468
  const e = eventSlug(event);
357
- if (!t || !e) return false;
358
- return t === e || SIGNUP.has(t) && SIGNUP.has(e);
469
+ if (!t2 || !e) return false;
470
+ return t2 === e || SIGNUP.has(t2) && SIGNUP.has(e);
359
471
  };
360
472
  var startsOnLoad = (trigger) => /^(first (launch|visit|open|session|login|sign ?in|app open)|app (opened|launched)|on load|every visit|always)?$/i.test(trigger.trim());
361
473
  var isPageTrigger = (trigger) => trigger.trim().startsWith("/");
362
- var manual = (trigger) => eventSlug(trigger) === "from_a_checklist";
474
+ var NEEDS_NEWER = "Needs tracking code 0.18.0";
475
+ var momentOf = (f) => f.when ?? f.trigger;
476
+ var manual = (trigger) => eventSlug(trigger) === "from_a_checklist" || eventSlug(trigger) === "from_your_code" || eventSlug(trigger) === eventSlug(NEEDS_NEWER);
477
+ var AFTER_PREFIX = "after:";
478
+ var afterOf = (trigger) => {
479
+ const t2 = (trigger ?? "").trim();
480
+ return t2.toLowerCase().startsWith(AFTER_PREFIX) ? t2.slice(AFTER_PREFIX.length).trim() || null : null;
481
+ };
482
+ var opensOf = (step) => step.opens ?? (step.startsTour ? { kind: "tour", id: step.startsTour } : void 0);
483
+ var OTHER_PREFIX = "other:";
484
+ var scaleOf = (q) => {
485
+ const min = q?.min ?? 0;
486
+ const max = q?.max ?? 10;
487
+ return Array.from({ length: Math.max(0, Math.min(max - min, 20) + 1) }, (_, i) => min + i);
488
+ };
489
+ var answerOk = (q, value) => {
490
+ if (!q || value == null) return false;
491
+ if (q.type === "choice") {
492
+ if (q.other && value.startsWith(OTHER_PREFIX)) return value.slice(OTHER_PREFIX.length).trim().length > 0;
493
+ return (q.options ?? []).some((o) => o.id === value);
494
+ }
495
+ if (q.type === "scale") return scaleOf(q).some((n) => String(n) === value);
496
+ return value.trim().length > 0 && value.length <= 500;
497
+ };
498
+ var answerLabel = (q, value) => {
499
+ if (q?.type !== "choice") return value.trim();
500
+ if (q.other && value.startsWith(OTHER_PREFIX)) return value.slice(OTHER_PREFIX.length).trim();
501
+ return q.options?.find((o) => o.id === value)?.label ?? value;
502
+ };
363
503
  var videoEmbed = (url) => {
364
504
  const u = (url ?? "").trim();
365
505
  if (!/^https?:\/\//i.test(u)) return null;
@@ -381,12 +521,14 @@ var runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toS
381
521
  var discounted = (amount, percent) => Math.round(amount * (1 - percent / 100) * 100) / 100;
382
522
  var money = (amount, currency) => {
383
523
  try {
384
- return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
524
+ return new Intl.NumberFormat(currentLocale(), { style: "currency", currency }).format(amount);
385
525
  } catch {
386
526
  return `${amount.toFixed(2)} ${currency}`;
387
527
  }
388
528
  };
389
- var offerButton = (o) => o.kind === "contact" ? "Send to the team" : o.kind === "pause" ? "Pause instead" : o.kind === "downgrade" ? "Switch plan" : "Accept offer";
529
+ 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");
530
+ var monthsWord = (n) => n === 1 ? t("1 month") : tf("{n} months", { n });
531
+ var forMonths = (n) => tf("for {months}", { months: monthsWord(n) });
390
532
  var safeUrl = (url) => {
391
533
  const u = (url ?? "").trim();
392
534
  return /^https?:\/\//i.test(u) || /^\/(?!\/)/.test(u) ? u : void 0;
@@ -425,7 +567,12 @@ var createFlows = (env) => {
425
567
  const now = () => env.now?.() ?? Date.now();
426
568
  let timer = null;
427
569
  let returnTo = null;
570
+ const answers = /* @__PURE__ */ new Map();
571
+ const drafts = /* @__PURE__ */ new Map();
572
+ const celebrated = /* @__PURE__ */ new Set();
573
+ let finaleNow = null;
428
574
  const flows = () => config?.onboarding ?? [];
575
+ const drawn = () => flows().filter((f) => f.shownBy !== "code");
429
576
  const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
430
577
  const prog = (id) => {
431
578
  let p = progress.get(id);
@@ -499,6 +646,14 @@ var createFlows = (env) => {
499
646
  };
500
647
  const keyOf = (pid) => `${PROGRESS_KEY}:${pid ?? "visitor"}`;
501
648
  const eventsKeyOf = (pid) => `${PROGRESS_KEY}:events:${pid ?? "visitor"}`;
649
+ const answersKeyOf = (pid) => `${PROGRESS_KEY}:answers:${pid ?? "visitor"}`;
650
+ const celebrate = (f) => {
651
+ if (!f.finish || f.finish === "none" || f.shownBy === "code" || celebrated.has(f.id)) return;
652
+ const p = progress.get(f.id);
653
+ if (!p || p.dismissed || walkLine(f, nodesOf(f), p, false).at !== "end") return;
654
+ celebrated.add(f.id);
655
+ finaleNow = { kind: f.finish, key: `${f.id}:${now()}` };
656
+ };
502
657
  const save = () => {
503
658
  dirty = false;
504
659
  const store = env.storage?.();
@@ -514,18 +669,19 @@ var createFlows = (env) => {
514
669
  };
515
670
  try {
516
671
  store.set(keyOf(env.personId()), JSON.stringify(out));
672
+ if (answers.size) store.set(answersKeyOf(env.personId()), JSON.stringify(Object.fromEntries(answers)));
517
673
  if (seenEvents.size) store.set(eventsKeyOf(env.personId()), JSON.stringify([...seenEvents].slice(-200)));
518
674
  } catch {
519
675
  }
520
676
  };
521
677
  const merge = (state) => {
522
- for (const [id, s] of Object.entries(state ?? {})) {
678
+ for (const [id, s2] of Object.entries(state ?? {})) {
523
679
  const p = prog(id);
524
- for (const d of s.done ?? []) p.done.add(d);
525
- if (s.dismissed) p.dismissed = true;
526
- for (const [k, v] of Object.entries(s.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === "yes");
527
- for (const [k, v] of Object.entries(s.at ?? {})) if (typeof v === "number" && !p.at.has(k)) p.at.set(k, v);
528
- 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);
680
+ for (const d of s2.done ?? []) p.done.add(d);
681
+ if (s2.dismissed) p.dismissed = true;
682
+ for (const [k, v] of Object.entries(s2.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === "yes");
683
+ for (const [k, v] of Object.entries(s2.at ?? {})) if (typeof v === "number" && !p.at.has(k)) p.at.set(k, v);
684
+ 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);
529
685
  }
530
686
  };
531
687
  const wakeAt = (until) => {
@@ -565,9 +721,18 @@ var createFlows = (env) => {
565
721
  return {
566
722
  type: "checklist",
567
723
  flow,
568
- items: items.map((s) => {
569
- const tour = s.startsTour && s.startsTour !== flow.id ? flowById(s.startsTour) : void 0;
570
- return { step: s, done: p.done.has(s.id), ...tour && nodesOf(tour).length ? { tour } : {} };
724
+ items: items.map((s2) => {
725
+ const o = opensOf(s2);
726
+ const t2 = o && (o.kind === "tour" || o.kind === "modal") && o.id !== flow.id ? flowById(o.id) : void 0;
727
+ const target = t2 && t2.shownBy !== "code" && nodesOf(t2).length ? t2 : void 0;
728
+ const opens = o && (o.kind === "page" ? !!o.target.trim() : o.kind === "code" || !!target) ? o : void 0;
729
+ return {
730
+ step: s2,
731
+ done: p.done.has(s2.id),
732
+ ...target && o?.kind === "tour" ? { tour: target } : {},
733
+ ...opens ? { opens } : {},
734
+ ...target ? { target } : {}
735
+ };
571
736
  })
572
737
  };
573
738
  }
@@ -578,7 +743,16 @@ var createFlows = (env) => {
578
743
  const rest = line.slice(index + 1);
579
744
  const fork = rest.findIndex((n) => n.kind === "condition");
580
745
  const ahead = (fork < 0 ? rest : rest.slice(0, fork)).filter(isStep).length;
581
- return { type: "step", flow, step, index: w.before, total: w.before + 1 + ahead, last: !ahead && fork < 0 };
746
+ return {
747
+ type: "step",
748
+ flow,
749
+ step,
750
+ index: w.before,
751
+ total: w.before + 1 + ahead,
752
+ last: !ahead && fork < 0,
753
+ // 0.18.0: a question's answer so far, and whether there is one before it to go back to.
754
+ ...step.kind === "question" ? { answer: drafts.get(step.id) ?? (step.question ? answers.get(step.question.key) : void 0), back: w.before > 0 } : {}
755
+ };
582
756
  };
583
757
  const view = () => {
584
758
  if (cancel) {
@@ -605,23 +779,25 @@ var createFlows = (env) => {
605
779
  if (ob) return ob;
606
780
  if (active && !holding) {
607
781
  const back = returnTo;
608
- const ended = active;
782
+ const ended2 = active;
609
783
  returnTo = null;
610
784
  active = null;
611
785
  const list = back ? flowById(back.flow) : void 0;
612
786
  if (back && list) {
613
- const item = stepsIn(nodesOf(list)).find((s) => s.id === back.step);
787
+ const item = stepsIn(nodesOf(list)).find((s2) => s2.id === back.step);
614
788
  const p = prog(list.id);
615
- if (item && !item.event && !progress.get(ended)?.dismissed && !p.done.has(item.id)) {
789
+ if (item && !item.event && !progress.get(ended2)?.dismissed && !p.done.has(item.id)) {
616
790
  markDone(p, item.id);
617
791
  record({ flow: list.id, step: item.id, kind: "checklist", action: "completed" });
792
+ celebrate(list);
618
793
  }
619
794
  active = list.id;
620
795
  const again = onboardingView();
621
796
  if (again) return again;
622
797
  if (!holding) active = null;
623
798
  } else if (known) {
624
- const after = flowById(ended)?.trigger;
799
+ const was = flowById(ended2);
800
+ const after = was ? momentOf(was) : void 0;
625
801
  Promise.resolve().then(() => autoStart(after));
626
802
  }
627
803
  }
@@ -649,22 +825,64 @@ var createFlows = (env) => {
649
825
  emit();
650
826
  return true;
651
827
  };
652
- const allowed = (f) => f.audience !== "signed_in" || !!env.personId();
828
+ const allowed = (f) => f.audience === "answered" ? !!f.answered && answers.get(f.answered.question) === f.answered.option : f.audience !== "signed_in" || !!env.personId();
829
+ const predecessorEnded = (id) => {
830
+ const g2 = flowById(id);
831
+ return !g2 || finished(g2);
832
+ };
833
+ const due = (f, trigger = momentOf(f)) => {
834
+ const path = env.path?.();
835
+ const pred = afterOf(trigger);
836
+ if (pred) return predecessorEnded(pred);
837
+ if (manual(trigger)) return false;
838
+ if (isPageTrigger(trigger)) return path !== void 0 && pageMatches(trigger, path);
839
+ if (startsOnLoad(trigger)) return true;
840
+ for (const e of seenEvents) if (matchesTrigger(trigger, e)) return true;
841
+ return false;
842
+ };
653
843
  const autoStart = (after) => {
654
844
  if (!auto || active || !known) return;
655
845
  if (!env.personId() && !env.storage?.()) return;
656
846
  const path = env.path?.();
657
847
  const same2 = after && !manual(after) ? eventSlug(after) : "";
658
- const next = flows().find(
659
- (f) => (isPageTrigger(f.trigger) ? path !== void 0 && pageMatches(f.trigger, path) : startsOnLoad(f.trigger) || !!same2 && eventSlug(f.trigger) === same2) && allowed(f) && nodesOf(f).length && !finished(f)
848
+ const next = drawn().find(
849
+ (f) => (afterOf(momentOf(f)) ? predecessorEnded(afterOf(momentOf(f))) : isPageTrigger(momentOf(f)) ? path !== void 0 && pageMatches(momentOf(f), path) : startsOnLoad(momentOf(f)) || !!same2 && eventSlug(momentOf(f)) === same2) && allowed(f) && nodesOf(f).length && !finished(f)
660
850
  );
661
851
  if (next) start(next);
662
852
  };
853
+ const openItem = (list, stepId, target) => {
854
+ const p = prog(target.id);
855
+ p.done.clear();
856
+ p.at.clear();
857
+ p.dismissed = false;
858
+ returnTo = { flow: list.id, step: stepId };
859
+ active = target.id;
860
+ env.track(ONBOARDING_EVENT, { flow: target.id, action: "started", from: list.id });
861
+ save();
862
+ emit();
863
+ };
864
+ const answerStep = (flow, step, raw) => {
865
+ const q = step.question;
866
+ 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;
867
+ answers.set(q.key, value);
868
+ drafts.delete(step.id);
869
+ markDone(prog(flow.id), step.id);
870
+ env.track(ONBOARDING_EVENT, { flow: flow.id, step: step.id, kind: "question", action: "completed", question: q.key, answer: value, label: answerLabel(q, value) });
871
+ celebrate(flow);
872
+ save();
873
+ };
874
+ const ended = (f) => {
875
+ save();
876
+ if (finished(f)) Promise.resolve().then(() => autoStart());
877
+ emit();
878
+ };
663
879
  const refresh = () => {
664
880
  const pid = env.personId();
665
881
  if (progressFor !== null && progressFor !== pid) {
666
882
  progress.clear();
667
883
  seenEvents.clear();
884
+ answers.clear();
885
+ drafts.clear();
668
886
  handled.clear();
669
887
  active = null;
670
888
  returnTo = null;
@@ -681,6 +899,10 @@ var createFlows = (env) => {
681
899
  if (seen) {
682
900
  for (const e of JSON.parse(seen)) if (typeof e === "string") seenEvents.add(e);
683
901
  }
902
+ const said = await store.get(answersKeyOf(pid));
903
+ if (said) {
904
+ for (const [k, v] of Object.entries(JSON.parse(said))) if (typeof v === "string") answers.set(k, v);
905
+ }
684
906
  } catch {
685
907
  }
686
908
  }
@@ -691,6 +913,7 @@ var createFlows = (env) => {
691
913
  if (state && env.personId() === pid) {
692
914
  merge(state.onboarding);
693
915
  for (const e of state.events ?? []) if (typeof e === "string") seenEvents.add(eventSlug(e));
916
+ for (const [k, v] of Object.entries(state.answers ?? {})) if (typeof v === "string" && !answers.has(k)) answers.set(k, v);
694
917
  messages = (state.messages ?? []).filter((m) => m && m.id && m.title);
695
918
  }
696
919
  }
@@ -751,7 +974,7 @@ var createFlows = (env) => {
751
974
  () => finish(c, r),
752
975
  () => {
753
976
  c.busy = false;
754
- c.error = FAILED;
977
+ c.error = t("Something went wrong. Please try again.");
755
978
  emit();
756
979
  }
757
980
  );
@@ -784,27 +1007,33 @@ var createFlows = (env) => {
784
1007
  if (fresh) seenEvents.add(slug);
785
1008
  const flow = flowById(active);
786
1009
  let ticked = false;
787
- if (flow) {
788
- const p = prog(flow.id);
789
- for (const s of stepsIn(nodesOf(flow)))
790
- if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
791
- markDone(p, s.id);
792
- ticked = true;
793
- env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: "completed" });
1010
+ const lists = [...flow ? [flow] : [], ...flows().filter((f) => f.shownBy === "code" && f.id !== flow?.id)];
1011
+ for (const l of lists) {
1012
+ const p = prog(l.id);
1013
+ let here = false;
1014
+ for (const s2 of stepsIn(nodesOf(l)))
1015
+ if (s2.kind === "checklist" && s2.event && !p.done.has(s2.id) && matchesTrigger(s2.event, name)) {
1016
+ markDone(p, s2.id);
1017
+ here = true;
1018
+ env.track(ONBOARDING_EVENT, { flow: l.id, step: s2.id, kind: s2.kind, action: "completed" });
794
1019
  }
1020
+ if (here) {
1021
+ ticked = true;
1022
+ celebrate(l);
1023
+ }
795
1024
  }
796
- if (ticked || fresh && flow) {
1025
+ if (ticked || fresh && (flow || lists.length)) {
797
1026
  save();
798
1027
  emit();
799
1028
  } else if (fresh) save();
800
1029
  if (!auto || !env.personId() && !env.storage?.()) return;
801
- const next = flows().find((f) => nodesOf(f).length && !manual(f.trigger) && matchesTrigger(f.trigger, name) && allowed(f) && !finished(f));
1030
+ const next = drawn().find((f) => nodesOf(f).length && !manual(momentOf(f)) && matchesTrigger(momentOf(f), name) && allowed(f) && !finished(f));
802
1031
  if (next) start(next);
803
1032
  },
804
1033
  openCancelFlow,
805
1034
  startOnboarding: (id) => {
806
- const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !manual(x.trigger) && !finished(x));
807
- if (!f || !nodesOf(f).length) return false;
1035
+ const f = id ? flowById(id) : drawn().find((x) => nodesOf(x).length && !manual(momentOf(x)) && !afterOf(momentOf(x)) && !finished(x));
1036
+ if (!f || !nodesOf(f).length || f.shownBy === "code") return false;
808
1037
  const p = prog(f.id);
809
1038
  p.dismissed = false;
810
1039
  return start(f);
@@ -825,8 +1054,8 @@ var createFlows = (env) => {
825
1054
  answer: (text2) => {
826
1055
  const c = cancel;
827
1056
  if (!c || c.busy || c.screen !== "followup") return;
828
- const t = text2.slice(0, 500);
829
- c.answer = t.trim() ? t : void 0;
1057
+ const t2 = text2.slice(0, 500);
1058
+ c.answer = t2.trim() ? t2 : void 0;
830
1059
  emit();
831
1060
  },
832
1061
  next: () => {
@@ -895,8 +1124,17 @@ var createFlows = (env) => {
895
1124
  next: () => {
896
1125
  const v = onboardingView();
897
1126
  if (v?.type !== "step") return;
1127
+ if (v.step.kind === "question") {
1128
+ const q = v.step.question;
1129
+ const a = drafts.get(v.step.id) ?? (q ? answers.get(q.key) : void 0);
1130
+ if (!q || !answerOk(q, a)) return;
1131
+ answerStep(v.flow, v.step, a);
1132
+ emit();
1133
+ return;
1134
+ }
898
1135
  markDone(prog(v.flow.id), v.step.id);
899
1136
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
1137
+ celebrate(v.flow);
900
1138
  save();
901
1139
  emit();
902
1140
  const url = v.step.kind === "modal" ? safeUrl(v.step.url) : void 0;
@@ -909,6 +1147,7 @@ var createFlows = (env) => {
909
1147
  if (!item || item.done) return;
910
1148
  markDone(prog(v.flow.id), stepId);
911
1149
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
1150
+ celebrate(v.flow);
912
1151
  save();
913
1152
  emit();
914
1153
  },
@@ -925,20 +1164,123 @@ var createFlows = (env) => {
925
1164
  startTour: (stepId) => {
926
1165
  const v = onboardingView();
927
1166
  if (v?.type !== "checklist") return false;
928
- const tour = v.items.find((i) => i.step.id === stepId)?.tour;
929
- if (!tour) return false;
930
- const p = prog(tour.id);
931
- p.done.clear();
932
- p.at.clear();
933
- p.dismissed = false;
934
- returnTo = { flow: v.flow.id, step: stepId };
935
- active = tour.id;
936
- env.track(ONBOARDING_EVENT, { flow: tour.id, action: "started", from: v.flow.id });
1167
+ const target = v.items.find((i) => i.step.id === stepId)?.target;
1168
+ if (!target) return false;
1169
+ openItem(v.flow, stepId, target);
1170
+ return true;
1171
+ },
1172
+ open: (stepId) => {
1173
+ const v = onboardingView();
1174
+ if (v?.type !== "checklist") return false;
1175
+ const it = v.items.find((i) => i.step.id === stepId);
1176
+ const o = it?.opens;
1177
+ if (!it || !o) return false;
1178
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "opened", opens: o.kind });
1179
+ if ((o.kind === "tour" || o.kind === "modal") && it.target) {
1180
+ openItem(v.flow, stepId, it.target);
1181
+ return true;
1182
+ }
1183
+ if (!it.done && !it.step.event) {
1184
+ markDone(prog(v.flow.id), stepId);
1185
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
1186
+ celebrate(v.flow);
1187
+ }
937
1188
  save();
938
1189
  emit();
1190
+ if (o.kind === "page") env.navigate?.(o.target.trim());
1191
+ else env.task?.(stepId, v.flow);
939
1192
  return true;
1193
+ },
1194
+ answer: (value) => {
1195
+ const v = onboardingView();
1196
+ if (v?.type !== "step" || v.step.kind !== "question") return;
1197
+ drafts.set(v.step.id, String(value).slice(0, 500));
1198
+ emit();
1199
+ },
1200
+ skip: () => {
1201
+ const v = onboardingView();
1202
+ const q = v?.type === "step" && v.step.kind === "question" ? v.step.question : void 0;
1203
+ if (v?.type !== "step" || !q?.optional) return;
1204
+ drafts.delete(v.step.id);
1205
+ markDone(prog(v.flow.id), v.step.id);
1206
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: "question", action: "completed", question: q.key, skipped: true });
1207
+ celebrate(v.flow);
1208
+ save();
1209
+ emit();
1210
+ },
1211
+ back: () => {
1212
+ const v = onboardingView();
1213
+ if (v?.type !== "step" || v.step.kind !== "question" || !v.back) return;
1214
+ const line = stepsIn(nodesOf(v.flow));
1215
+ const at = line.findIndex((s2) => s2.id === v.step.id);
1216
+ const p = prog(v.flow.id);
1217
+ for (let i = at - 1; i >= 0; i--)
1218
+ if (p.done.has(line[i].id)) {
1219
+ p.done.delete(line[i].id);
1220
+ p.at.delete(line[i].id);
1221
+ break;
1222
+ }
1223
+ save();
1224
+ emit();
1225
+ },
1226
+ item: (id) => {
1227
+ const f = flowById(id);
1228
+ if (!f) return null;
1229
+ const p = progress.get(f.id);
1230
+ const steps = stepsIn(nodesOf(f)).map((s2) => {
1231
+ const a = s2.question ? answers.get(s2.question.key) : void 0;
1232
+ return { ...s2, done: !!p?.done.has(s2.id), ...a !== void 0 ? { answer: a } : {} };
1233
+ });
1234
+ const done = steps.length > 0 && steps.every((s2) => s2.done);
1235
+ const dismissed = !!p?.dismissed;
1236
+ return { item: f, steps, done, dismissed, eligible: !done && !dismissed && allowed(f) && due(f) };
1237
+ },
1238
+ seen: (id) => {
1239
+ const f = flowById(id);
1240
+ if (!f || f.shownBy !== "code" || !known) return;
1241
+ if (!shown.has(`f:${f.id}`)) {
1242
+ shown.add(`f:${f.id}`);
1243
+ env.track(ONBOARDING_EVENT, { flow: f.id, action: "started" });
1244
+ }
1245
+ const p = progress.get(f.id);
1246
+ for (const s2 of stepsIn(nodesOf(f)))
1247
+ if (!p?.done.has(s2.id) && !shown.has(`s:${s2.id}`)) {
1248
+ shown.add(`s:${s2.id}`);
1249
+ env.track(ONBOARDING_EVENT, { flow: f.id, step: s2.id, kind: s2.kind, action: "shown" });
1250
+ if (s2.kind !== "checklist") break;
1251
+ }
1252
+ },
1253
+ complete: (id, stepId) => {
1254
+ const f = flowById(id);
1255
+ const s2 = f && stepsIn(nodesOf(f)).find((x) => x.id === stepId);
1256
+ if (!f || !s2 || s2.kind === "question") return;
1257
+ const p = prog(f.id);
1258
+ if (p.done.has(s2.id)) return;
1259
+ markDone(p, s2.id);
1260
+ env.track(ONBOARDING_EVENT, { flow: f.id, step: s2.id, kind: s2.kind, action: "completed" });
1261
+ ended(f);
1262
+ },
1263
+ dismissItem: (id) => {
1264
+ const f = flowById(id);
1265
+ if (!f) return;
1266
+ const p = prog(f.id);
1267
+ if (p.dismissed) return;
1268
+ p.dismissed = true;
1269
+ env.track(ONBOARDING_EVENT, { flow: f.id, action: "dismissed" });
1270
+ if (active === f.id) active = null;
1271
+ ended(f);
1272
+ },
1273
+ answerItem: (id, question, value) => {
1274
+ const f = flowById(id);
1275
+ const s2 = f && stepsIn(nodesOf(f)).find((x) => x.kind === "question" && (x.id === question || x.question?.key === question));
1276
+ if (!f || !s2?.question || !answerOk(s2.question, value)) return;
1277
+ answerStep(f, s2, value);
1278
+ ended(f);
940
1279
  }
941
1280
  },
1281
+ finale: () => finaleNow,
1282
+ accent: () => config?.onboardingAccent,
1283
+ answers: () => Object.fromEntries(answers),
942
1284
  message: {
943
1285
  click: () => {
944
1286
  const v = view();
@@ -1020,7 +1362,7 @@ var createPaywallHost = (env) => {
1020
1362
  },
1021
1363
  () => {
1022
1364
  c.busy = false;
1023
- c.error = "The purchase did not go through. Please try again.";
1365
+ c.error = t("The purchase did not go through. Please try again.");
1024
1366
  emit();
1025
1367
  }
1026
1368
  );
@@ -1095,10 +1437,10 @@ var RageDetector = class {
1095
1437
  __publicField(this, "clicks", []);
1096
1438
  __publicField(this, "fired", false);
1097
1439
  }
1098
- push(x, y, t) {
1099
- const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
1440
+ push(x, y, t2) {
1441
+ const near = this.clicks.filter((c) => t2 - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
1100
1442
  if (near.length !== this.clicks.length) this.fired = false;
1101
- this.clicks = [...near, { x, y, t }];
1443
+ this.clicks = [...near, { x, y, t: t2 }];
1102
1444
  if (this.clicks.length >= this.count && !this.fired) {
1103
1445
  this.fired = true;
1104
1446
  return true;
@@ -1114,9 +1456,9 @@ var BUFFER_MS = 6e4;
1114
1456
  var CHUNK_FRAMES = 12;
1115
1457
  var CHUNK_CHARS = 7e5;
1116
1458
  var MAX_MS = 30 * 6e4;
1117
- var shareOf = (s) => {
1459
+ var shareOf = (s2) => {
1118
1460
  let h3 = 2166136261;
1119
- for (let i = 0; i < s.length; i++) h3 = Math.imul(h3 ^ s.charCodeAt(i), 16777619);
1461
+ for (let i = 0; i < s2.length; i++) h3 = Math.imul(h3 ^ s2.charCodeAt(i), 16777619);
1120
1462
  return (h3 >>> 0) / 4294967296;
1121
1463
  };
1122
1464
  var createRecorder = (deps, st) => {
@@ -1241,14 +1583,14 @@ var measureMasks = () => Promise.all(
1241
1583
  (ref) => new Promise((resolve) => {
1242
1584
  const v = ref.current;
1243
1585
  if (!v) return resolve("none");
1244
- const t = setTimeout(() => resolve("failed"), 100);
1586
+ const t2 = setTimeout(() => resolve("failed"), 100);
1245
1587
  try {
1246
1588
  v.measureInWindow((x, y, w, h3) => {
1247
- clearTimeout(t);
1589
+ clearTimeout(t2);
1248
1590
  resolve(w > 0 && h3 > 0 ? [x, y, w, h3] : "none");
1249
1591
  });
1250
1592
  } catch {
1251
- clearTimeout(t);
1593
+ clearTimeout(t2);
1252
1594
  resolve("failed");
1253
1595
  }
1254
1596
  })
@@ -1285,7 +1627,7 @@ var MAX_CRUMBS = 20;
1285
1627
  var MAX_BACKOFF_MS = 5 * 60 * 1e3;
1286
1628
  var REQUEST_TIMEOUT_MS = 15e3;
1287
1629
  var EAGER_COUNT = 20;
1288
- var clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1630
+ var clip2 = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
1289
1631
  var createClient = (options, deps) => {
1290
1632
  const apiKey = (options.apiKey ?? "").trim();
1291
1633
  const host = (options.host ?? DEFAULT_HOST).trim().replace(/\/+$/, "");
@@ -1313,6 +1655,8 @@ var createClient = (options, deps) => {
1313
1655
  context = deps.context();
1314
1656
  } catch {
1315
1657
  }
1658
+ setDeviceLocale(context.locale);
1659
+ if (options.locale) setLocale(options.locale);
1316
1660
  const appId = (deps.appId || "app").replace(/[^A-Za-z0-9._-]/g, "") || "app";
1317
1661
  const release = options.release ?? (context.appVersion ? `${context.appVersion}${context.build ? ` (${context.build})` : ""}` : void 0);
1318
1662
  const sdk = { v: SDK_VERSION, errors: errorsOn, platform: context.platform };
@@ -1358,7 +1702,7 @@ var createClient = (options, deps) => {
1358
1702
  const fetchInApp = async (id) => {
1359
1703
  if (!apiKey) return null;
1360
1704
  const hash = hashOf(id);
1361
- const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}${hash ? `&userHash=${hash}` : ""}`, { method: "GET" });
1705
+ const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}${hash ? `&userHash=${hash}` : ""}${langParam()}`, { method: "GET" });
1362
1706
  warnUnsigned(r.status, deps.log);
1363
1707
  return r.ok ? await r.json() : null;
1364
1708
  };
@@ -1372,7 +1716,14 @@ var createClient = (options, deps) => {
1372
1716
  set: (k, v) => void set(k, v)
1373
1717
  } : null,
1374
1718
  openUrl: deps.openUrl,
1375
- fetchInApp: (id) => fetchInApp(id)
1719
+ fetchInApp: (id) => fetchInApp(id),
1720
+ // 0.18.0: a task's page or screen goes to the app's navigator; without one only a link with a scheme opens.
1721
+ navigate: (target) => {
1722
+ if (options.onNavigate) return options.onNavigate(target);
1723
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) && !/^(javascript|data|vbscript):/i.test(target)) deps.openUrl?.(target);
1724
+ else warn(`A checklist task opens "${target}": pass onNavigate to MRRocketProvider to open screens and paths.`);
1725
+ },
1726
+ task: (taskId, item) => options.onTask?.(taskId, item)
1376
1727
  });
1377
1728
  const refreshFlows = () => {
1378
1729
  if (!isReady || closed) return;
@@ -1437,9 +1788,9 @@ var createClient = (options, deps) => {
1437
1788
  const sendId = () => consent === "persistent" ? distinctId() : identified;
1438
1789
  const hashOf = (id) => hashFor(signed, id);
1439
1790
  const touchSession = () => {
1440
- const t = now();
1441
- if (!session || t - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t };
1442
- else session.last = t;
1791
+ const t2 = now();
1792
+ if (!session || t2 - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t2 };
1793
+ else session.last = t2;
1443
1794
  return session.id;
1444
1795
  };
1445
1796
  const urlOf = (path = currentScreen) => `app://${appId}${path ?? "/"}`;
@@ -1628,7 +1979,7 @@ var createClient = (options, deps) => {
1628
1979
  const fetchConfig = async () => {
1629
1980
  if (!apiKey) return;
1630
1981
  try {
1631
- const r = await deps.fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}`, { method: "GET" });
1982
+ const r = await deps.fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}${langParam()}`, { method: "GET" });
1632
1983
  if (!r.ok) {
1633
1984
  warn(`Config request answered ${r.status}. Check the public key and the host.`);
1634
1985
  return;
@@ -1636,6 +1987,7 @@ var createClient = (options, deps) => {
1636
1987
  const c = await r.json();
1637
1988
  if (c && Array.isArray(c.experiments)) {
1638
1989
  config = c;
1990
+ setWords(c.words, c.lang);
1639
1991
  if (consent !== "off") await set(KEYS.config, JSON.stringify(c));
1640
1992
  changed();
1641
1993
  }
@@ -1754,7 +2106,10 @@ var createClient = (options, deps) => {
1754
2106
  }
1755
2107
  try {
1756
2108
  const c = cached ? JSON.parse(cached) : null;
1757
- if (c && Array.isArray(c.experiments) && !config) config = c;
2109
+ if (c && Array.isArray(c.experiments) && !config) {
2110
+ config = c;
2111
+ setWords(c.words, c.lang);
2112
+ }
1758
2113
  } catch {
1759
2114
  await remove(KEYS.config);
1760
2115
  }
@@ -1810,6 +2165,9 @@ var createClient = (options, deps) => {
1810
2165
  await fetchConfig();
1811
2166
  markReady();
1812
2167
  refreshFlows();
2168
+ onLocale(() => {
2169
+ void fetchConfig().then(refreshFlows);
2170
+ });
1813
2171
  };
1814
2172
  void start().catch(() => {
1815
2173
  loaded = true;
@@ -1998,42 +2356,227 @@ var createClient = (options, deps) => {
1998
2356
  return client;
1999
2357
  };
2000
2358
  var activeRouteName = (state) => {
2001
- let s = state;
2359
+ let s2 = state;
2002
2360
  let name;
2003
- for (let depth = 0; s && Array.isArray(s.routes) && s.routes.length && depth < 20; depth++) {
2004
- const route = s.routes[typeof s.index === "number" ? s.index : s.routes.length - 1];
2361
+ for (let depth = 0; s2 && Array.isArray(s2.routes) && s2.routes.length && depth < 20; depth++) {
2362
+ const route = s2.routes[typeof s2.index === "number" ? s2.index : s2.routes.length - 1];
2005
2363
  if (!route) break;
2006
2364
  name = route.name ?? name;
2007
- s = route.state;
2365
+ s2 = route.state;
2008
2366
  }
2009
2367
  return name;
2010
2368
  };
2011
2369
 
2012
2370
  // src/native/flows-view.ts
2013
- var import_react = require("react");
2371
+ var import_react2 = require("react");
2372
+
2373
+ // src/icons.ts
2374
+ var FLOW_ICONS = {
2375
+ check: { label: "Check", paths: ["M5 12.5l4.5 4.5L19 7.5"], emoji: "\u2705" },
2376
+ star: { label: "Star", paths: ["M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.9l-5.2 2.7 1-5.8-4.3-4.1 5.9-.9z"], emoji: "\u2B50" },
2377
+ rocket: {
2378
+ label: "Rocket",
2379
+ paths: ["M12 3c3 2 4.5 5.5 4 10l-2 3h-4l-2-3c-.5-4.5 1-8 4-10z", "M8 13.5l-3 3 3.5.5", "M16 13.5l3 3-3.5.5", "M13.5 9.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z", "M11 19.5v1.5M13 19.5v1.5"],
2380
+ emoji: "\u{1F680}"
2381
+ },
2382
+ user: { label: "Person", paths: ["M15.5 8a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0z", "M5 20c.8-3.5 3.6-5.5 7-5.5s6.2 2 7 5.5"], emoji: "\u{1F464}" },
2383
+ users: {
2384
+ label: "Team",
2385
+ paths: ["M13 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z", "M3.5 19c.6-3 3-4.8 6.5-4.8s5.9 1.8 6.5 4.8", "M16 5.2a3 3 0 0 1 0 5.6", "M18 14.6c1.5.6 2.4 2 2.7 4.4"],
2386
+ emoji: "\u{1F465}"
2387
+ },
2388
+ settings: {
2389
+ label: "Settings",
2390
+ paths: ["M12 2.8v2.4M12 18.8v2.4M2.8 12h2.4M18.8 12h2.4M5.5 5.5l1.7 1.7M16.8 16.8l1.7 1.7M5.5 18.5l1.7-1.7M16.8 7.2l1.7-1.7", "M17 12a5 5 0 1 1-10 0 5 5 0 0 1 10 0z", "M14 12a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"],
2391
+ emoji: "\u2699\uFE0F"
2392
+ },
2393
+ bell: { label: "Bell", paths: ["M6 16v-5a6 6 0 0 1 12 0v5l1.5 2h-15z", "M10 20.5a2 2 0 0 0 4 0"], emoji: "\u{1F514}" },
2394
+ mail: { label: "Mail", paths: ["M4 6h16v12H4z", "M4 7l8 6 8-6"], emoji: "\u2709\uFE0F" },
2395
+ chat: { label: "Chat", paths: ["M4 5h16v11H9l-5 4z"], emoji: "\u{1F4AC}" },
2396
+ heart: { label: "Heart", paths: ["M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10z"], emoji: "\u2764\uFE0F" },
2397
+ flag: { label: "Flag", paths: ["M5 21V4", "M5 4h11l-2 4 2 4H5"], emoji: "\u{1F6A9}" },
2398
+ bolt: { label: "Bolt", paths: ["M13 3L5 13.5h6L10 21l8-10.5h-6z"], emoji: "\u26A1" },
2399
+ book: { label: "Book", paths: ["M4 5.5C6.5 4 9.5 4 12 6c2.5-2 5.5-2 8-.5V19c-2.5-1.5-5.5-1.5-8 .5-2.5-2-5.5-2-8-.5z", "M12 6v13.5"], emoji: "\u{1F4D6}" },
2400
+ calendar: { label: "Calendar", paths: ["M4 6h16v14H4z", "M4 10h16", "M8 3.5v4M16 3.5v4"], emoji: "\u{1F4C5}" },
2401
+ camera: { label: "Camera", paths: ["M4 8h3.5L9 5.5h6L16.5 8H20v11H4z", "M15.5 13a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0z"], emoji: "\u{1F4F7}" },
2402
+ cart: { label: "Cart", paths: ["M3.5 4.5H6l2 10h10l2-7H7", "M9.5 19.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z", "M18.5 19.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"], emoji: "\u{1F6D2}" },
2403
+ chart: { label: "Chart", paths: ["M4 20h16", "M7 16v-4M12 16V8M17 16V9"], emoji: "\u{1F4CA}" },
2404
+ gift: { label: "Gift", paths: ["M4 10h16v10H4z", "M3 7h18v3H3z", "M12 7v13", "M12 7c-1.5-3-5-3-5-1s3.5 1 5 1z", "M12 7c1.5-3 5-3 5-1s-3.5 1-5 1z"], emoji: "\u{1F381}" },
2405
+ home: { label: "Home", paths: ["M4 11l8-7 8 7", "M6 9.5V20h12V9.5", "M10 20v-6h4v6"], emoji: "\u{1F3E0}" },
2406
+ link: { label: "Link", paths: ["M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1", "M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"], emoji: "\u{1F517}" },
2407
+ lock: { label: "Lock", paths: ["M6 11h12v9H6z", "M8.5 11V8a3.5 3.5 0 0 1 7 0v3"], emoji: "\u{1F512}" },
2408
+ pencil: { label: "Pencil", paths: ["M4 20l1-4L16 5l3 3L8 19z", "M14 7l3 3"], emoji: "\u270F\uFE0F" },
2409
+ plus: { label: "Plus", paths: ["M12 5v14M5 12h14"], emoji: "\u2795" },
2410
+ search: { label: "Search", paths: ["M16 10.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z", "M14.5 14.5L20 20"], emoji: "\u{1F50D}" },
2411
+ upload: { label: "Upload", paths: ["M12 15V4", "M7.5 8.5L12 4l4.5 4.5", "M4 15v5h16v-5"], emoji: "\u2B06\uFE0F" },
2412
+ play: { label: "Play", paths: ["M8 5l11 7-11 7z"], emoji: "\u25B6\uFE0F" },
2413
+ sparkle: { label: "Sparkle", paths: ["M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z", "M19 16l.7 2 2 .7-2 .7-.7 2-.7-2-2-.7 2-.7z"], emoji: "\u2728" },
2414
+ globe: {
2415
+ label: "Globe",
2416
+ paths: ["M20 12a8 8 0 1 1-16 0 8 8 0 0 1 16 0z", "M4 12h16", "M12 4c2.2 2.2 3.3 4.9 3.3 8s-1.1 5.8-3.3 8c-2.2-2.2-3.3-4.9-3.3-8S9.8 6.2 12 4z"],
2417
+ emoji: "\u{1F310}"
2418
+ }
2419
+ };
2420
+ var FLOW_ICON_NAMES = Object.keys(FLOW_ICONS);
2421
+ var iconOf = (icon) => {
2422
+ if (!icon) return void 0;
2423
+ if (icon.kind === "builtin") return icon.name && FLOW_ICONS[icon.name] ? icon : void 0;
2424
+ if (icon.kind === "emoji") return icon.value && icon.value.trim() && [...icon.value.trim()].length <= 8 ? icon : void 0;
2425
+ if (icon.kind === "image") return icon.url && /^https?:\/\//i.test(icon.url) ? icon : void 0;
2426
+ return void 0;
2427
+ };
2428
+
2429
+ // src/native/flows-view.ts
2014
2430
  var RN = require("react-native");
2431
+ var svg = (() => {
2432
+ try {
2433
+ return require("react-native-svg");
2434
+ } catch {
2435
+ return null;
2436
+ }
2437
+ })();
2438
+ var iconNode = (icon, size, color) => {
2439
+ const i = iconOf(icon);
2440
+ if (!i) return null;
2441
+ if (i.kind === "image")
2442
+ return RN.Image ? (0, import_react2.createElement)(RN.Image, { source: { uri: i.url }, accessibilityElementsHidden: true, style: { width: size, height: size, borderRadius: size / 4 } }) : null;
2443
+ const Svg = svg?.Svg ?? svg?.default;
2444
+ if (i.kind === "builtin" && Svg && svg?.Path)
2445
+ return (0, import_react2.createElement)(
2446
+ Svg,
2447
+ { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
2448
+ ...FLOW_ICONS[i.name].paths.map((d, k) => (0, import_react2.createElement)(svg.Path, { key: k, d }))
2449
+ );
2450
+ return (0, import_react2.createElement)(RN.Text, { style: { fontSize: size * 0.8, lineHeight: size, textAlign: "center", width: size } }, i.kind === "emoji" ? i.value : FLOW_ICONS[i.name].emoji);
2451
+ };
2452
+ var FINALE_COLORS = ["#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#06b6d4", "#ec4899"];
2453
+ var finaleBits = (kind, accent) => {
2454
+ const rnd = (a, b) => a + Math.random() * (b - a);
2455
+ const colors = [accent, accent, ...FINALE_COLORS];
2456
+ const pick = () => colors[Math.floor(Math.random() * colors.length)];
2457
+ if (kind === "confetti")
2458
+ return Array.from({ length: 48 }, (_, i) => ({
2459
+ x: i % 2 ? 12 : 82,
2460
+ y: 92,
2461
+ dx: (i % 2 ? 1 : -1) * rnd(40, 200),
2462
+ mid: -rnd(380, 620),
2463
+ dy: -rnd(120, 300),
2464
+ size: rnd(7, 11),
2465
+ rot: rnd(-540, 540),
2466
+ color: pick(),
2467
+ delay: rnd(0, 160),
2468
+ life: rnd(1500, 2e3),
2469
+ shape: "rect"
2470
+ }));
2471
+ if (kind === "fireworks")
2472
+ return Array.from({ length: 3 }, (_, b) => {
2473
+ const x = rnd(20, 75);
2474
+ const y = rnd(18, 45);
2475
+ const color = pick();
2476
+ return Array.from({ length: 18 }, (_2, i) => {
2477
+ const a = Math.PI * 2 * i / 18;
2478
+ const r = rnd(70, 130);
2479
+ return { x, y, dx: Math.cos(a) * r, mid: Math.sin(a) * r * 0.8, dy: Math.sin(a) * r + 40, size: 5, rot: 0, color, delay: b * 320, life: rnd(900, 1200), shape: "dot" };
2480
+ });
2481
+ }).flat();
2482
+ return Array.from({ length: 22 }, () => ({
2483
+ x: rnd(6, 88),
2484
+ y: rnd(20, 80),
2485
+ dx: 0,
2486
+ mid: -rnd(10, 25),
2487
+ dy: -rnd(30, 60),
2488
+ size: rnd(16, 28),
2489
+ rot: rnd(-40, 40),
2490
+ color: pick(),
2491
+ delay: rnd(0, 600),
2492
+ life: rnd(900, 1300),
2493
+ shape: "star"
2494
+ }));
2495
+ };
2496
+ var Finale = ({ kind, accent }) => {
2497
+ const A = RN.Animated;
2498
+ const [bits] = (0, import_react2.useState)(() => finaleBits(kind, accent));
2499
+ const [values] = (0, import_react2.useState)(() => A ? bits.map(() => new A.Value(0)) : []);
2500
+ const [on, setOn] = (0, import_react2.useState)(!!A);
2501
+ (0, import_react2.useEffect)(() => {
2502
+ if (!A) return;
2503
+ let stopped = false;
2504
+ let run = null;
2505
+ const go = () => {
2506
+ if (stopped) return;
2507
+ run = A.parallel(values.map((v, i) => A.timing(v, { toValue: 1, duration: bits[i].life, delay: bits[i].delay, useNativeDriver: true })));
2508
+ run.start(() => !stopped && setOn(false));
2509
+ };
2510
+ Promise.resolve(RN.AccessibilityInfo?.isReduceMotionEnabled?.()).then((reduce) => reduce ? !stopped && setOn(false) : go()).catch(go);
2511
+ return () => {
2512
+ stopped = true;
2513
+ run?.stop();
2514
+ };
2515
+ }, [A, bits, values]);
2516
+ if (!on || !A) return null;
2517
+ return (0, import_react2.createElement)(
2518
+ RN.View,
2519
+ { pointerEvents: "none", accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 } },
2520
+ ...bits.map((b, i) => {
2521
+ const t2 = values[i];
2522
+ const fade = t2.interpolate({ inputRange: [0, 0.7, 1], outputRange: [1, 1, 0] });
2523
+ const scale = b.shape === "star" ? t2.interpolate({ inputRange: [0, 0.25, 1], outputRange: [0.2, 1.15, 0.6] }) : 1;
2524
+ return (0, import_react2.createElement)(
2525
+ A.View,
2526
+ {
2527
+ key: i,
2528
+ style: {
2529
+ position: "absolute",
2530
+ left: `${b.x}%`,
2531
+ top: `${b.y}%`,
2532
+ width: b.size,
2533
+ height: b.shape === "rect" ? b.size * 0.45 : b.size,
2534
+ borderRadius: b.shape === "dot" ? b.size / 2 : 2,
2535
+ backgroundColor: b.shape === "star" ? "transparent" : b.color,
2536
+ alignItems: "center",
2537
+ justifyContent: "center",
2538
+ opacity: fade,
2539
+ transform: [
2540
+ { translateX: t2.interpolate({ inputRange: [0, 1], outputRange: [0, b.dx] }) },
2541
+ { translateY: t2.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, b.mid, b.dy] }) },
2542
+ { rotate: t2.interpolate({ inputRange: [0, 1], outputRange: ["0deg", `${b.rot}deg`] }) },
2543
+ { scale }
2544
+ ]
2545
+ }
2546
+ },
2547
+ b.shape === "star" ? (0, import_react2.createElement)(RN.Text, { style: { color: b.color, fontSize: b.size, lineHeight: b.size } }, "\u2605") : null
2548
+ );
2549
+ })
2550
+ );
2551
+ };
2015
2552
  var LIGHT = { bg: "#ffffff", fg: "#1c1d20", muted: "#5d6068", soft: "#f4f5f7", line: "#e4e5e8" };
2016
2553
  var DARK = { bg: "#17181b", fg: "#f1f2f4", muted: "#a9acb3", soft: "#232428", line: "#2e2f35" };
2017
- var months = (n) => `${n} ${n === 1 ? "month" : "months"}`;
2018
- var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2554
+ var MRRocketFlows = (props) => {
2555
+ const [, setTick] = (0, import_react2.useState)(0);
2556
+ (0, import_react2.useEffect)(() => props.flows.subscribe(() => setTick((t2) => t2 + 1)), [props.flows]);
2557
+ const fin = props.flows.finale?.() ?? null;
2558
+ const accent = props.appearance?.accent ?? props.flows.accent?.() ?? "#3f76ff";
2559
+ 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);
2560
+ };
2561
+ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2019
2562
  const scheme = RN.useColorScheme?.();
2020
2563
  const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
2021
- const p = { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
2022
- const [, setTick] = (0, import_react.useState)(0);
2023
- (0, import_react.useEffect)(() => flows.subscribe(() => setTick((t) => t + 1)), [flows]);
2024
- (0, import_react.useEffect)(() => paywall?.subscribe(() => setTick((t) => t + 1)), [paywall]);
2564
+ const p = { ...dark ? DARK : LIGHT, accent: appearance.accent ?? flows.accent?.() ?? "#3f76ff", danger: "#d92d20" };
2565
+ const [, setTick] = (0, import_react2.useState)(0);
2566
+ (0, import_react2.useEffect)(() => flows.subscribe(() => setTick((t2) => t2 + 1)), [flows]);
2567
+ (0, import_react2.useEffect)(() => paywall?.subscribe(() => setTick((t2) => t2 + 1)), [paywall]);
2025
2568
  const pw = paywall?.view() ?? null;
2026
2569
  const v = flows.view();
2027
- const [comment, setComment] = (0, import_react.useState)("");
2570
+ const [comment, setComment] = (0, import_react2.useState)("");
2028
2571
  const screen = v?.type === "cancel" ? v.screen : null;
2029
- (0, import_react.useEffect)(() => {
2572
+ (0, import_react2.useEffect)(() => {
2030
2573
  if (!screen) setComment("");
2031
2574
  }, [screen]);
2032
2575
  if (!v && !pw) return null;
2033
- const text2 = (children, style = {}) => (0, import_react.createElement)(RN.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
2576
+ const text2 = (children, style = {}) => (0, import_react2.createElement)(RN.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
2034
2577
  const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
2035
- const title = (children) => (0, import_react.createElement)(RN.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 18, fontWeight: "700" } }, children);
2036
- const btn = (label, kind, onPress, disabled = false) => (0, import_react.createElement)(
2578
+ const title = (children) => (0, import_react2.createElement)(RN.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 18, fontWeight: "700" } }, children);
2579
+ const btn = (label, kind, onPress, disabled = false) => (0, import_react2.createElement)(
2037
2580
  RN.Pressable,
2038
2581
  {
2039
2582
  key: label,
@@ -2057,12 +2600,12 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2057
2600
  fontWeight: kind === "quiet" ? "500" : "600"
2058
2601
  })
2059
2602
  );
2060
- const close = (label, onPress) => (0, import_react.createElement)(
2603
+ const close = (label, onPress) => (0, import_react2.createElement)(
2061
2604
  RN.Pressable,
2062
2605
  { onPress, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 12, style: { marginLeft: "auto", padding: 4 } },
2063
2606
  text2("\xD7", { fontSize: 22, color: p.muted })
2064
2607
  );
2065
- const card = (children, style = {}) => (0, import_react.createElement)(
2608
+ const card = (children, style = {}) => (0, import_react2.createElement)(
2066
2609
  RN.View,
2067
2610
  {
2068
2611
  style: {
@@ -2079,16 +2622,16 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2079
2622
  },
2080
2623
  ...children
2081
2624
  );
2082
- const sheet = (children, onClose) => (0, import_react.createElement)(
2625
+ const sheet = (children, onClose) => (0, import_react2.createElement)(
2083
2626
  RN.Modal,
2084
2627
  { visible: true, transparent: true, animationType: "fade", onRequestClose: onClose },
2085
- (0, import_react.createElement)(
2628
+ (0, import_react2.createElement)(
2086
2629
  RN.View,
2087
2630
  { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.4)", justifyContent: "flex-end" } },
2088
2631
  card(children, { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, paddingBottom: 34 })
2089
2632
  )
2090
2633
  );
2091
- const input = (placeholder, value = comment, onChange = setComment) => (0, import_react.createElement)(RN.TextInput, {
2634
+ const input = (placeholder, value = comment, onChange = setComment) => (0, import_react2.createElement)(RN.TextInput, {
2092
2635
  value,
2093
2636
  onChangeText: onChange,
2094
2637
  placeholder,
@@ -2103,19 +2646,19 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2103
2646
  const host = paywall;
2104
2647
  return sheet(
2105
2648
  [
2106
- (0, import_react.createElement)(
2649
+ (0, import_react2.createElement)(
2107
2650
  RN.View,
2108
2651
  { key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
2109
2652
  ...w.total > 1 ? Array.from(
2110
2653
  { length: w.total },
2111
- (_, i) => (0, import_react.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= w.index ? p.accent : p.line } })
2654
+ (_, i) => (0, import_react2.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= w.index ? p.accent : p.line } })
2112
2655
  ) : [],
2113
- close("Close", host.close)
2656
+ close(t("Close"), host.close)
2114
2657
  ),
2115
2658
  title(pg.title),
2116
2659
  pg.body ? muted(pg.body, { fontSize: 15 }) : null,
2117
2660
  ...pg.kind === "benefits" ? (pg.items ?? []).map(
2118
- (item, i) => (0, import_react.createElement)(
2661
+ (item, i) => (0, import_react2.createElement)(
2119
2662
  RN.View,
2120
2663
  { key: `b${i}`, style: { flexDirection: "row", gap: 10, alignItems: "flex-start" } },
2121
2664
  text2("\u2713", { color: p.accent, fontWeight: "700" }),
@@ -2123,13 +2666,13 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2123
2666
  )
2124
2667
  ) : [],
2125
2668
  ...pg.kind === "proof" ? (pg.quotes ?? []).map(
2126
- (q, i) => (0, import_react.createElement)(RN.View, { key: `q${i}`, style: { backgroundColor: p.soft, borderRadius: 12, padding: 12, gap: 6 } }, text2(q.text), q.by ? muted(q.by) : null)
2669
+ (q, i) => (0, import_react2.createElement)(RN.View, { key: `q${i}`, style: { backgroundColor: p.soft, borderRadius: 12, padding: 12, gap: 6 } }, text2(q.text), q.by ? muted(q.by) : null)
2127
2670
  ) : [],
2128
- pg.kind === "plans" ? (0, import_react.createElement)(
2671
+ pg.kind === "plans" ? (0, import_react2.createElement)(
2129
2672
  RN.View,
2130
2673
  { key: "plans", accessibilityRole: "radiogroup", style: { gap: 6 } },
2131
2674
  ...w.plans.map(
2132
- (plan) => (0, import_react.createElement)(
2675
+ (plan) => (0, import_react2.createElement)(
2133
2676
  RN.Pressable,
2134
2677
  {
2135
2678
  key: plan.id,
@@ -2149,14 +2692,14 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2149
2692
  gap: 10
2150
2693
  }
2151
2694
  },
2152
- (0, import_react.createElement)(RN.View, { style: { flex: 1 } }, text2(plan.name, { fontWeight: "600" }), plan.note ? muted(plan.note) : null),
2695
+ (0, import_react2.createElement)(RN.View, { style: { flex: 1 } }, text2(plan.name, { fontWeight: "600" }), plan.note ? muted(plan.note) : null),
2153
2696
  text2(plan.price, { fontWeight: "700" })
2154
2697
  )
2155
2698
  )
2156
2699
  ) : null,
2157
2700
  w.error ? text2(w.error, { color: p.danger, fontSize: 13 }) : null,
2158
- btn(w.busy ? "One moment\u2026" : pg.button || "Continue", "primary", host.next, w.busy || pg.kind === "plans" && !w.plan),
2159
- w.index ? btn("Back", "quiet", host.back, w.busy) : null
2701
+ btn(w.busy ? t("One moment\u2026") : pg.button || t("Continue"), "primary", host.next, w.busy || pg.kind === "plans" && !w.plan),
2702
+ w.index ? btn(t("Back"), "quiet", host.back, w.busy) : null
2160
2703
  ],
2161
2704
  host.close
2162
2705
  );
@@ -2164,22 +2707,22 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2164
2707
  if (pw) return paywallSheet(pw);
2165
2708
  if (!v) return null;
2166
2709
  if (v.type === "cancel") {
2167
- const top = (0, import_react.createElement)(
2710
+ const top = (0, import_react2.createElement)(
2168
2711
  RN.View,
2169
2712
  { style: { flexDirection: "row", alignItems: "center", gap: 4 } },
2170
2713
  ...v.steps > 1 ? Array.from(
2171
2714
  { length: v.steps },
2172
- (_, i) => (0, import_react.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i < v.step ? p.accent : p.line } })
2715
+ (_, i) => (0, import_react2.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i < v.step ? p.accent : p.line } })
2173
2716
  ) : [],
2174
- close("Close", flows.cancel.close)
2717
+ close(t("Close"), flows.cancel.close)
2175
2718
  );
2176
2719
  const error = v.error ? text2(v.error, { color: p.danger, fontSize: 13 }) : null;
2177
2720
  let body = [];
2178
- const choices = (items) => (0, import_react.createElement)(
2721
+ const choices = (items) => (0, import_react2.createElement)(
2179
2722
  RN.ScrollView,
2180
2723
  { style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
2181
2724
  ...items.map(
2182
- (c) => (0, import_react.createElement)(
2725
+ (c) => (0, import_react2.createElement)(
2183
2726
  RN.Pressable,
2184
2727
  {
2185
2728
  key: c.key,
@@ -2202,48 +2745,48 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2202
2745
  );
2203
2746
  if (v.screen === "survey")
2204
2747
  body = [
2205
- title(v.flow.question || "Why are you leaving?"),
2748
+ title(v.flow.question || t("Why are you leaving?")),
2206
2749
  choices(v.flow.reasons.map((r) => ({ key: r.id, label: r.label, on: v.reason === r.id, press: () => flows.cancel.pick(r.id) }))),
2207
- btn("Continue", "primary", flows.cancel.next, !v.reason),
2208
- btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2750
+ btn(t("Continue"), "primary", flows.cancel.next, !v.reason),
2751
+ btn(t("Never mind, keep my plan"), "quiet", flows.cancel.close)
2209
2752
  ];
2210
2753
  else if (v.screen === "followup" && v.followUp) {
2211
2754
  const f = v.followUp;
2212
2755
  const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2213
2756
  body = [
2214
- label ? muted(`You said: ${label}`) : null,
2757
+ label ? muted(tf("You said: {label}", { label })) : null,
2215
2758
  title(f.question),
2216
- 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),
2217
- btn("Continue", "primary", flows.cancel.next),
2218
- btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2759
+ 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),
2760
+ btn(t("Continue"), "primary", flows.cancel.next),
2761
+ btn(t("Never mind, keep my plan"), "quiet", flows.cancel.close)
2219
2762
  ];
2220
2763
  } else if (v.screen === "offer" && v.offer) {
2221
2764
  const o = v.offer;
2222
2765
  const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2223
- 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;
2766
+ 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;
2224
2767
  body = [
2225
- label ? muted(`You said: ${label}`) : null,
2768
+ label ? muted(tf("You said: {label}", { label })) : null,
2226
2769
  title(o.headline),
2227
- detail ? (0, import_react.createElement)(RN.View, { style: { backgroundColor: p.soft, borderRadius: 10, padding: 12 } }, text2(detail, { fontWeight: "600" })) : null,
2228
- o.kind === "contact" ? input("What would make you stay?") : null,
2229
- btn(v.busy ? "One moment\u2026" : offerButton(o), "primary", () => flows.cancel.accept(comment), v.busy),
2230
- btn("No thanks, continue cancelling", "quiet", flows.cancel.decline, v.busy)
2770
+ detail ? (0, import_react2.createElement)(RN.View, { style: { backgroundColor: p.soft, borderRadius: 10, padding: 12 } }, text2(detail, { fontWeight: "600" })) : null,
2771
+ o.kind === "contact" ? input(t("What would make you stay?")) : null,
2772
+ btn(v.busy ? t("One moment\u2026") : offerButton(o), "primary", () => flows.cancel.accept(comment), v.busy),
2773
+ btn(t("No thanks, continue cancelling"), "quiet", flows.cancel.decline, v.busy)
2231
2774
  ];
2232
2775
  } else
2233
2776
  body = [
2234
2777
  title(v.flow.confirm.title),
2235
2778
  v.flow.confirm.body ? muted(v.flow.confirm.body, { fontSize: 15 }) : null,
2236
- v.steps > 1 ? input("Anything else we should know? (optional)") : null,
2237
- btn(v.busy ? "One moment\u2026" : v.flow.confirm.button, "danger", () => flows.cancel.confirm(comment), v.busy),
2238
- btn("Keep my plan", "secondary", flows.cancel.close, v.busy)
2779
+ v.steps > 1 ? input(t("Anything else we should know? (optional)")) : null,
2780
+ btn(v.busy ? t("One moment\u2026") : v.flow.confirm.button, "danger", () => flows.cancel.confirm(comment), v.busy),
2781
+ btn(t("Keep my plan"), "secondary", flows.cancel.close, v.busy)
2239
2782
  ];
2240
2783
  return sheet([top, ...body, error], flows.cancel.close);
2241
2784
  }
2242
- const media = (s) => {
2243
- const m2 = mediaOf(s);
2785
+ const media = (s2) => {
2786
+ const m2 = mediaOf(s2);
2244
2787
  if (!m2) return null;
2245
2788
  if (m2.kind === "image")
2246
- return RN.Image ? (0, import_react.createElement)(RN.Image, {
2789
+ return RN.Image ? (0, import_react2.createElement)(RN.Image, {
2247
2790
  key: "media",
2248
2791
  source: { uri: m2.url },
2249
2792
  accessibilityLabel: m2.alt,
@@ -2252,51 +2795,148 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2252
2795
  }) : null;
2253
2796
  const v2 = videoEmbed(m2.url);
2254
2797
  const open = v2?.kind === "file" ? v2.src : m2.url;
2255
- return RN.Linking ? btn("\u25B6 Watch the video", "secondary", () => void RN.Linking?.openURL(open).catch(() => {
2798
+ return RN.Linking ? btn(`\u25B6 ${t("Watch the video")}`, "secondary", () => void RN.Linking?.openURL(open).catch(() => {
2256
2799
  })) : null;
2257
2800
  };
2801
+ if (v.type === "step" && v.step.kind === "question") {
2802
+ const s2 = v.step;
2803
+ const q = s2.question;
2804
+ const ok = answerOk(q, v.answer);
2805
+ return sheet(
2806
+ [
2807
+ (0, import_react2.createElement)(
2808
+ RN.View,
2809
+ { key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
2810
+ ...v.total > 1 ? Array.from(
2811
+ { length: v.total },
2812
+ (_, i) => (0, import_react2.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= v.index ? p.accent : p.line } })
2813
+ ) : [],
2814
+ close(t("Close"), flows.onboarding.dismiss)
2815
+ ),
2816
+ v.index === 0 && v.flow.icon ? (0, import_react2.createElement)(RN.View, { key: "icon" }, iconNode(v.flow.icon, 36, p.accent)) : null,
2817
+ title(s2.title),
2818
+ s2.body ? muted(s2.body, { fontSize: 15 }) : null,
2819
+ q?.type === "choice" ? (0, import_react2.createElement)(
2820
+ RN.ScrollView,
2821
+ { key: "opts", style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
2822
+ ...[...q.options ?? [], ...q.other ? [{ id: OTHER_PREFIX, label: q.otherLabel || t("Other") }] : []].map((o) => {
2823
+ const isOther = o.id === OTHER_PREFIX;
2824
+ const on = isOther ? !!v.answer?.startsWith(OTHER_PREFIX) : v.answer === o.id;
2825
+ return (0, import_react2.createElement)(
2826
+ RN.Pressable,
2827
+ {
2828
+ key: o.id,
2829
+ onPress: () => isOther && on ? void 0 : flows.onboarding.answer(o.id),
2830
+ accessibilityRole: "radio",
2831
+ accessibilityState: { checked: on },
2832
+ style: {
2833
+ minHeight: 44,
2834
+ paddingHorizontal: 12,
2835
+ borderRadius: 10,
2836
+ borderWidth: 1,
2837
+ borderColor: on ? p.accent : p.line,
2838
+ backgroundColor: on ? p.soft : p.bg,
2839
+ justifyContent: "center"
2840
+ }
2841
+ },
2842
+ text2(o.label, { fontWeight: "500" })
2843
+ );
2844
+ }),
2845
+ q.other && v.answer?.startsWith(OTHER_PREFIX) ? (0, import_react2.createElement)(RN.TextInput, {
2846
+ key: "other",
2847
+ value: v.answer.slice(OTHER_PREFIX.length),
2848
+ onChangeText: (t2) => flows.onboarding.answer(OTHER_PREFIX + t2),
2849
+ onSubmitEditing: flows.onboarding.next,
2850
+ placeholder: t("Your answer"),
2851
+ placeholderTextColor: p.muted,
2852
+ maxLength: 490,
2853
+ accessibilityLabel: q.otherLabel || t("Other"),
2854
+ style: { height: 44, borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 12, color: p.fg }
2855
+ }) : null
2856
+ ) : q?.type === "scale" ? (0, import_react2.createElement)(
2857
+ RN.View,
2858
+ { key: "scale", style: { gap: 6 } },
2859
+ (0, import_react2.createElement)(
2860
+ RN.View,
2861
+ { style: { flexDirection: "row", gap: 4 }, accessibilityRole: "radiogroup" },
2862
+ ...scaleOf(q).map((n) => {
2863
+ const on = v.answer === String(n);
2864
+ return (0, import_react2.createElement)(
2865
+ RN.Pressable,
2866
+ {
2867
+ key: n,
2868
+ onPress: () => flows.onboarding.answer(String(n)),
2869
+ accessibilityRole: "radio",
2870
+ accessibilityState: { checked: on },
2871
+ 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" }
2872
+ },
2873
+ text2(String(n), { fontWeight: "500", color: on ? "#fff" : p.fg })
2874
+ );
2875
+ })
2876
+ ),
2877
+ q.lowLabel || q.highLabel ? (0, import_react2.createElement)(RN.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, muted(q.lowLabel || ""), muted(q.highLabel || "")) : null
2878
+ ) : q ? (0, import_react2.createElement)(RN.TextInput, {
2879
+ key: "answer",
2880
+ value: v.answer ?? "",
2881
+ onChangeText: (t2) => flows.onboarding.answer(t2),
2882
+ onSubmitEditing: flows.onboarding.next,
2883
+ placeholder: q.placeholder || t("Your answer"),
2884
+ placeholderTextColor: p.muted,
2885
+ maxLength: 500,
2886
+ accessibilityLabel: s2.title,
2887
+ style: { height: 44, borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 12, color: p.fg }
2888
+ }) : null,
2889
+ btn(s2.button || (v.last ? t("Done") : t("Continue")), "primary", flows.onboarding.next, !ok),
2890
+ q?.optional ? btn(q.skipLabel || t("Skip"), "quiet", flows.onboarding.skip) : null,
2891
+ v.back ? btn(t("Back"), "quiet", flows.onboarding.back) : null
2892
+ ],
2893
+ flows.onboarding.dismiss
2894
+ );
2895
+ }
2258
2896
  if (v.type === "step")
2259
2897
  return sheet(
2260
2898
  [
2261
- (0, import_react.createElement)(
2899
+ (0, import_react2.createElement)(
2262
2900
  RN.View,
2263
2901
  { style: { flexDirection: "row", alignItems: "center" } },
2264
- v.total > 1 ? muted(`Step ${v.index + 1} of ${v.total}`) : null,
2265
- close("Close the tour", flows.onboarding.dismiss)
2902
+ v.total > 1 ? muted(tf("Step {n} of {total}", { n: v.index + 1, total: v.total })) : null,
2903
+ close(t("Close the tour"), flows.onboarding.dismiss)
2266
2904
  ),
2905
+ v.step.kind === "modal" && v.flow.icon ? (0, import_react2.createElement)(RN.View, { key: "icon" }, iconNode(v.flow.icon, 36, p.accent)) : null,
2267
2906
  media(v.step),
2268
2907
  title(v.step.title),
2269
2908
  v.step.body ? muted(v.step.body, { fontSize: 15 }) : null,
2270
- btn(v.step.button || (v.last ? "Done" : "Next"), "primary", flows.onboarding.next)
2909
+ btn(v.step.button || (v.last ? t("Done") : t("Next")), "primary", flows.onboarding.next)
2271
2910
  ],
2272
2911
  flows.onboarding.dismiss
2273
2912
  );
2274
2913
  if (v.type === "checklist") {
2275
2914
  const done = v.items.filter((i) => i.done).length;
2276
- return (0, import_react.createElement)(
2915
+ return (0, import_react2.createElement)(
2277
2916
  RN.View,
2278
2917
  { pointerEvents: "box-none", style: { position: "absolute", left: 12, right: 12, bottom: 28 } },
2279
2918
  card([
2280
- (0, import_react.createElement)(
2919
+ (0, import_react2.createElement)(
2281
2920
  RN.View,
2282
2921
  { key: "t", style: { flexDirection: "row", alignItems: "center" } },
2283
2922
  title(v.flow.name),
2284
- close("Hide the checklist", flows.onboarding.dismiss)
2923
+ close(t("Hide the checklist"), flows.onboarding.dismiss)
2285
2924
  ),
2286
- muted(`${done} of ${v.items.length} done`),
2925
+ muted(tf("{done} of {total} done", { done, total: v.items.length })),
2287
2926
  ...v.items.map(
2288
- ({ step, done: d, tour }) => (0, import_react.createElement)(
2927
+ ({ step, done: d, opens }) => (0, import_react2.createElement)(
2289
2928
  RN.Pressable,
2290
2929
  {
2291
2930
  key: step.id,
2292
- // 0.17.0: an item that starts a tour opens it; finishing the tour ticks it.
2293
- disabled: d || !!step.event && !tour,
2294
- onPress: () => tour ? flows.onboarding.startTour(step.id) : flows.onboarding.check(step.id),
2295
- accessibilityRole: tour && !d ? "button" : "checkbox",
2931
+ // 0.17.0/0.18.0: a task that opens something (a tour, a modal, a screen, the app's code) does it;
2932
+ // a finished tour or modal ticks it, a screen or the app's code once opened (unless an event does).
2933
+ disabled: d || !!step.event && !opens,
2934
+ onPress: () => opens ? flows.onboarding.open(step.id) : flows.onboarding.check(step.id),
2935
+ accessibilityRole: opens && !d ? "button" : "checkbox",
2296
2936
  accessibilityState: { checked: d },
2297
2937
  style: { flexDirection: "row", gap: 10, alignItems: "flex-start", paddingVertical: 6 }
2298
2938
  },
2299
- (0, import_react.createElement)(
2939
+ (0, import_react2.createElement)(
2300
2940
  RN.View,
2301
2941
  {
2302
2942
  style: {
@@ -2312,7 +2952,8 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2312
2952
  },
2313
2953
  d ? text2("\u2713", { color: "#fff", fontSize: 12 }) : null
2314
2954
  ),
2315
- (0, import_react.createElement)(
2955
+ step.icon ? iconNode(step.icon, 20, p.accent) : null,
2956
+ (0, import_react2.createElement)(
2316
2957
  RN.View,
2317
2958
  { style: { flex: 1 } },
2318
2959
  text2(step.title, { fontWeight: "600", color: d ? p.muted : p.fg, textDecorationLine: d ? "line-through" : "none" }),
@@ -2325,22 +2966,22 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
2325
2966
  }
2326
2967
  const m = v.message;
2327
2968
  if (m.style === "banner")
2328
- return (0, import_react.createElement)(
2969
+ return (0, import_react2.createElement)(
2329
2970
  RN.View,
2330
2971
  { pointerEvents: "box-none", style: { position: "absolute", left: 12, right: 12, top: 56 } },
2331
2972
  card([
2332
- (0, import_react.createElement)(
2973
+ (0, import_react2.createElement)(
2333
2974
  RN.View,
2334
2975
  { key: "t", style: { flexDirection: "row", alignItems: "center", gap: 8 } },
2335
- (0, import_react.createElement)(RN.View, { style: { flex: 1 } }, text2(m.title, { fontWeight: "700" }), m.body ? muted(m.body) : null),
2336
- close("Dismiss", flows.message.dismiss)
2976
+ (0, import_react2.createElement)(RN.View, { style: { flex: 1 } }, text2(m.title, { fontWeight: "700" }), m.body ? muted(m.body) : null),
2977
+ close(t("Dismiss"), flows.message.dismiss)
2337
2978
  ),
2338
2979
  m.button ? btn(m.button, "primary", flows.message.click) : null
2339
2980
  ])
2340
2981
  );
2341
2982
  return sheet(
2342
2983
  [
2343
- (0, import_react.createElement)(RN.View, { key: "t", style: { flexDirection: "row" } }, close("Dismiss", flows.message.dismiss)),
2984
+ (0, import_react2.createElement)(RN.View, { key: "t", style: { flexDirection: "row" } }, close(t("Dismiss"), flows.message.dismiss)),
2344
2985
  title(m.title),
2345
2986
  m.body ? muted(m.body, { fontSize: 15 }) : null,
2346
2987
  m.button ? btn(m.button, "primary", flows.message.click) : null
@@ -2414,10 +3055,10 @@ var expoPush = () => {
2414
3055
  },
2415
3056
  token: async (projectId) => {
2416
3057
  if (RN2.Platform.OS === "android" && n.setNotificationChannelAsync)
2417
- await n.setNotificationChannelAsync("default", { name: "Default", importance: n.AndroidImportance?.HIGH ?? 4 });
3058
+ await n.setNotificationChannelAsync("default", { name: t("Default"), importance: n.AndroidImportance?.HIGH ?? 4 });
2418
3059
  const id = projectId ?? constants.expoConfig?.extra?.eas?.projectId ?? constants.easConfig?.projectId;
2419
- const t = await n.getExpoPushTokenAsync(id ? { projectId: id } : void 0);
2420
- return typeof t?.data === "string" && t.data ? t.data : null;
3060
+ const push = await n.getExpoPushTokenAsync(id ? { projectId: id } : void 0);
3061
+ return typeof push?.data === "string" && push.data ? push.data : null;
2421
3062
  },
2422
3063
  onOpen: (listener) => {
2423
3064
  const sub = n.addNotificationResponseReceivedListener((r) => listener(dataOf(r) ?? {}));
@@ -2428,8 +3069,8 @@ var expoPush = () => {
2428
3069
  };
2429
3070
  var asyncStorage = () => {
2430
3071
  const m = asyncStorageModule;
2431
- const s = m?.default ?? m;
2432
- return s && typeof s.getItem === "function" && typeof s.setItem === "function" && typeof s.removeItem === "function" ? s : null;
3072
+ const s2 = m?.default ?? m;
3073
+ return s2 && typeof s2.getItem === "function" && typeof s2.setItem === "function" && typeof s2.removeItem === "function" ? s2 : null;
2433
3074
  };
2434
3075
  var DEVICE_TYPES = { 1: "phone", 2: "tablet", 3: "desktop", 4: "tv" };
2435
3076
  var text = (v, max = 80) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : typeof v === "number" ? String(v) : void 0;
@@ -2473,10 +3114,10 @@ var recorderDeps = () => {
2473
3114
  }
2474
3115
  return {
2475
3116
  capture: async (width) => {
2476
- const s = RN2.Dimensions.get("screen");
2477
- if (!s?.width) return null;
3117
+ const s2 = RN2.Dimensions.get("screen");
3118
+ if (!s2?.width) return null;
2478
3119
  const w = Math.round(width);
2479
- const h3 = Math.round(s.height / s.width * width);
3120
+ const h3 = Math.round(s2.height / s2.width * width);
2480
3121
  const img = await capture({ format: "jpg", quality: 0.5, result: "base64", width: w, height: h3 });
2481
3122
  return img ? { img: img.replace(/^data:image\/\w+;base64,/, ""), w, h: h3 } : null;
2482
3123
  },
@@ -2503,8 +3144,8 @@ var nativeDeps = (overrides = {}) => ({
2503
3144
  context: readContext,
2504
3145
  appId: text(expoApplication?.applicationId, 150),
2505
3146
  screen: () => {
2506
- const s = RN2.Dimensions.get("screen");
2507
- return s ? { w: s.width, h: s.height } : void 0;
3147
+ const s2 = RN2.Dimensions.get("screen");
3148
+ return s2 ? { w: s2.width, h: s2.height } : void 0;
2508
3149
  },
2509
3150
  initialUrl: () => RN2.Linking.getInitialURL(),
2510
3151
  openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
@@ -2523,7 +3164,7 @@ var optionalModules = () => ({
2523
3164
  });
2524
3165
 
2525
3166
  // src/native/widgets-view.ts
2526
- var import_react2 = require("react");
3167
+ var import_react3 = require("react");
2527
3168
  var RN3 = require("react-native");
2528
3169
  var usePalette = (appearance = {}) => {
2529
3170
  const scheme = RN3.useColorScheme?.();
@@ -2531,10 +3172,10 @@ var usePalette = (appearance = {}) => {
2531
3172
  return { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
2532
3173
  };
2533
3174
  var kit = (p) => {
2534
- const text2 = (children, style = {}) => (0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
3175
+ const text2 = (children, style = {}) => (0, import_react3.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
2535
3176
  const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
2536
- const title = (children) => (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700", flex: 1 } }, children);
2537
- const btn = (label, kind, onPress, disabled = false, style = {}) => (0, import_react2.createElement)(
3177
+ const title = (children) => (0, import_react3.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700", flex: 1 } }, children);
3178
+ const btn = (label, kind, onPress, disabled = false, style = {}) => (0, import_react3.createElement)(
2538
3179
  RN3.Pressable,
2539
3180
  {
2540
3181
  key: label,
@@ -2557,8 +3198,8 @@ var kit = (p) => {
2557
3198
  },
2558
3199
  text2(label, { color: kind === "primary" ? "#ffffff" : p.fg, fontWeight: "600" })
2559
3200
  );
2560
- const closeX = (label, onPress) => (0, import_react2.createElement)(RN3.Pressable, { onPress, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 12, style: { padding: 4 } }, text2("\xD7", { fontSize: 24, color: p.muted }));
2561
- const field = (o) => (0, import_react2.createElement)(RN3.TextInput, {
3201
+ const closeX = (label, onPress) => (0, import_react3.createElement)(RN3.Pressable, { onPress, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 12, style: { padding: 4 } }, text2("\xD7", { fontSize: 24, color: p.muted }));
3202
+ const field = (o) => (0, import_react3.createElement)(RN3.TextInput, {
2562
3203
  value: o.value,
2563
3204
  onChangeText: o.onChange,
2564
3205
  placeholder: o.placeholder,
@@ -2581,7 +3222,7 @@ var kit = (p) => {
2581
3222
  ...o.style
2582
3223
  }
2583
3224
  });
2584
- const chip = (label, on, onPress, key = label) => (0, import_react2.createElement)(
3225
+ const chip = (label, on, onPress, key = label) => (0, import_react3.createElement)(
2585
3226
  RN3.Pressable,
2586
3227
  {
2587
3228
  key,
@@ -2592,17 +3233,17 @@ var kit = (p) => {
2592
3233
  },
2593
3234
  text2(label, { fontSize: 13, fontWeight: "600", color: on ? p.accent : p.fg })
2594
3235
  );
2595
- const page = (heading, onClose, children) => (0, import_react2.createElement)(
3236
+ const page = (heading, onClose, children) => (0, import_react3.createElement)(
2596
3237
  RN3.Modal,
2597
3238
  { visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: onClose },
2598
- (0, import_react2.createElement)(
3239
+ (0, import_react3.createElement)(
2599
3240
  RN3.KeyboardAvoidingView,
2600
3241
  { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
2601
- (0, import_react2.createElement)(
3242
+ (0, import_react3.createElement)(
2602
3243
  RN3.View,
2603
3244
  { style: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: p.line } },
2604
3245
  title(heading),
2605
- closeX("Close", onClose)
3246
+ closeX(t("Close"), onClose)
2606
3247
  ),
2607
3248
  ...children
2608
3249
  )
@@ -2610,13 +3251,13 @@ var kit = (p) => {
2610
3251
  return { text: text2, muted, title, btn, closeX, field, chip, page };
2611
3252
  };
2612
3253
  var timeAgo = (iso) => {
2613
- const t = new Date(iso).getTime();
2614
- if (Number.isNaN(t)) return "";
2615
- const d = Math.max(0, Date.now() - t) / 1e3;
2616
- if (d < 60) return "just now";
2617
- if (d < 3600) return `${Math.floor(d / 60)}m ago`;
2618
- if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
2619
- return `${Math.floor(d / 86400)}d ago`;
3254
+ const at = new Date(iso).getTime();
3255
+ if (Number.isNaN(at)) return "";
3256
+ const d = Math.max(0, Date.now() - at) / 1e3;
3257
+ if (d < 60) return t("just now");
3258
+ if (d < 3600) return tf("{n}m ago", { n: Math.floor(d / 60) });
3259
+ if (d < 86400) return tf("{n}h ago", { n: Math.floor(d / 3600) });
3260
+ return tf("{n}d ago", { n: Math.floor(d / 86400) });
2620
3261
  };
2621
3262
  var opener = () => {
2622
3263
  let open = false;
@@ -2626,8 +3267,8 @@ var opener = () => {
2626
3267
  listeners.forEach((l) => l(v));
2627
3268
  };
2628
3269
  const useOpen = () => {
2629
- const [v, setV] = (0, import_react2.useState)(open);
2630
- (0, import_react2.useEffect)(() => {
3270
+ const [v, setV] = (0, import_react3.useState)(open);
3271
+ (0, import_react3.useEffect)(() => {
2631
3272
  listeners.add(setV);
2632
3273
  setV(open);
2633
3274
  return () => {
@@ -2641,22 +3282,22 @@ var opener = () => {
2641
3282
  var isAnswered = (a) => Array.isArray(a) ? a.length > 0 : typeof a === "number" ? true : !!a?.trim();
2642
3283
  var SurveySheet = ({ survey, api, p, onDone }) => {
2643
3284
  const k = kit(p);
2644
- const [step, setStep] = (0, import_react2.useState)(0);
2645
- const [answers, setAnswers] = (0, import_react2.useState)({});
2646
- const [done, setDone] = (0, import_react2.useState)(false);
2647
- const [error, setError] = (0, import_react2.useState)("");
2648
- const started = (0, import_react2.useRef)(Date.now());
2649
- const finished = (0, import_react2.useRef)(false);
3285
+ const [step, setStep] = (0, import_react3.useState)(0);
3286
+ const [answers, setAnswers] = (0, import_react3.useState)({});
3287
+ const [done, setDone] = (0, import_react3.useState)(false);
3288
+ const [error, setError] = (0, import_react3.useState)("");
3289
+ const started = (0, import_react3.useRef)(Date.now());
3290
+ const finished = (0, import_react3.useRef)(false);
2650
3291
  const q = survey.questions[step];
2651
3292
  const last = step === survey.questions.length - 1;
2652
3293
  const value = answers[q.key];
2653
- (0, import_react2.useEffect)(() => {
3294
+ (0, import_react3.useEffect)(() => {
2654
3295
  api.seen(survey.id).catch(() => void 0);
2655
3296
  }, [api, survey.id]);
2656
- (0, import_react2.useEffect)(() => {
3297
+ (0, import_react3.useEffect)(() => {
2657
3298
  if (!done) return;
2658
- const t = setTimeout(onDone, 2500);
2659
- return () => clearTimeout(t);
3299
+ const t2 = setTimeout(onDone, 2500);
3300
+ return () => clearTimeout(t2);
2660
3301
  }, [done, onDone]);
2661
3302
  const finish = (completed, all = answers) => {
2662
3303
  if (finished.current) return;
@@ -2668,7 +3309,7 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
2668
3309
  onDone();
2669
3310
  };
2670
3311
  const next = () => {
2671
- if (q.required && !isAnswered(value)) return setError("Please answer this question.");
3312
+ if (q.required && !isAnswered(value)) return setError(t("Please answer this question."));
2672
3313
  setError("");
2673
3314
  if (!last) return setStep(step + 1);
2674
3315
  finish(true);
@@ -2682,40 +3323,40 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
2682
3323
  if (q.kind === "nps" || q.kind === "rating") {
2683
3324
  const scale = q.kind === "nps" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);
2684
3325
  const start = q.kind === "nps" ? 0 : 1;
2685
- input = (0, import_react2.createElement)(
3326
+ input = (0, import_react3.createElement)(
2686
3327
  RN3.View,
2687
3328
  { style: { gap: 6 } },
2688
- (0, import_react2.createElement)(
3329
+ (0, import_react3.createElement)(
2689
3330
  RN3.View,
2690
3331
  { style: { flexDirection: "row", flexWrap: "wrap", gap: 6 }, accessibilityRole: "radiogroup", accessibilityLabel: q.title },
2691
3332
  ...Array.from({ length: scale }, (_, i) => {
2692
3333
  const n = i + start;
2693
3334
  const on = value === n;
2694
- return (0, import_react2.createElement)(
3335
+ return (0, import_react3.createElement)(
2695
3336
  RN3.Pressable,
2696
3337
  {
2697
3338
  key: n,
2698
3339
  onPress: () => pick(n),
2699
3340
  accessibilityRole: "radio",
2700
3341
  accessibilityState: { checked: on },
2701
- accessibilityLabel: `${n} of ${scale - 1 + start}`,
3342
+ accessibilityLabel: tf("{n} of {total}", { n, total: scale - 1 + start }),
2702
3343
  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 }
2703
3344
  },
2704
3345
  k.text(String(n), { color: on ? "#ffffff" : p.fg, fontWeight: "600" })
2705
3346
  );
2706
3347
  })
2707
3348
  ),
2708
- q.low || q.high || q.kind === "nps" ? (0, import_react2.createElement)(RN3.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, k.muted(q.low ?? (q.kind === "nps" ? "Not likely" : "")), k.muted(q.high ?? (q.kind === "nps" ? "Very likely" : ""))) : null
3349
+ 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
2709
3350
  );
2710
3351
  } else if (q.kind === "single" || q.kind === "multi") {
2711
3352
  const multi = q.kind === "multi";
2712
3353
  const picked = multi ? value ?? [] : [];
2713
- input = (0, import_react2.createElement)(
3354
+ input = (0, import_react3.createElement)(
2714
3355
  RN3.View,
2715
3356
  { style: { gap: 6 } },
2716
- ...(q.options ?? []).map((o) => {
3357
+ ...(q.options ?? []).map((o, i) => {
2717
3358
  const on = multi ? picked.includes(o) : value === o;
2718
- return (0, import_react2.createElement)(
3359
+ return (0, import_react3.createElement)(
2719
3360
  RN3.Pressable,
2720
3361
  {
2721
3362
  key: o,
@@ -2724,32 +3365,33 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
2724
3365
  accessibilityState: { checked: on },
2725
3366
  style: { minHeight: 44, paddingHorizontal: 12, borderRadius: 10, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
2726
3367
  },
2727
- k.text(o, { fontWeight: "500" })
3368
+ // The label in the person's language; what is recorded stays the option itself.
3369
+ k.text(q.labels?.[i] || o, { fontWeight: "500" })
2728
3370
  );
2729
3371
  })
2730
3372
  );
2731
- } else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? "Type your answer", multiline: true });
2732
- const body = done ? [k.text(survey.thanks || "Thanks for your feedback.", { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
2733
- (0, import_react2.createElement)(
3373
+ } else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? t("Type your answer"), multiline: true });
3374
+ const body = done ? [k.text(survey.thanks || t("Thanks for your feedback."), { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
3375
+ (0, import_react3.createElement)(
2734
3376
  RN3.View,
2735
3377
  { key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
2736
- ...survey.questions.map((x, i) => (0, import_react2.createElement)(RN3.View, { key: x.key, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= step ? p.accent : p.line } })),
2737
- (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
2738
- k.closeX("Close survey", dismiss)
3378
+ ...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 } })),
3379
+ (0, import_react3.createElement)(RN3.View, { style: { flex: 1 } }),
3380
+ k.closeX(t("Close survey"), dismiss)
2739
3381
  ),
2740
- (0, import_react2.createElement)(RN3.Text, { key: "q", accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700" } }, q.title, q.required ? "" : (0, import_react2.createElement)(RN3.Text, { style: { color: p.muted, fontSize: 13, fontWeight: "400" } }, " optional")),
3382
+ (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")}`)),
2741
3383
  q.description ? k.muted(q.description) : null,
2742
3384
  input,
2743
3385
  error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2744
- k.btn(last ? "Send" : "Next", "primary", next)
3386
+ k.btn(last ? t("Send") : t("Next"), "primary", next)
2745
3387
  ];
2746
- return (0, import_react2.createElement)(
3388
+ return (0, import_react3.createElement)(
2747
3389
  RN3.Modal,
2748
3390
  { visible: true, transparent: true, animationType: "slide", onRequestClose: dismiss },
2749
- (0, import_react2.createElement)(
3391
+ (0, import_react3.createElement)(
2750
3392
  RN3.KeyboardAvoidingView,
2751
3393
  { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: "rgba(0,0,0,0.35)", justifyContent: "flex-end" } },
2752
- (0, import_react2.createElement)(
3394
+ (0, import_react3.createElement)(
2753
3395
  RN3.View,
2754
3396
  {
2755
3397
  accessibilityViewIsModal: true,
@@ -2763,10 +3405,10 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
2763
3405
  };
2764
3406
  var MRRocketSurveys = ({ client, appearance }) => {
2765
3407
  const p = usePalette(appearance);
2766
- const [survey, setSurvey] = (0, import_react2.useState)(null);
2767
- const [who2, setWho] = (0, import_react2.useState)(() => client.getDistinctId());
2768
- (0, import_react2.useEffect)(() => client.subscribe(() => setWho(client.getDistinctId())), [client]);
2769
- (0, import_react2.useEffect)(() => {
3408
+ const [survey, setSurvey] = (0, import_react3.useState)(null);
3409
+ const [who2, setWho] = (0, import_react3.useState)(() => client.getDistinctId());
3410
+ (0, import_react3.useEffect)(() => client.subscribe(() => setWho(client.getDistinctId())), [client]);
3411
+ (0, import_react3.useEffect)(() => {
2770
3412
  let stop = false;
2771
3413
  const ctrl = new AbortController();
2772
3414
  const timer = setTimeout(() => {
@@ -2774,9 +3416,9 @@ var MRRocketSurveys = ({ client, appearance }) => {
2774
3416
  const api = client.api();
2775
3417
  if (stop || !api) return;
2776
3418
  api.survey("", ctrl.signal).then((r) => {
2777
- const s = r.survey;
2778
- if (stop || !s || !s.questions.length || client.isSurveyDone(s.id)) return;
2779
- setSurvey({ s, api });
3419
+ const s2 = r.survey;
3420
+ if (stop || !s2 || !s2.questions.length || client.isSurveyDone(s2.id)) return;
3421
+ setSurvey({ s: s2, api });
2780
3422
  }).catch(() => void 0);
2781
3423
  });
2782
3424
  }, 3e3);
@@ -2786,35 +3428,35 @@ var MRRocketSurveys = ({ client, appearance }) => {
2786
3428
  ctrl.abort();
2787
3429
  };
2788
3430
  }, [client, who2]);
2789
- const done = (0, import_react2.useCallback)(() => {
3431
+ const done = (0, import_react3.useCallback)(() => {
2790
3432
  setSurvey((cur) => {
2791
3433
  if (cur) client.markSurveyDone(cur.s.id);
2792
3434
  return null;
2793
3435
  });
2794
3436
  }, [client]);
2795
3437
  if (!survey) return null;
2796
- return (0, import_react2.createElement)(SurveySheet, { key: survey.s.id, survey: survey.s, api: survey.api, p, onDone: done });
3438
+ return (0, import_react3.createElement)(SurveySheet, { key: survey.s.id, survey: survey.s, api: survey.api, p, onDone: done });
2797
3439
  };
2798
3440
  var feedbackOpen = opener();
2799
3441
  var useFeedback = () => ({ open: () => feedbackOpen.set(true), close: () => feedbackOpen.set(false) });
2800
3442
  var FeedbackSheet = ({ client, p, title }) => {
2801
3443
  const k = kit(p);
2802
- const [tab, setTab] = (0, import_react2.useState)("top");
2803
- const [posts, setPosts] = (0, import_react2.useState)(null);
2804
- const [boards, setBoards] = (0, import_react2.useState)([]);
2805
- const [error, setError] = (0, import_react2.useState)("");
2806
- const [board, setBoard] = (0, import_react2.useState)("feature");
2807
- const [subject, setSubject] = (0, import_react2.useState)("");
2808
- const [details, setDetails] = (0, import_react2.useState)("");
2809
- const [name, setName] = (0, import_react2.useState)("");
2810
- const [email, setEmail] = (0, import_react2.useState)("");
2811
- const [sending, setSending] = (0, import_react2.useState)(false);
2812
- const [sent, setSent] = (0, import_react2.useState)(false);
3444
+ const [tab, setTab] = (0, import_react3.useState)("top");
3445
+ const [posts, setPosts] = (0, import_react3.useState)(null);
3446
+ const [boards, setBoards] = (0, import_react3.useState)([]);
3447
+ const [error, setError] = (0, import_react3.useState)("");
3448
+ const [board, setBoard] = (0, import_react3.useState)("feature");
3449
+ const [subject, setSubject] = (0, import_react3.useState)("");
3450
+ const [details, setDetails] = (0, import_react3.useState)("");
3451
+ const [name, setName] = (0, import_react3.useState)("");
3452
+ const [email, setEmail] = (0, import_react3.useState)("");
3453
+ const [sending, setSending] = (0, import_react3.useState)(false);
3454
+ const [sent, setSent] = (0, import_react3.useState)(false);
2813
3455
  const close = () => feedbackOpen.set(false);
2814
3456
  const user = client.getUser();
2815
- (0, import_react2.useEffect)(() => {
3457
+ (0, import_react3.useEffect)(() => {
2816
3458
  const api = client.api();
2817
- if (!api) return setError("Feedback is off while tracking is off.");
3459
+ if (!api) return setError(t("Feedback is off while tracking is off."));
2818
3460
  const ctrl = new AbortController();
2819
3461
  api.list({ sort: "top" }, ctrl.signal).then((l) => {
2820
3462
  setPosts(l.posts);
@@ -2844,74 +3486,77 @@ var FeedbackSheet = ({ client, p, title }) => {
2844
3486
  setSent(true);
2845
3487
  }).catch((e) => setError(e.message)).finally(() => setSending(false));
2846
3488
  };
2847
- const tabs = (0, import_react2.createElement)(
3489
+ const tabs = (0, import_react3.createElement)(
2848
3490
  RN3.View,
2849
3491
  { style: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingTop: 12 } },
2850
- k.chip("Top requests", tab === "top", () => setTab("top")),
2851
- k.chip("Suggest", tab === "new", () => {
3492
+ k.chip(t("Top requests"), tab === "top", () => setTab("top")),
3493
+ k.chip(t("Suggest"), tab === "new", () => {
2852
3494
  setTab("new");
2853
3495
  setSent(false);
2854
3496
  })
2855
3497
  );
2856
- const list = (0, import_react2.createElement)(
3498
+ const list = (0, import_react3.createElement)(
2857
3499
  RN3.ScrollView,
2858
3500
  { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
2859
3501
  error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2860
- posts === null && !error ? k.muted("Loading\u2026") : null,
2861
- posts?.length === 0 ? k.muted("No requests yet. Be the first to suggest one.") : null,
3502
+ posts === null && !error ? k.muted(t("Loading\u2026")) : null,
3503
+ posts?.length === 0 ? k.muted(t("No requests yet. Be the first to suggest one.")) : null,
2862
3504
  ...(posts ?? []).map(
2863
- (post) => (0, import_react2.createElement)(
3505
+ (post) => (0, import_react3.createElement)(
2864
3506
  RN3.View,
2865
3507
  { key: post.id, style: { flexDirection: "row", gap: 12, alignItems: "flex-start", paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: p.line } },
2866
- (0, import_react2.createElement)(
3508
+ (0, import_react3.createElement)(
2867
3509
  RN3.Pressable,
2868
3510
  {
2869
3511
  onPress: () => vote(post),
2870
3512
  accessibilityRole: "button",
2871
3513
  accessibilityState: { selected: post.voted },
2872
- accessibilityLabel: `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
3514
+ 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 }) }),
2873
3515
  style: { width: 44, minHeight: 48, borderRadius: 10, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: post.voted ? p.accent : p.line }
2874
3516
  },
2875
3517
  k.text("\u25B2", { fontSize: 11, color: post.voted ? p.accent : p.muted }),
2876
3518
  k.text(String(post.votes), { fontWeight: "700", fontSize: 14, color: post.voted ? p.accent : p.fg })
2877
3519
  ),
2878
- (0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 2 } }, k.text(post.title, { fontWeight: "600" }), post.body ? k.muted(post.body.slice(0, 160)) : null)
3520
+ (0, import_react3.createElement)(RN3.View, { style: { flex: 1, gap: 2 } }, k.text(post.title, { fontWeight: "600" }), post.body ? k.muted(post.body.slice(0, 160)) : null)
2879
3521
  )
2880
3522
  )
2881
3523
  );
2882
- const form = (0, import_react2.createElement)(
3524
+ const form = (0, import_react3.createElement)(
2883
3525
  RN3.ScrollView,
2884
3526
  { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 }, keyboardShouldPersistTaps: "handled" },
2885
- sent ? k.text("Thanks! Your request is on the board.", { fontWeight: "600" }) : null,
2886
- boards.length > 1 ? (0, import_react2.createElement)(RN3.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 6 } }, ...boards.map((b) => k.chip(b.label, board === b.id, () => setBoard(b.id), b.id))) : null,
2887
- k.field({ value: subject, onChange: setSubject, placeholder: "Short title", max: 120 }),
2888
- k.field({ value: details, onChange: setDetails, placeholder: "Details (optional)", multiline: true }),
3527
+ sent ? k.text(t("Thanks! Your request is on the board."), { fontWeight: "600" }) : null,
3528
+ 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,
3529
+ k.field({ value: subject, onChange: setSubject, placeholder: t("Short title"), max: 120 }),
3530
+ k.field({ value: details, onChange: setDetails, placeholder: t("Details (optional)"), multiline: true }),
2889
3531
  ...user?.email ? [] : [
2890
- k.muted("Your name and email are optional; they let the team follow up."),
2891
- k.field({ value: name, onChange: setName, placeholder: "Your name (optional)", max: 100 }),
2892
- k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional)", max: 200, email: true })
3532
+ k.muted(t("Your name and email are optional; they let the team follow up.")),
3533
+ k.field({ value: name, onChange: setName, placeholder: t("Your name (optional)"), max: 100 }),
3534
+ k.field({ value: email, onChange: setEmail, placeholder: t("Your email (optional)"), max: 200, email: true })
2893
3535
  ],
2894
3536
  error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2895
- k.btn(sending ? "Sending\u2026" : "Send request", "primary", submit, subject.trim().length < 3 || sending)
3537
+ k.btn(sending ? t("Sending\u2026") : t("Send request"), "primary", submit, subject.trim().length < 3 || sending)
2896
3538
  );
2897
3539
  return k.page(title, close, [tabs, tab === "top" ? list : form]);
2898
3540
  };
2899
3541
  var MRRocketFeedback = ({ client, appearance, options = {} }) => {
2900
3542
  const p = usePalette(appearance);
2901
3543
  const open = feedbackOpen.useOpen();
2902
- return open ? (0, import_react2.createElement)(FeedbackSheet, { client, p, title: options.title ?? "Feedback" }) : null;
3544
+ return open ? (0, import_react3.createElement)(FeedbackSheet, { client, p, title: options.title ?? t("Feedback") }) : null;
2903
3545
  };
2904
3546
  var POLL_OPEN_MS = 4e3;
2905
3547
  var POLL_WAITING_MS = 8e3;
2906
3548
  var POLL_IDLE_MS = 3e4;
3549
+ var POLL_QUIET_MS = 6e4;
2907
3550
  var WAITING_FOR_MS = 10 * 6e4;
2908
3551
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
2909
- var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
2910
- var who = (from, agentName) => from === "ai" && agentName?.trim() || whoLabel[from];
3552
+ var whoLabel = (from) => from === "visitor" ? t("You") : from === "ai" ? t("AI agent") : t("Team");
3553
+ var who = (from, agentName) => from === "ai" && agentName?.trim() || whoLabel(from);
2911
3554
  var msgView = "home";
3555
+ var msgChat = null;
2912
3556
  var messengerOpen = opener();
2913
- var openAt = (v = "home") => {
3557
+ var openAt = (v = "home", chatId = null) => {
2914
3558
  msgView = v;
3559
+ msgChat = chatId;
2915
3560
  messengerOpen.set(true);
2916
3561
  };
2917
3562
  var useMessenger = () => ({
@@ -2938,12 +3583,15 @@ try {
2938
3583
  imagePicker = null;
2939
3584
  }
2940
3585
  var PHOTO_TYPES = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif" };
2941
- var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
2942
- var svg = null;
3586
+ var samePlace = (a, b) => a.chatId === b.chatId && a.fresh === b.fresh;
3587
+ var shownChat = (at, chats) => at.fresh ? null : at.chatId ?? (chats.length === 1 ? chats[0].id : null);
3588
+ var isAnswer = (m) => m.from === "team" || m.from === "ai";
3589
+ 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 }) : "");
3590
+ var svg2 = null;
2943
3591
  try {
2944
- svg = require("react-native-svg");
3592
+ svg2 = require("react-native-svg");
2945
3593
  } catch {
2946
- svg = null;
3594
+ svg2 = null;
2947
3595
  }
2948
3596
  var GLYPH = {
2949
3597
  chat: "M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z",
@@ -2958,15 +3606,15 @@ var GLYPH = {
2958
3606
  };
2959
3607
  var FALLBACK = { chat: "\u{1F4AC}", home: "\u2302", help: "?", send: "\u2192", up: "\u2191", next: "\u203A", search: "\u2315", cross: "\xD7", clip: "\u{1F4CE}" };
2960
3608
  var glyph = (name, color, size = 20) => {
2961
- const Svg = svg?.Svg ?? svg?.default;
2962
- if (!svg || !Svg) return (0, import_react2.createElement)(RN3.Text, { style: { color, fontSize: Math.round(size * 0.85), lineHeight: size + 2, textAlign: "center", minWidth: size } }, FALLBACK[name]);
2963
- return (0, import_react2.createElement)(Svg, { width: size, height: size, viewBox: "0 0 16 16", fill: "none" }, (0, import_react2.createElement)(svg.Path, { d: GLYPH[name], stroke: color, strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }));
3609
+ const Svg = svg2?.Svg ?? svg2?.default;
3610
+ if (!svg2 || !Svg) return (0, import_react3.createElement)(RN3.Text, { style: { color, fontSize: Math.round(size * 0.85), lineHeight: size + 2, textAlign: "center", minWidth: size } }, FALLBACK[name]);
3611
+ return (0, import_react3.createElement)(Svg, { width: size, height: size, viewBox: "0 0 16 16", fill: "none" }, (0, import_react3.createElement)(svg2.Path, { d: GLYPH[name], stroke: color, strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }));
2964
3612
  };
2965
3613
  var alpha = (hex, a) => /^#[0-9a-f]{6}$/i.test(hex) ? `${hex}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex;
2966
- var teamMark = (title, bg, fg, size = 36) => (0, import_react2.createElement)(
3614
+ var teamMark = (title, bg, fg, size = 36) => (0, import_react3.createElement)(
2967
3615
  RN3.View,
2968
3616
  { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: bg, alignItems: "center", justifyContent: "center" } },
2969
- (0, import_react2.createElement)(RN3.Text, { style: { color: fg, fontSize: Math.round(size * 0.42), fontWeight: "700" } }, (title.trim()[0] ?? "?").toUpperCase())
3617
+ (0, import_react3.createElement)(RN3.Text, { style: { color: fg, fontSize: Math.round(size * 0.42), fontWeight: "700" } }, (title.trim()[0] ?? "?").toUpperCase())
2970
3618
  );
2971
3619
  var HU = {
2972
3620
  light: { bg: "#ffffff", surface: "#f4f4f5", def: "#ececee", segment: "#ffffff", fg: "#18181b", muted: "#71717a" },
@@ -2974,43 +3622,81 @@ var HU = {
2974
3622
  };
2975
3623
  var hairline = { shadowColor: "#000", shadowOpacity: 0.07, shadowRadius: 3, shadowOffset: { width: 0, height: 1 }, elevation: 1 };
2976
3624
  var useChat = (client, o, open, view) => {
2977
- const [thread, setThread] = (0, import_react2.useState)(null);
2978
- const [loaded, setLoaded] = (0, import_react2.useState)(false);
2979
- const [error, setError] = (0, import_react2.useState)("");
2980
- const [unseen, setUnseen] = (0, import_react2.useState)([]);
2981
- const known = (0, import_react2.useRef)(null);
3625
+ const [thread, setThread] = (0, import_react3.useState)(null);
3626
+ const [chats, setChats] = (0, import_react3.useState)([]);
3627
+ const [hasEmail, setHasEmail] = (0, import_react3.useState)(false);
3628
+ const [place, setPlace] = (0, import_react3.useState)({ chatId: null, fresh: false });
3629
+ const [loaded, setLoaded] = (0, import_react3.useState)(false);
3630
+ const [error, setError] = (0, import_react3.useState)("");
3631
+ const [unseen, setUnseen] = (0, import_react3.useState)([]);
3632
+ const known = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
3633
+ const seeded = (0, import_react3.useRef)(false);
2982
3634
  const watching = open && view === "messages";
2983
- const watch = (0, import_react2.useRef)(watching);
3635
+ const watch = (0, import_react3.useRef)(watching);
2984
3636
  watch.current = watching;
2985
- const [active, setActive] = (0, import_react2.useState)(RN3.AppState.currentState === "active");
2986
- (0, import_react2.useEffect)(() => {
3637
+ const at = (0, import_react3.useRef)(place);
3638
+ const list = (0, import_react3.useRef)(chats);
3639
+ const readFailed = (0, import_react3.useRef)(false);
3640
+ const [active, setActive] = (0, import_react3.useState)(RN3.AppState.currentState === "active");
3641
+ (0, import_react3.useEffect)(() => {
2987
3642
  const sub = RN3.AppState.addEventListener("change", (st) => setActive(st === "active"));
2988
3643
  return () => sub.remove();
2989
3644
  }, []);
2990
- const take = (0, import_react2.useCallback)(
2991
- (t) => {
2992
- const team = teamOf(t);
2993
- if (known.current !== null) {
2994
- const i = team.findIndex((m) => m.id === known.current);
2995
- const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
2996
- if (fresh.length) {
2997
- if (o.haptics !== false) buzz();
2998
- if (!watch.current) setUnseen((u) => [...u, ...fresh]);
2999
- }
3000
- }
3001
- known.current = team.at(-1)?.id ?? "";
3002
- setThread(t);
3645
+ const enter = (0, import_react3.useCallback)((chatId, fresh = false) => {
3646
+ const next = { chatId, fresh };
3647
+ if (samePlace(at.current, next)) return;
3648
+ at.current = next;
3649
+ setPlace(next);
3650
+ setThread(null);
3651
+ setError("");
3652
+ }, []);
3653
+ const take = (0, import_react3.useCallback)(
3654
+ (t2) => {
3655
+ const read = t2.conversation;
3656
+ const last = t2.messages.at(-1);
3657
+ const all = t2.conversations ?? (read && last ? [{ id: read.id, status: read.status, subject: lineOf(t2.messages[0]), last }] : []);
3658
+ const fresh = [];
3659
+ const learn = (chat, answers, newest) => {
3660
+ const was = known.current.get(chat);
3661
+ const since = was ?? (seeded.current ? "" : void 0);
3662
+ if (since !== void 0) fresh.push(...answers.filter((m) => m.at > since).map((m) => ({ ...m, chat })));
3663
+ known.current.set(chat, newest ?? was ?? "");
3664
+ };
3665
+ for (const c of all) if (c.id !== read?.id) learn(c.id, c.last && isAnswer(c.last) ? [c.last] : [], c.last?.at);
3666
+ if (read) learn(read.id, t2.messages.filter(isAnswer), last?.at);
3667
+ seeded.current = true;
3668
+ if (fresh.length && o.haptics !== false) buzz();
3669
+ const shown2 = shownChat(at.current, all);
3670
+ const counted = fresh.filter((m) => !watch.current || m.chat !== shown2);
3671
+ setUnseen((u) => [...u.filter((m) => all.some((c) => c.id === m.chat)), ...counted]);
3672
+ list.current = all;
3673
+ setChats(all);
3674
+ setHasEmail(t2.hasEmail);
3675
+ if (at.current.chatId === null || read?.id === at.current.chatId) setThread(t2);
3003
3676
  },
3004
3677
  [o.haptics]
3005
3678
  );
3006
- (0, import_react2.useEffect)(() => {
3007
- if (watching) setUnseen([]);
3008
- }, [watching]);
3009
- (0, import_react2.useEffect)(() => {
3679
+ const arrived = (0, import_react3.useCallback)(
3680
+ (from, t2) => {
3681
+ const went = t2.conversation?.id;
3682
+ if (went && samePlace(at.current, from)) {
3683
+ at.current = { chatId: went, fresh: false };
3684
+ setPlace(at.current);
3685
+ }
3686
+ take(t2);
3687
+ },
3688
+ [take]
3689
+ );
3690
+ const shown = shownChat(place, chats);
3691
+ (0, import_react3.useEffect)(() => {
3692
+ if (watching && shown) setUnseen((u) => u.filter((m) => m.chat !== shown));
3693
+ }, [watching, shown]);
3694
+ (0, import_react3.useEffect)(() => {
3010
3695
  if (!active) return;
3011
3696
  let stop = false;
3012
3697
  let timer;
3013
3698
  let ctrl = null;
3699
+ let refused = false;
3014
3700
  const tick = async () => {
3015
3701
  const api = client.api();
3016
3702
  if (!api) {
@@ -3018,23 +3704,32 @@ var useChat = (client, o, open, view) => {
3018
3704
  return;
3019
3705
  }
3020
3706
  ctrl = new AbortController();
3021
- let t = null;
3022
3707
  try {
3023
3708
  await client.ready;
3024
- t = await api.chat(ctrl.signal);
3709
+ const t2 = await api.chat(ctrl.signal, place.chatId ?? void 0);
3025
3710
  if (stop) return;
3026
- take(t);
3027
- setError("");
3711
+ take(t2);
3712
+ if (readFailed.current) setError("");
3713
+ readFailed.current = false;
3028
3714
  } catch (e) {
3029
3715
  if (e.name === "AbortError" || stop) return;
3030
- if (watch.current) setError(e.message);
3716
+ refused = [401, 403].includes(e.status ?? 0);
3717
+ if (e.status === 404 && place.chatId) enter(null);
3718
+ else if (watch.current) {
3719
+ readFailed.current = true;
3720
+ setError(e.message);
3721
+ }
3031
3722
  }
3032
3723
  setLoaded(true);
3033
- const waiting = !!t?.messages.some((m) => m.from === "visitor") && t?.conversation?.status !== "solved";
3034
- const wrote = (t?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
3035
- const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
3036
- const ms = watch.current ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : waiting || open ? POLL_IDLE_MS : 0;
3724
+ const running = list.current.filter((c) => c.status !== "solved");
3725
+ const recent = running.some((c) => c.last && Date.now() - new Date(c.last.at).getTime() < WAITING_FOR_MS);
3726
+ const ms = watch.current ? POLL_OPEN_MS : running.length && recent ? POLL_WAITING_MS : running.length || open ? POLL_IDLE_MS : 0;
3037
3727
  if (!stop && ms) timer = setTimeout(tick, ms);
3728
+ else if (!stop) timer = setTimeout(quiet, POLL_QUIET_MS);
3729
+ };
3730
+ const quiet = () => {
3731
+ if (client.getUser() && !refused) void tick();
3732
+ else timer = setTimeout(quiet, POLL_QUIET_MS);
3038
3733
  };
3039
3734
  void tick();
3040
3735
  return () => {
@@ -3042,16 +3737,30 @@ var useChat = (client, o, open, view) => {
3042
3737
  ctrl?.abort();
3043
3738
  clearTimeout(timer);
3044
3739
  };
3045
- }, [client, take, active, open, watching]);
3046
- return { thread, loaded, error, setError, unseen, take };
3740
+ }, [client, take, enter, active, open, watching, place]);
3741
+ return {
3742
+ // A new conversation has nothing to show yet; polls keep reading the newest for the list.
3743
+ thread: place.fresh ? null : thread,
3744
+ chats,
3745
+ hasEmail,
3746
+ place,
3747
+ /** Messages shows the list: several conversations and none chosen. */
3748
+ listing: place.chatId === null && !place.fresh && chats.length > 1,
3749
+ enter,
3750
+ loaded,
3751
+ error,
3752
+ setError,
3753
+ unseen,
3754
+ arrived
3755
+ };
3047
3756
  };
3048
3757
  var useArticles = (client, on, query) => {
3049
- const [list, setList] = (0, import_react2.useState)(null);
3050
- const [any, setAny] = (0, import_react2.useState)(null);
3051
- (0, import_react2.useEffect)(() => {
3758
+ const [list, setList] = (0, import_react3.useState)(null);
3759
+ const [any, setAny] = (0, import_react3.useState)(null);
3760
+ (0, import_react3.useEffect)(() => {
3052
3761
  if (!on) return;
3053
3762
  let stop = false;
3054
- const t = setTimeout(
3763
+ const t2 = setTimeout(
3055
3764
  () => void client.ready.then(
3056
3765
  () => client.api()?.articles(query.trim() || void 0).then((r) => {
3057
3766
  if (stop) return;
@@ -3063,74 +3772,74 @@ var useArticles = (client, on, query) => {
3063
3772
  );
3064
3773
  return () => {
3065
3774
  stop = true;
3066
- clearTimeout(t);
3775
+ clearTimeout(t2);
3067
3776
  };
3068
3777
  }, [client, on, query]);
3069
3778
  return { list, any };
3070
3779
  };
3071
3780
  var spansOf = (p, spans, base) => spans.map(
3072
- (s, i) => (0, import_react2.createElement)(
3781
+ (s2, i) => (0, import_react3.createElement)(
3073
3782
  RN3.Text,
3074
3783
  {
3075
3784
  key: i,
3076
- onPress: s.href ? () => void RN3.Linking.openURL(s.href).catch(() => void 0) : void 0,
3077
- accessibilityRole: s.href ? "link" : void 0,
3785
+ onPress: s2.href ? () => void RN3.Linking.openURL(s2.href).catch(() => void 0) : void 0,
3786
+ accessibilityRole: s2.href ? "link" : void 0,
3078
3787
  style: {
3079
3788
  ...base,
3080
- ...s.bold ? { fontWeight: "700" } : {},
3081
- ...s.italic ? { fontStyle: "italic" } : {},
3082
- ...s.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
3083
- ...s.href ? { color: p.accent, textDecorationLine: "underline" } : {}
3789
+ ...s2.bold ? { fontWeight: "700" } : {},
3790
+ ...s2.italic ? { fontStyle: "italic" } : {},
3791
+ ...s2.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
3792
+ ...s2.href ? { color: p.accent, textDecorationLine: "underline" } : {}
3084
3793
  }
3085
3794
  },
3086
- s.text
3795
+ s2.text
3087
3796
  )
3088
3797
  );
3089
3798
  var ArticleView = ({ client, p, id, onAsk }) => {
3090
3799
  const k = kit(p);
3091
- const [a, setA] = (0, import_react2.useState)(null);
3092
- const [error, setError] = (0, import_react2.useState)("");
3093
- const [rated, setRated] = (0, import_react2.useState)(false);
3094
- (0, import_react2.useEffect)(() => {
3800
+ const [a, setA] = (0, import_react3.useState)(null);
3801
+ const [error, setError] = (0, import_react3.useState)("");
3802
+ const [rated, setRated] = (0, import_react3.useState)(false);
3803
+ (0, import_react3.useEffect)(() => {
3095
3804
  const ctrl = new AbortController();
3096
3805
  client.api()?.article(id, ctrl.signal).then(setA).catch((e) => e.name !== "AbortError" && setError(e.message));
3097
3806
  return () => ctrl.abort();
3098
3807
  }, [client, id]);
3099
3808
  if (error) return k.text(error, { color: p.danger });
3100
- if (!a) return k.muted("Loading\u2026");
3809
+ if (!a) return k.muted(t("Loading\u2026"));
3101
3810
  const base = { color: p.fg, fontSize: 15, lineHeight: 22 };
3102
3811
  const blocks = a.blocks.map((b, i) => {
3103
- if (b.type === "h2" || b.type === "h3") return (0, import_react2.createElement)(RN3.Text, { key: i, accessibilityRole: "header", style: { ...base, fontSize: b.type === "h2" ? 17 : 16, fontWeight: "700", marginTop: 8 } }, ...spansOf(p, b.spans, {}));
3104
- if (b.type === "p") return (0, import_react2.createElement)(RN3.Text, { key: i, style: base }, ...spansOf(p, b.spans, {}));
3105
- if (b.type === "quote") return (0, import_react2.createElement)(RN3.View, { key: i, style: { borderLeftWidth: 3, borderLeftColor: p.line, paddingLeft: 10 } }, (0, import_react2.createElement)(RN3.Text, { style: { ...base, color: p.muted } }, ...spansOf(p, b.spans, {})));
3812
+ 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, {}));
3813
+ if (b.type === "p") return (0, import_react3.createElement)(RN3.Text, { key: i, style: base }, ...spansOf(p, b.spans, {}));
3814
+ if (b.type === "quote") return (0, import_react3.createElement)(RN3.View, { key: i, style: { borderLeftWidth: 3, borderLeftColor: p.line, paddingLeft: 10 } }, (0, import_react3.createElement)(RN3.Text, { style: { ...base, color: p.muted } }, ...spansOf(p, b.spans, {})));
3106
3815
  if (b.type === "ul" || b.type === "ol")
3107
- return (0, import_react2.createElement)(
3816
+ return (0, import_react3.createElement)(
3108
3817
  RN3.View,
3109
3818
  { key: i, style: { gap: 4 } },
3110
- ...b.items.map((it, j) => (0, import_react2.createElement)(RN3.View, { key: j, style: { flexDirection: "row", gap: 8 } }, k.text(b.type === "ol" ? `${j + 1}.` : "\u2022", base), (0, import_react2.createElement)(RN3.Text, { style: { ...base, flex: 1 } }, ...spansOf(p, it, {}))))
3819
+ ...b.items.map((it, j) => (0, import_react3.createElement)(RN3.View, { key: j, style: { flexDirection: "row", gap: 8 } }, k.text(b.type === "ol" ? `${j + 1}.` : "\u2022", base), (0, import_react3.createElement)(RN3.Text, { style: { ...base, flex: 1 } }, ...spansOf(p, it, {}))))
3111
3820
  );
3112
- if (b.type === "code") return (0, import_react2.createElement)(RN3.View, { key: i, style: { backgroundColor: p.soft, borderRadius: 8, padding: 10 } }, k.text(b.text, { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", fontSize: 13 }));
3113
- return (0, import_react2.createElement)(RN3.View, { key: i, style: { height: 1, backgroundColor: p.line } });
3821
+ if (b.type === "code") return (0, import_react3.createElement)(RN3.View, { key: i, style: { backgroundColor: p.soft, borderRadius: 8, padding: 10 } }, k.text(b.text, { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", fontSize: 13 }));
3822
+ return (0, import_react3.createElement)(RN3.View, { key: i, style: { height: 1, backgroundColor: p.line } });
3114
3823
  });
3115
3824
  const rate = (helpful) => {
3116
3825
  setRated(true);
3117
3826
  client.api()?.rateArticle(id, helpful).catch(() => void 0);
3118
3827
  if (!helpful) onAsk();
3119
3828
  };
3120
- return (0, import_react2.createElement)(
3829
+ return (0, import_react3.createElement)(
3121
3830
  RN3.View,
3122
3831
  { style: { gap: 10 } },
3123
3832
  k.muted(a.collection),
3124
- (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 20, fontWeight: "700" } }, a.title),
3833
+ (0, import_react3.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 20, fontWeight: "700" } }, a.title),
3125
3834
  ...blocks,
3126
- (0, import_react2.createElement)(
3835
+ (0, import_react3.createElement)(
3127
3836
  RN3.View,
3128
3837
  { style: { flexDirection: "row", alignItems: "center", gap: 8, padding: 12, borderRadius: 10, backgroundColor: p.soft, marginTop: 8 } },
3129
- rated ? k.muted("Thanks for letting us know.") : k.muted("Did this answer your question?", { flex: 1 }),
3130
- rated ? null : k.btn("Yes", "secondary", () => rate(true), false, { height: 34 }),
3131
- rated ? null : k.btn("No", "secondary", () => rate(false), false, { height: 34 })
3838
+ rated ? k.muted(t("Thanks for letting us know.")) : k.muted(t("Did this answer your question?"), { flex: 1 }),
3839
+ rated ? null : k.btn(t("Yes"), "secondary", () => rate(true), false, { height: 34 }),
3840
+ rated ? null : k.btn(t("No"), "secondary", () => rate(false), false, { height: 34 })
3132
3841
  ),
3133
- k.btn("Still need help? Send us a message", "secondary", onAsk)
3842
+ k.btn(t("Still need help? Send us a message"), "secondary", onAsk)
3134
3843
  );
3135
3844
  };
3136
3845
  var MessengerSheet = ({
@@ -3143,33 +3852,42 @@ var MessengerSheet = ({
3143
3852
  brand
3144
3853
  }) => {
3145
3854
  const k = kit(p);
3146
- const [body, setBody] = (0, import_react2.useState)("");
3147
- const [email, setEmail] = (0, import_react2.useState)("");
3148
- const [pending, setPending] = (0, import_react2.useState)(null);
3149
- const [photos, setPhotos] = (0, import_react2.useState)([]);
3150
- const [query, setQuery] = (0, import_react2.useState)("");
3855
+ const [body, setBody] = (0, import_react3.useState)("");
3856
+ const [asked, setAsked] = (0, import_react3.useState)("");
3857
+ const [email, setEmail] = (0, import_react3.useState)("");
3858
+ const [pending, setPending] = (0, import_react3.useState)(null);
3859
+ const [slow, setSlow] = (0, import_react3.useState)(false);
3860
+ (0, import_react3.useEffect)(() => {
3861
+ setSlow(false);
3862
+ if (!pending) return;
3863
+ const t2 = setTimeout(() => setSlow(true), 1500);
3864
+ return () => clearTimeout(t2);
3865
+ }, [pending]);
3866
+ const [photos, setPhotos] = (0, import_react3.useState)([]);
3867
+ const [query, setQuery] = (0, import_react3.useState)("");
3151
3868
  const helpOn = o.help !== false;
3152
3869
  const arts = useArticles(client, helpOn, query);
3153
- const scroll = (0, import_react2.useRef)(null);
3870
+ const scroll = (0, import_react3.useRef)(null);
3154
3871
  const close = () => messengerOpen.set(false);
3155
- const greeting = o.greeting ?? "Hi! How can we help?";
3156
- const { thread } = chat;
3157
- const count = (thread?.messages.length ?? 0) + (pending ? 1 : 0);
3158
- (0, import_react2.useEffect)(() => {
3872
+ const greeting = o.greeting ?? t("Hi! How can we help?");
3873
+ const { thread, place } = chat;
3874
+ const pendingHere = pending && samePlace(pending.to, place) ? pending : null;
3875
+ const count = (thread?.messages.length ?? 0) + (pendingHere ? 1 : 0);
3876
+ (0, import_react3.useEffect)(() => {
3159
3877
  if (view === "messages") setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 50);
3160
- }, [count, view]);
3161
- const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
3878
+ }, [count, view, place]);
3879
+ const needEmail = (o.askEmail ?? true) && !chat.hasEmail && !client.getUser()?.email;
3162
3880
  const emailOk = !email.trim() || EMAIL.test(email.trim());
3163
3881
  const pickPhotos = async () => {
3164
3882
  const api = client.api();
3165
3883
  if (!api || !imagePicker?.launchImageLibraryAsync || pending) return;
3166
3884
  const room = CHAT_FILES_PER_MESSAGE - photos.length;
3167
- if (room <= 0) return chat.setError(`Up to ${CHAT_FILES_PER_MESSAGE} files per message.`);
3885
+ if (room <= 0) return chat.setError(tf("Up to {n} files per message.", { n: CHAT_FILES_PER_MESSAGE }));
3168
3886
  let res;
3169
3887
  try {
3170
3888
  res = await imagePicker.launchImageLibraryAsync({ allowsMultipleSelection: room > 1, selectionLimit: room, quality: 0.8, exif: false });
3171
3889
  } catch (e) {
3172
- return chat.setError(e.message || "The photos could not be opened.");
3890
+ return chat.setError(e.message || t("The photos could not be opened."));
3173
3891
  }
3174
3892
  if (res.canceled || res.cancelled || !res.assets?.length) return;
3175
3893
  chat.setError("");
@@ -3178,11 +3896,11 @@ var MessengerSheet = ({
3178
3896
  const name = a.fileName || a.uri.split("/").pop() || "photo.jpg";
3179
3897
  const type = a.mimeType && Object.values(PHOTO_TYPES).includes(a.mimeType) ? a.mimeType : PHOTO_TYPES[name.split(".").pop()?.toLowerCase() ?? ""];
3180
3898
  if (!type) {
3181
- chat.setError("Only pictures (PNG, JPEG, WebP, GIF) can be sent.");
3899
+ chat.setError(t("Only pictures (PNG, JPEG, WebP, GIF) can be sent."));
3182
3900
  continue;
3183
3901
  }
3184
3902
  if (a.fileSize && a.fileSize > CHAT_FILE_MAX_BYTES) {
3185
- chat.setError(`${name} is larger than 10 MB.`);
3903
+ chat.setError(tf("{name} is larger than 10 MB.", { name }));
3186
3904
  continue;
3187
3905
  }
3188
3906
  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() };
@@ -3201,29 +3919,29 @@ var MessengerSheet = ({
3201
3919
  setPhotos((list) => [...list, ...added]);
3202
3920
  };
3203
3921
  const canSend = (!!body.trim() || photos.length > 0) && !pending && emailOk;
3204
- const send = async () => {
3205
- const text2 = body.trim();
3922
+ const deliver = async (text2, files, to) => {
3206
3923
  const api = client.api();
3207
- const files = photos;
3208
- if (!text2 && !files.length || pending || !emailOk || !api) return;
3924
+ if (!api) return;
3209
3925
  setPending({
3210
3926
  id: "pending",
3211
3927
  from: "visitor",
3212
3928
  body: text2,
3213
3929
  at: (/* @__PURE__ */ new Date()).toISOString(),
3930
+ to,
3214
3931
  ...files.length ? { attachments: files.map((f) => ({ id: f.key, name: f.name, type: f.type, size: f.size })) } : {}
3215
3932
  });
3216
- setBody("");
3217
- setPhotos([]);
3218
3933
  chat.setError("");
3219
3934
  try {
3220
3935
  await Promise.all(files.map((f) => f.up));
3221
3936
  const ids = files.filter((f) => f.id).map((f) => f.id);
3222
- if (!text2 && !ids.length) throw new Error("The photo could not be sent.");
3937
+ if (!text2 && !ids.length) throw new Error(t("The photo could not be sent."));
3223
3938
  const win = RN3.Dimensions.get("screen");
3224
- chat.take(
3939
+ const conversation = to.fresh ? "new" : to.chatId ?? thread?.conversation?.id;
3940
+ chat.arrived(
3941
+ to,
3225
3942
  await api.sendChat({
3226
3943
  body: text2,
3944
+ ...conversation ? { conversation } : {},
3227
3945
  email: needEmail && email.trim() ? email.trim() : void 0,
3228
3946
  ...ids.length ? { attachments: ids } : {},
3229
3947
  // A little context for a bug report: the system and the screen size.
@@ -3231,14 +3949,45 @@ var MessengerSheet = ({
3231
3949
  })
3232
3950
  );
3233
3951
  } catch (e) {
3234
- setBody(text2);
3235
- setPhotos(files.filter((f) => f.id));
3236
- chat.setError(e.message);
3952
+ const gone = e.status === 404;
3953
+ chat.enter(gone ? null : to.chatId, gone || to.fresh);
3954
+ setBody((typed) => [text2, typed.trim()].filter(Boolean).join("\n"));
3955
+ setPhotos((list) => [...files.filter((f) => f.id), ...list]);
3956
+ chat.setError(gone ? t("This conversation is no longer available. Your message is still here to send as a new one.") : e.message);
3237
3957
  } finally {
3238
3958
  setPending(null);
3239
3959
  }
3240
3960
  };
3241
- const localUris = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3961
+ const send = () => {
3962
+ const text2 = body.trim();
3963
+ const files = photos;
3964
+ if (!text2 && !files.length || pending || !emailOk || !client.api()) return;
3965
+ setBody("");
3966
+ setPhotos([]);
3967
+ void deliver(text2, files, place);
3968
+ };
3969
+ const openMessages = () => {
3970
+ chat.enter(null);
3971
+ setView("messages");
3972
+ };
3973
+ const openChat = (id) => {
3974
+ chat.enter(id);
3975
+ setView("messages");
3976
+ };
3977
+ const startChat = () => {
3978
+ chat.enter(null, true);
3979
+ setView("messages");
3980
+ };
3981
+ const ask = () => {
3982
+ const text2 = asked.trim();
3983
+ if (!text2) return openMessages();
3984
+ if (pending || !client.api()) return;
3985
+ setAsked("");
3986
+ startChat();
3987
+ if (emailOk) void deliver(text2, [], { chatId: null, fresh: true });
3988
+ else setBody((typed) => [text2, typed.trim()].filter(Boolean).join("\n"));
3989
+ };
3990
+ const localUris = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
3242
3991
  for (const f of photos) localUris.current.set(f.key, f.uri);
3243
3992
  const fileUrl = (id) => localUris.current.get(id) ?? client.api()?.attachmentUrl(id) ?? "";
3244
3993
  const ink = inkOn(p.accent);
@@ -3253,11 +4002,11 @@ var MessengerSheet = ({
3253
4002
  const title = o.title?.trim() ?? "";
3254
4003
  const bubble = (m, isPending = false) => {
3255
4004
  const mine = m.from === "visitor";
3256
- return (0, import_react2.createElement)(
4005
+ return (0, import_react3.createElement)(
3257
4006
  RN3.View,
3258
4007
  { key: m.id, style: { alignSelf: mine ? "flex-end" : "flex-start", maxWidth: "82%", gap: 4 } },
3259
4008
  ...(m.attachments ?? []).map(
3260
- (f) => (0, import_react2.createElement)(
4009
+ (f) => (0, import_react3.createElement)(
3261
4010
  RN3.Pressable,
3262
4011
  {
3263
4012
  key: f.id,
@@ -3266,10 +4015,10 @@ var MessengerSheet = ({
3266
4015
  accessibilityLabel: f.name,
3267
4016
  style: { alignSelf: mine ? "flex-end" : "flex-start", opacity: isPending ? 0.7 : 1 }
3268
4017
  },
3269
- f.type.startsWith("image/") ? (0, import_react2.createElement)(RN3.Image, { source: { uri: fileUrl(f.id) }, resizeMode: "cover", style: { width: 200, height: 150, borderRadius: r.bubble, backgroundColor: p.soft } }) : (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.soft, borderRadius: r.card, paddingHorizontal: 10, paddingVertical: 7 } }, k.text(`\u{1F4C4} ${f.name}`, { fontSize: 13 }))
4018
+ f.type.startsWith("image/") ? (0, import_react3.createElement)(RN3.Image, { source: { uri: fileUrl(f.id) }, resizeMode: "cover", style: { width: 200, height: 150, borderRadius: r.bubble, backgroundColor: p.soft } }) : (0, import_react3.createElement)(RN3.View, { style: { backgroundColor: p.soft, borderRadius: r.card, paddingHorizontal: 10, paddingVertical: 7 } }, k.text(`\u{1F4C4} ${f.name}`, { fontSize: 13 }))
3270
4019
  )
3271
4020
  ),
3272
- !m.body && m.attachments?.length ? null : (0, import_react2.createElement)(
4021
+ !m.body && m.attachments?.length ? null : (0, import_react3.createElement)(
3273
4022
  RN3.View,
3274
4023
  {
3275
4024
  style: {
@@ -3286,120 +4035,149 @@ var MessengerSheet = ({
3286
4035
  k.text(m.body, { color: mine ? ink : hu.fg })
3287
4036
  ),
3288
4037
  ...(m.articles ?? []).map(
3289
- (a) => (0, import_react2.createElement)(
4038
+ (a) => (0, import_react3.createElement)(
3290
4039
  RN3.Pressable,
3291
4040
  { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { backgroundColor: p.soft, borderRadius: r.card, paddingHorizontal: 10, paddingVertical: 7 } },
3292
4041
  k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
3293
4042
  )
3294
4043
  ),
3295
- m.at ? k.muted(`${mine ? "" : `${who(m.from, o.agentName)} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
4044
+ 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
3296
4045
  );
3297
4046
  };
3298
- const row = (a) => (0, import_react2.createElement)(
4047
+ const row = (a) => (0, import_react3.createElement)(
3299
4048
  RN3.Pressable,
3300
4049
  { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "button", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 11, paddingHorizontal: 8 } },
3301
4050
  k.text(a.title, { flex: 1 }),
3302
4051
  glyph("next", p.accent, 14)
3303
4052
  );
3304
- const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: raised, borderRadius: r.card, padding: 16, ...surf, ...style } }, ...children);
3305
- const searchRow = (onPress) => (0, import_react2.createElement)(
4053
+ const card = (children, style = {}) => (0, import_react3.createElement)(RN3.View, { style: { backgroundColor: raised, borderRadius: r.card, padding: 16, ...surf, ...style } }, ...children);
4054
+ const searchRow = (onPress) => (0, import_react3.createElement)(
3306
4055
  RN3.Pressable,
3307
4056
  { key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: r.field, paddingHorizontal: 14, backgroundColor: hu.def } },
3308
- k.muted("Search for help", { flex: 1, fontSize: 15 }),
4057
+ k.muted(t("Search for help"), { flex: 1, fontSize: 15 }),
3309
4058
  glyph("search", p.accent, 16)
3310
4059
  );
3311
- const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: r.field, backgroundColor: raised, ...surf } });
4060
+ const search = k.field({ value: query, onChange: setQuery, placeholder: t("Search for help"), max: 200, style: { borderWidth: 0, borderRadius: r.field, backgroundColor: raised, ...surf } });
3312
4061
  const first = client.getUser()?.name?.trim().split(/\s+/)[0];
3313
- const rest = greeting.replace(/^(hi|hello|hey|hallo)( there)?\s*[!,.]*\s*/i, "").trim() || "How can we help you today?";
3314
- const hero = (fg, sub) => (0, import_react2.createElement)(
4062
+ 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?");
4063
+ const hero = (fg, sub) => (0, import_react3.createElement)(
3315
4064
  RN3.Text,
3316
4065
  { key: "hero", accessibilityRole: "header", style: { color: fg, fontSize: 25, fontWeight: "600", letterSpacing: -0.6, lineHeight: 30 } },
3317
- first ? `Hi, ${first} \u{1F44B}
3318
- ` : "Hi there \u{1F44B}\n",
3319
- (0, import_react2.createElement)(RN3.Text, { style: { color: sub, fontWeight: "500" } }, rest)
4066
+ `${first ? tf("Hi, {name}", { name: first }) : t("Hi there")} \u{1F44B}
4067
+ `,
4068
+ (0, import_react3.createElement)(RN3.Text, { style: { color: sub, fontWeight: "500" } }, rest)
3320
4069
  );
3321
- const last = thread?.messages.at(-1);
4070
+ const chatRow = (c) => {
4071
+ const m = c.last;
4072
+ const n = chat.unseen.filter((u) => u.chat === c.id).length;
4073
+ const closed = c.status === "solved" ? ` \xB7 ${t("Closed")}` : "";
4074
+ const subject = c.subject || (m ? lineOf(m) : "");
4075
+ const line = m ? lineOf(m) : "";
4076
+ return (0, import_react3.createElement)(
4077
+ RN3.Pressable,
4078
+ { key: c.id, onPress: () => openChat(c.id), accessibilityRole: "button", accessibilityLabel: `${tf("Conversation: {subject}", { subject })}${closed}${n ? `, ${tf("{n} new", { n })}` : ""}` },
4079
+ card(
4080
+ [
4081
+ (0, import_react3.createElement)(
4082
+ RN3.View,
4083
+ { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 10 } },
4084
+ title ? teamMark(title, alpha(p.accent, 0.16), p.accent, 26) : null,
4085
+ (0, import_react3.createElement)(
4086
+ RN3.View,
4087
+ { style: { flex: 1, gap: 1 } },
4088
+ (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.fg, fontSize: 14, fontWeight: "500" } }, subject),
4089
+ m && line !== subject ? (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.muted, fontSize: 13 } }, line) : null,
4090
+ m ? k.muted(`${who(m.from, o.agentName)} \xB7 ${timeAgo(m.at)}${closed}`, { fontSize: 11.5 }) : null
4091
+ ),
4092
+ 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)
4093
+ )
4094
+ ],
4095
+ { paddingVertical: 10, paddingHorizontal: 12 }
4096
+ )
4097
+ );
4098
+ };
3322
4099
  const showHelp = helpOn && arts.any !== false;
3323
4100
  let content;
3324
4101
  if (view === "home") {
4102
+ const running = chat.chats.filter((c) => c.status !== "solved");
4103
+ const rows = (running.length ? running : chat.chats).slice(0, 2);
3325
4104
  const cards = [
3326
- // "Ask a question": the words go into the chat's field and are sent as the chat opens.
3327
- (0, import_react2.createElement)(
4105
+ // "Ask a question": its own field; the words start a new conversation.
4106
+ (0, import_react3.createElement)(
3328
4107
  RN3.View,
3329
4108
  { key: "ask", style: { flexDirection: "row", alignItems: "center", gap: 8, height: 48, borderRadius: 24, paddingLeft: 18, paddingRight: 6, backgroundColor: hu.surface, ...surf } },
3330
- k.field({ value: body, onChange: setBody, placeholder: "Ask a question", max: 4e3, style: { flex: 1, borderWidth: 0, paddingHorizontal: 0, backgroundColor: "transparent" } }),
3331
- (0, import_react2.createElement)(
4109
+ k.field({ value: asked, onChange: setAsked, placeholder: t("Ask a question"), max: 4e3, onSubmit: ask, style: { flex: 1, borderWidth: 0, paddingHorizontal: 0, backgroundColor: "transparent" } }),
4110
+ (0, import_react3.createElement)(
3332
4111
  RN3.Pressable,
3333
4112
  {
3334
- onPress: () => {
3335
- setView("messages");
3336
- if (body.trim()) void send();
3337
- },
4113
+ onPress: ask,
3338
4114
  accessibilityRole: "button",
3339
- accessibilityLabel: "Send",
3340
- style: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: body.trim() ? p.accent : hu.def }
4115
+ accessibilityLabel: t("Send"),
4116
+ style: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: asked.trim() ? p.accent : hu.def }
3341
4117
  },
3342
- glyph("up", body.trim() ? ink : hu.muted, 18)
4118
+ glyph("up", asked.trim() ? ink : hu.muted, 18)
3343
4119
  )
3344
4120
  ),
3345
- last ? (0, import_react2.createElement)(
4121
+ ...rows.map(chatRow),
4122
+ chat.chats.length > rows.length ? (0, import_react3.createElement)(
3346
4123
  RN3.Pressable,
3347
- { key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
3348
- card([
3349
- (0, import_react2.createElement)(
3350
- RN3.View,
3351
- { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 10 } },
3352
- title ? teamMark(title, alpha(p.accent, 0.16), p.accent, 26) : null,
3353
- (0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 1 } }, (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.fg, fontSize: 14, fontWeight: "500" } }, last.body || (last.attachments?.length ? `Sent ${last.attachments.length === 1 ? last.attachments[0].name : `${last.attachments.length} files`}` : "")), k.muted(`${last.from === "visitor" ? "You" : who(last.from, o.agentName)} \xB7 ${timeAgo(last.at)}`, { fontSize: 11.5 })),
3354
- chat.unseen.length ? (0, import_react2.createElement)(RN3.View, { style: { width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : glyph("next", p.muted, 14)
3355
- )
3356
- ], { paddingVertical: 10, paddingHorizontal: 12 })
4124
+ { 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 } },
4125
+ k.muted(tf("All conversations ({n})", { n: chat.chats.length }), { fontSize: 13, fontWeight: "500" }),
4126
+ glyph("next", p.muted, 12)
3357
4127
  ) : null,
3358
4128
  showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
3359
4129
  ];
3360
- content = plain ? (0, import_react2.createElement)(
4130
+ content = plain ? (0, import_react3.createElement)(
3361
4131
  RN3.ScrollView,
3362
4132
  { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 24, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3363
- (0, import_react2.createElement)(RN3.View, { key: "top", style: { paddingHorizontal: 6, paddingBottom: 10 } }, hero(p.fg, p.muted)),
4133
+ (0, import_react3.createElement)(RN3.View, { key: "top", style: { paddingHorizontal: 6, paddingBottom: 10 } }, hero(p.fg, p.muted)),
3364
4134
  ...cards
3365
- ) : (0, import_react2.createElement)(
4135
+ ) : (0, import_react3.createElement)(
3366
4136
  RN3.ScrollView,
3367
4137
  { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingBottom: 24 }, keyboardShouldPersistTaps: "handled" },
3368
- (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, hero(ink, alpha(ink, 0.8))),
3369
- (0, import_react2.createElement)(RN3.View, { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } }, ...cards)
4138
+ (0, import_react3.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, hero(ink, alpha(ink, 0.8))),
4139
+ (0, import_react3.createElement)(RN3.View, { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } }, ...cards)
4140
+ );
4141
+ } else if (view === "messages" && chat.listing)
4142
+ content = (0, import_react3.createElement)(
4143
+ RN3.ScrollView,
4144
+ { key: "chats", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 } },
4145
+ ...chat.chats.map(chatRow),
4146
+ k.btn(t("New conversation"), "primary", startChat)
3370
4147
  );
3371
- } else if (view === "messages")
3372
- content = (0, import_react2.createElement)(
4148
+ else if (view === "messages")
4149
+ content = (0, import_react3.createElement)(
3373
4150
  RN3.View,
3374
4151
  { key: "messages", style: { flex: 1, backgroundColor: hu.bg } },
3375
- (0, import_react2.createElement)(
4152
+ (0, import_react3.createElement)(
3376
4153
  RN3.ScrollView,
3377
4154
  { ref: scroll, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 } },
3378
- bubble({ id: "greeting", from: "team", body: greeting, at: "" }),
3379
- !chat.loaded ? k.muted("Loading\u2026") : null,
4155
+ // The greeting opens an empty chat only, never above the visitor's first words (2026-10-30).
4156
+ !thread?.messages.length && !pendingHere && !place.chatId ? bubble({ id: "greeting", from: "team", body: greeting, at: "" }) : null,
4157
+ !chat.loaded && !place.fresh && !pendingHere || place.chatId && !thread ? k.muted(t("Loading\u2026")) : null,
3380
4158
  ...(thread?.messages ?? []).map((m) => bubble(m)),
3381
- pending ? bubble(pending, true) : null,
3382
- thread?.conversation?.status === "solved" && !pending ? k.muted("This conversation is closed. Write to start a new one.", { textAlign: "center" }) : null
4159
+ pendingHere ? bubble(pendingHere, true) : null,
4160
+ thread?.conversation?.status === "solved" && !pendingHere ? k.muted(t("This conversation is closed. Write to start a new one."), { textAlign: "center" }) : null
3383
4161
  ),
3384
- (0, import_react2.createElement)(
4162
+ (0, import_react3.createElement)(
3385
4163
  RN3.View,
3386
4164
  { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 6, gap: 8 } },
3387
4165
  chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
3388
4166
  // Photos picked for the next message, each with a way to take it out again.
3389
- photos.length ? (0, import_react2.createElement)(
4167
+ photos.length ? (0, import_react3.createElement)(
3390
4168
  RN3.View,
3391
4169
  { style: { flexDirection: "row", flexWrap: "wrap", gap: 10, paddingTop: 6, paddingHorizontal: 2 } },
3392
4170
  ...photos.map(
3393
- (f) => (0, import_react2.createElement)(
4171
+ (f) => (0, import_react3.createElement)(
3394
4172
  RN3.View,
3395
4173
  { key: f.key, style: { width: 56, height: 56 } },
3396
- (0, import_react2.createElement)(RN3.Image, { source: { uri: f.uri }, resizeMode: "cover", accessibilityLabel: f.id ? f.name : `${f.name}, uploading`, style: { width: 56, height: 56, borderRadius: Math.max(4, r.card - 4), backgroundColor: p.soft, opacity: f.id ? 1 : 0.5 } }),
3397
- (0, import_react2.createElement)(
4174
+ (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 } }),
4175
+ (0, import_react3.createElement)(
3398
4176
  RN3.Pressable,
3399
4177
  {
3400
4178
  onPress: () => setPhotos((list) => list.filter((x) => x.key !== f.key)),
3401
4179
  accessibilityRole: "button",
3402
- accessibilityLabel: `Remove ${f.name}`,
4180
+ accessibilityLabel: tf("Remove {name}", { name: f.name }),
3403
4181
  hitSlop: 8,
3404
4182
  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" }
3405
4183
  },
@@ -3409,35 +4187,35 @@ var MessengerSheet = ({
3409
4187
  )
3410
4188
  ) : null,
3411
4189
  // The message field on top, the paperclip (when the app has expo-image-picker) and the send button under it.
3412
- (0, import_react2.createElement)(
4190
+ (0, import_react3.createElement)(
3413
4191
  RN3.View,
3414
4192
  { style: { gap: 2, borderRadius: 20, paddingTop: 10, paddingBottom: 6, paddingRight: 6, paddingLeft: 16, backgroundColor: hu.surface, ...surf } },
3415
4193
  // The optional email: the box's first line, under a hairline the message follows.
3416
4194
  needEmail ? k.field({
3417
4195
  value: email,
3418
4196
  onChange: setEmail,
3419
- placeholder: "Email for our reply (optional)",
4197
+ placeholder: t("Email for our reply (optional)"),
3420
4198
  email: true,
3421
4199
  max: 200,
3422
4200
  style: { minHeight: 0, marginRight: 10, marginBottom: 4, paddingHorizontal: 0, paddingTop: 0, paddingBottom: 8, borderWidth: 0, borderRadius: 0, borderBottomWidth: 1, borderBottomColor: emailOk ? p.line : p.danger, backgroundColor: "transparent", fontSize: 13.5 }
3423
4201
  }) : null,
3424
- k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { minHeight: 28, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 4, backgroundColor: "transparent" } }),
3425
- (0, import_react2.createElement)(
4202
+ 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" } }),
4203
+ (0, import_react3.createElement)(
3426
4204
  RN3.View,
3427
4205
  { style: { flexDirection: "row", alignItems: "center", marginLeft: -8 } },
3428
- imagePicker?.launchImageLibraryAsync ? (0, import_react2.createElement)(
4206
+ imagePicker?.launchImageLibraryAsync ? (0, import_react3.createElement)(
3429
4207
  RN3.Pressable,
3430
- { 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" } },
4208
+ { 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" } },
3431
4209
  glyph("clip", hu.muted, 19)
3432
4210
  ) : null,
3433
- (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
3434
- (0, import_react2.createElement)(
4211
+ (0, import_react3.createElement)(RN3.View, { style: { flex: 1 } }),
4212
+ (0, import_react3.createElement)(
3435
4213
  RN3.Pressable,
3436
4214
  {
3437
- onPress: () => void send(),
4215
+ onPress: send,
3438
4216
  disabled: !canSend,
3439
4217
  accessibilityRole: "button",
3440
- accessibilityLabel: "Send",
4218
+ accessibilityLabel: t("Send"),
3441
4219
  style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: canSend ? p.accent : hu.def }
3442
4220
  },
3443
4221
  glyph("up", canSend ? ink : hu.muted, 18)
@@ -3449,28 +4227,28 @@ var MessengerSheet = ({
3449
4227
  else if (view === "help") {
3450
4228
  const groups = /* @__PURE__ */ new Map();
3451
4229
  for (const a of arts.list ?? []) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
3452
- content = (0, import_react2.createElement)(
4230
+ content = (0, import_react3.createElement)(
3453
4231
  RN3.ScrollView,
3454
4232
  { key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3455
4233
  search,
3456
- arts.list === null ? k.muted("Loading\u2026") : null,
3457
- arts.list?.length === 0 ? (0, import_react2.createElement)(RN3.View, { style: { gap: 10, alignItems: "center", paddingVertical: 24 } }, k.muted(query.trim() ? `Nothing found for \u201C${query.trim()}\u201D.` : "No help articles yet."), k.btn("Send us a message", "primary", () => setView("messages"))) : null,
3458
- ...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row), { padding: 8 })] : [] : [...groups].map(([name, items]) => (0, import_react2.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`, { fontSize: 12, fontWeight: "600", paddingHorizontal: 2 }), card(items.map(row), { padding: 8 })))
4234
+ arts.list === null ? k.muted(t("Loading\u2026")) : null,
4235
+ 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,
4236
+ ...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 })))
3459
4237
  );
3460
4238
  } else
3461
- content = (0, import_react2.createElement)(
4239
+ content = (0, import_react3.createElement)(
3462
4240
  RN3.ScrollView,
3463
4241
  { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 32 } },
3464
- (0, import_react2.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
4242
+ (0, import_react3.createElement)(ArticleView, { client, p, id: view.article, onAsk: startChat })
3465
4243
  );
3466
4244
  const current = typeof view === "string" ? view : "help";
3467
4245
  const tab = (v, name, icon, badge = 0) => {
3468
4246
  const on = current === v;
3469
- return (0, import_react2.createElement)(
4247
+ return (0, import_react3.createElement)(
3470
4248
  RN3.Pressable,
3471
4249
  {
3472
4250
  key: v,
3473
- onPress: () => setView(v),
4251
+ onPress: () => v === "messages" ? openMessages() : setView(v),
3474
4252
  accessibilityRole: "tab",
3475
4253
  accessibilityLabel: name,
3476
4254
  accessibilityState: { selected: on },
@@ -3479,68 +4257,69 @@ var MessengerSheet = ({
3479
4257
  },
3480
4258
  glyph(icon, on ? hu.fg : hu.muted, 17),
3481
4259
  on ? k.text(name, { fontSize: 13.5, fontWeight: "500", color: hu.fg }) : null,
3482
- badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 7, right: 12, width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : null
4260
+ badge ? (0, import_react3.createElement)(RN3.View, { style: { position: "absolute", top: 7, right: 12, width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : null
3483
4261
  );
3484
4262
  };
3485
4263
  const onAccent = view === "home" && !plain;
3486
4264
  const ground = onAccent ? p.accent : hu.bg;
3487
- const back = (to, label) => (0, import_react2.createElement)(
4265
+ const back = (to, label) => (0, import_react3.createElement)(
3488
4266
  RN3.Pressable,
3489
- { key: "back", onPress: () => setView(to), accessibilityRole: "button", accessibilityLabel: label, hitSlop: 10, style: { width: 32, height: 32, alignItems: "center", justifyContent: "center", transform: [{ rotate: "180deg" }] } },
4267
+ { key: "back", onPress: to, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 10, style: { width: 32, height: 32, alignItems: "center", justifyContent: "center", transform: [{ rotate: "180deg" }] } },
3490
4268
  glyph("next", hu.fg, 18)
3491
4269
  );
3492
- const head = (0, import_react2.createElement)(
4270
+ const head = (0, import_react3.createElement)(
3493
4271
  RN3.View,
3494
4272
  { key: "head", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingLeft: 12, paddingRight: 12, paddingTop: 12, paddingBottom: 6, backgroundColor: ground } },
3495
- typeof view !== "string" ? back("help", "All articles") : view === "messages" ? back("home", "Back to home") : view === "home" && (o.logo || title) ? o.logo ? (0, import_react2.createElement)(RN3.Image, { key: "logo", source: { uri: o.logo }, accessibilityIgnoresInvertColors: true, resizeMode: "contain", style: { height: 28, aspectRatio: 1, maxWidth: 132, borderRadius: 8, marginLeft: 4 } }) : teamMark(title, alpha(p.accent, 0.16), p.accent, 28) : null,
3496
- // In the chat: who answers, in the middle.
3497
- view === "messages" && title ? (0, import_react2.createElement)(
4273
+ 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,
4274
+ // In the chat: who answers, in the middle; over the list its name.
4275
+ 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)(
3498
4276
  RN3.View,
3499
4277
  { style: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8 } },
3500
4278
  teamMark(title, alpha(p.accent, 0.16), p.accent, 28),
3501
- (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { flexShrink: 1, color: hu.fg, fontSize: 15, fontWeight: "600" } }, title)
3502
- ) : (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
3503
- (0, import_react2.createElement)(RN3.Pressable, { onPress: close, accessibilityRole: "button", accessibilityLabel: "Close", hitSlop: 12, style: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" } }, glyph("cross", onAccent ? ink : p.muted, 18))
4279
+ (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { flexShrink: 1, color: hu.fg, fontSize: 15, fontWeight: "600" } }, title)
4280
+ ) : (0, import_react3.createElement)(RN3.View, { style: { flex: 1 } }),
4281
+ (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))
3504
4282
  );
3505
- const powered = brand ? (0, import_react2.createElement)(
4283
+ const powered = brand ? (0, import_react3.createElement)(
3506
4284
  RN3.View,
3507
4285
  { key: "brand", style: { paddingTop: 2, paddingBottom: 6, alignItems: "center", backgroundColor: hu.bg } },
3508
- (0, import_react2.createElement)(
4286
+ (0, import_react3.createElement)(
3509
4287
  RN3.Text,
3510
4288
  { onPress: () => void RN3.Linking.openURL(BRAND_URL).catch(() => void 0), accessibilityRole: "link", hitSlop: 8, style: { color: p.muted, fontSize: 11.5 } },
3511
- "Powered by ",
3512
- (0, import_react2.createElement)(RN3.Text, { style: { fontWeight: "600" } }, "MRRocket")
4289
+ t("Powered by {brand}").split("{brand}")[0],
4290
+ (0, import_react3.createElement)(RN3.Text, { style: { fontWeight: "600" } }, "MRRocket"),
4291
+ t("Powered by {brand}").split("{brand}")[1]
3513
4292
  )
3514
4293
  ) : null;
3515
- return (0, import_react2.createElement)(
4294
+ return (0, import_react3.createElement)(
3516
4295
  RN3.Modal,
3517
4296
  { visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: close },
3518
- (0, import_react2.createElement)(
4297
+ (0, import_react3.createElement)(
3519
4298
  RN3.KeyboardAvoidingView,
3520
4299
  { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: hu.bg } },
3521
4300
  head,
3522
4301
  content,
3523
- view === "messages" ? null : (0, import_react2.createElement)(
4302
+ view === "messages" && !chat.listing ? null : (0, import_react3.createElement)(
3524
4303
  RN3.View,
3525
4304
  { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", marginHorizontal: 12, marginTop: 6, marginBottom: 6, padding: 4, borderRadius: 22, backgroundColor: hu.surface } },
3526
- tab("home", "Home", "home"),
3527
- tab("messages", "Messages", "chat", chat.unseen.length),
3528
- showHelp ? tab("help", "Help", "help") : null
4305
+ tab("home", t("Home"), "home"),
4306
+ tab("messages", t("Messages"), "chat", chat.unseen.length),
4307
+ showHelp ? tab("help", t("Help"), "help") : null
3529
4308
  ),
3530
4309
  powered,
3531
- (0, import_react2.createElement)(RN3.View, { style: { height: 16 } })
4310
+ (0, import_react3.createElement)(RN3.View, { style: { height: 16 } })
3532
4311
  )
3533
4312
  );
3534
4313
  };
3535
4314
  var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3536
- const [look, setLook] = (0, import_react2.useState)(null);
3537
- (0, import_react2.useEffect)(() => {
4315
+ const [look, setLook] = (0, import_react3.useState)(null);
4316
+ (0, import_react3.useEffect)(() => {
3538
4317
  const ctrl = new AbortController();
3539
4318
  void client.ready.then(
3540
4319
  () => client.api()?.messengerLook(ctrl.signal).then(setLook).catch(() => void 0)
3541
4320
  );
3542
4321
  return () => ctrl.abort();
3543
- }, [client]);
4322
+ }, [client, wordsVersion()]);
3544
4323
  const defined = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== void 0));
3545
4324
  const options = look ? {
3546
4325
  title: look.title,
@@ -3562,27 +4341,31 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3562
4341
  const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
3563
4342
  const k = kit(p);
3564
4343
  const open = messengerOpen.useOpen();
3565
- const [view, setViewState] = (0, import_react2.useState)(msgView);
3566
- const [banner, setBanner] = (0, import_react2.useState)(null);
3567
- (0, import_react2.useEffect)(() => {
3568
- if (open) setViewState(msgView);
3569
- }, [open]);
4344
+ const [view, setViewState] = (0, import_react3.useState)(msgView);
4345
+ const [banner, setBanner] = (0, import_react3.useState)(null);
3570
4346
  const setView = (v) => {
3571
4347
  msgView = v;
3572
4348
  setViewState(v);
3573
4349
  };
3574
4350
  const chat = useChat(client, options, open, view);
4351
+ const { enter } = chat;
4352
+ (0, import_react3.useEffect)(() => {
4353
+ if (!open) return;
4354
+ setViewState(msgView);
4355
+ if (msgView === "messages") enter(msgChat);
4356
+ }, [open, enter]);
3575
4357
  const latest = chat.unseen.at(-1);
3576
- (0, import_react2.useEffect)(() => {
4358
+ (0, import_react3.useEffect)(() => {
3577
4359
  if (!latest || open || options.preview === false) return;
3578
4360
  setBanner(latest);
3579
- const t = setTimeout(() => setBanner(null), 6e3);
3580
- return () => clearTimeout(t);
4361
+ const t2 = setTimeout(() => setBanner(null), 6e3);
4362
+ return () => clearTimeout(t2);
3581
4363
  }, [latest, open, options.preview]);
3582
- (0, import_react2.useEffect)(() => {
4364
+ (0, import_react3.useEffect)(() => {
3583
4365
  if (open) setBanner(null);
3584
4366
  }, [open]);
3585
4367
  const unseen = chat.unseen.length;
4368
+ const news = latest?.chat ?? null;
3586
4369
  const ink = inkOn(p.accent);
3587
4370
  const label = options.label?.trim() ?? "";
3588
4371
  const r = CORNERS[options.corners ?? "round"];
@@ -3591,15 +4374,15 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3591
4374
  ...edge === "top" ? { top: 60 } : edge === "bottom" ? { bottom: 34 } : { top: 0, bottom: 0, justifyContent: "center" },
3592
4375
  ...side === "left" ? { left: 20 } : side === "right" ? { right: 20 } : { left: 0, right: 0, alignItems: "center" }
3593
4376
  };
3594
- const launcher = options.launcher ? (0, import_react2.createElement)(
4377
+ const launcher = options.launcher ? (0, import_react3.createElement)(
3595
4378
  RN3.View,
3596
4379
  { key: "launcher", pointerEvents: "box-none", style: { position: "absolute", ...place } },
3597
- (0, import_react2.createElement)(
4380
+ (0, import_react3.createElement)(
3598
4381
  RN3.Pressable,
3599
4382
  {
3600
- onPress: () => openAt(unseen ? "messages" : "home"),
4383
+ onPress: () => openAt(news ? "messages" : "home", news),
3601
4384
  accessibilityRole: "button",
3602
- accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
4385
+ accessibilityLabel: unseen === 1 ? t("Open chat, 1 new reply") : unseen ? tf("Open chat, {n} new replies", { n: unseen }) : t("Open chat"),
3603
4386
  style: {
3604
4387
  height: label ? 50 : 56,
3605
4388
  minWidth: label ? 0 : 56,
@@ -3619,14 +4402,14 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3619
4402
  },
3620
4403
  glyph("chat", ink, label ? 20 : 26),
3621
4404
  label ? k.text(label, { color: ink, fontWeight: "700" }) : null,
3622
- unseen ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: p.danger, alignItems: "center", justifyContent: "center", borderWidth: 2, borderColor: p.bg } }, k.text(String(Math.min(unseen, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
4405
+ unseen ? (0, import_react3.createElement)(RN3.View, { style: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: p.danger, alignItems: "center", justifyContent: "center", borderWidth: 2, borderColor: p.bg } }, k.text(String(Math.min(unseen, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
3623
4406
  )
3624
4407
  ) : null;
3625
- const toast = banner ? (0, import_react2.createElement)(
4408
+ const toast = banner ? (0, import_react3.createElement)(
3626
4409
  RN3.Pressable,
3627
4410
  {
3628
4411
  key: "banner",
3629
- onPress: () => openAt("messages"),
4412
+ onPress: () => openAt("messages", banner.chat),
3630
4413
  accessibilityRole: "button",
3631
4414
  accessibilityLiveRegion: "polite",
3632
4415
  style: {
@@ -3646,30 +4429,30 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3646
4429
  elevation: 6
3647
4430
  }
3648
4431
  },
3649
- k.muted(`${who(banner.from, options.agentName)}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
3650
- (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15, lineHeight: 20 } }, banner.body)
4432
+ k.muted(`${who(banner.from, options.agentName)}${unseen > 1 ? ` \xB7 ${tf("{n} new", { n: unseen })}` : ""}`, { fontWeight: "600", fontSize: 12 }),
4433
+ (0, import_react3.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15, lineHeight: 20 } }, banner.body)
3651
4434
  ) : null;
3652
4435
  if (!launcher && !toast && !open) return null;
3653
- return (0, import_react2.createElement)(
4436
+ return (0, import_react3.createElement)(
3654
4437
  RN3.View,
3655
4438
  { pointerEvents: "box-none", style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 } },
3656
4439
  launcher,
3657
4440
  toast,
3658
- open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView, brand: look?.branding !== false }) : null
4441
+ open ? (0, import_react3.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView, brand: look?.branding !== false }) : null
3659
4442
  );
3660
4443
  };
3661
4444
 
3662
4445
  // src/native/index.tsx
3663
4446
  var RN4 = require("react-native");
3664
4447
  var MRRocketMask = ({ children, style }) => {
3665
- const ref = (0, import_react3.useRef)(null);
3666
- (0, import_react3.useEffect)(() => {
4448
+ const ref = (0, import_react4.useRef)(null);
4449
+ (0, import_react4.useEffect)(() => {
3667
4450
  maskedViews.add(ref);
3668
4451
  return () => {
3669
4452
  maskedViews.delete(ref);
3670
4453
  };
3671
4454
  }, []);
3672
- return (0, import_react3.createElement)(RN4.View, { ref, collapsable: false, style }, children);
4455
+ return (0, import_react4.createElement)(RN4.View, { ref, collapsable: false, style }, children);
3673
4456
  };
3674
4457
  var shared = null;
3675
4458
  var sharedKey = "";
@@ -3686,16 +4469,20 @@ var createMRRocket = (options) => {
3686
4469
  return shared;
3687
4470
  };
3688
4471
  var getMRRocket = () => shared;
3689
- var Ctx = (0, import_react3.createContext)(null);
4472
+ var Ctx = (0, import_react4.createContext)(null);
3690
4473
  var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, surveys = true, messenger = {}, feedback = {}, heatmaps = true, ...options }) => {
3691
- const [client] = (0, import_react3.useState)(() => given ?? createMRRocket({ ...options, consent }));
3692
- (0, import_react3.useEffect)(() => {
4474
+ const [client] = (0, import_react4.useState)(() => given ?? createMRRocket({ ...options, consent }));
4475
+ (0, import_react4.useSyncExternalStore)(onWords, wordsVersion, wordsVersion);
4476
+ (0, import_react4.useEffect)(() => {
4477
+ if (options.locale) setLocale(options.locale);
4478
+ }, [options.locale]);
4479
+ (0, import_react4.useEffect)(() => {
3693
4480
  if (consent) client.setConsent(consent);
3694
4481
  }, [client, consent]);
3695
- (0, import_react3.useEffect)(() => {
4482
+ (0, import_react4.useEffect)(() => {
3696
4483
  if (options.replay !== void 0) client.setReplay(options.replay);
3697
4484
  }, [client, options.replay]);
3698
- const start = (0, import_react3.useRef)(null);
4485
+ const start = (0, import_react4.useRef)(null);
3699
4486
  const touch = (e, end) => {
3700
4487
  const n = e.nativeEvent;
3701
4488
  const x = n.pageX ?? n.changedTouches?.[0]?.pageX;
@@ -3704,22 +4491,22 @@ var MRRocketProvider = ({ client: given, children, consent, flows = true, appear
3704
4491
  if (!end) start.current = { x, y, t: Date.now() };
3705
4492
  else if (start.current && Math.hypot(x - start.current.x, y - start.current.y) < 10 && Date.now() - start.current.t < 500) client.tap(x, y);
3706
4493
  };
3707
- return (0, import_react3.createElement)(
4494
+ return (0, import_react4.createElement)(
3708
4495
  Ctx.Provider,
3709
4496
  { value: client },
3710
- heatmaps ? (0, import_react3.createElement)(RN4.View, { style: { flex: 1 }, onTouchStartCapture: (e) => touch(e, false), onTouchEndCapture: (e) => touch(e, true) }, children) : children,
3711
- flows ? (0, import_react3.createElement)(MRRocketFlows, { flows: client.flows, paywall: client.paywall, appearance }) : null,
3712
- surveys ? (0, import_react3.createElement)(MRRocketSurveys, { client, appearance }) : null,
3713
- feedback ? (0, import_react3.createElement)(MRRocketFeedback, { client, appearance, options: feedback }) : null,
3714
- messenger ? (0, import_react3.createElement)(MRRocketMessenger, { client, appearance, options: messenger }) : null
4497
+ heatmaps ? (0, import_react4.createElement)(RN4.View, { style: { flex: 1 }, onTouchStartCapture: (e) => touch(e, false), onTouchEndCapture: (e) => touch(e, true) }, children) : children,
4498
+ flows ? (0, import_react4.createElement)(MRRocketFlows, { flows: client.flows, paywall: client.paywall, appearance }) : null,
4499
+ surveys ? (0, import_react4.createElement)(MRRocketSurveys, { client, appearance }) : null,
4500
+ feedback ? (0, import_react4.createElement)(MRRocketFeedback, { client, appearance, options: feedback }) : null,
4501
+ messenger ? (0, import_react4.createElement)(MRRocketMessenger, { client, appearance, options: messenger }) : null
3715
4502
  );
3716
4503
  };
3717
4504
  var useClient = () => {
3718
- const c = (0, import_react3.useContext)(Ctx);
4505
+ const c = (0, import_react4.useContext)(Ctx);
3719
4506
  if (!c) throw new Error("MRRocket: wrap your app in <MRRocketProvider apiKey=\u2026>.");
3720
4507
  return c;
3721
4508
  };
3722
- var useVersion = (client) => (0, import_react3.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
4509
+ var useVersion = (client) => (0, import_react4.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
3723
4510
  var useMRRocket = () => {
3724
4511
  const client = useClient();
3725
4512
  useVersion(client);
@@ -3753,9 +4540,9 @@ var useReview = () => {
3753
4540
  const client = useClient();
3754
4541
  return { request: client.requestReview };
3755
4542
  };
3756
- var useOnboarding = () => {
4543
+ var useOnboarding = (id) => {
3757
4544
  const client = useClient();
3758
- return { start: client.startOnboarding };
4545
+ return useOnboardingItem(client.flows, id, client.startOnboarding);
3759
4546
  };
3760
4547
  var useFlag = (key) => {
3761
4548
  const client = useClient();
@@ -3772,7 +4559,7 @@ var useVariant = (key) => {
3772
4559
  useVersion(client);
3773
4560
  const config = client.getConfig();
3774
4561
  const variant = client.peekVariant(key);
3775
- (0, import_react3.useEffect)(() => {
4562
+ (0, import_react4.useEffect)(() => {
3776
4563
  if (config) client.expose(key, variant);
3777
4564
  }, [client, config, key, variant]);
3778
4565
  return variant;
@@ -3783,7 +4570,7 @@ var useText = (id, fallback) => {
3783
4570
  const config = client.getConfig();
3784
4571
  const text2 = client.peekText(id);
3785
4572
  const person = client.getDistinctId();
3786
- (0, import_react3.useEffect)(() => {
4573
+ (0, import_react4.useEffect)(() => {
3787
4574
  if (config) client.exposeText(id);
3788
4575
  }, [client, config, id, person]);
3789
4576
  return text2 ?? fallback;
@@ -3808,13 +4595,13 @@ var useExpoRouterTracking = () => {
3808
4595
  console.warn("[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.");
3809
4596
  }
3810
4597
  const pathname = (router?.usePathname ?? noPathname)();
3811
- (0, import_react3.useEffect)(() => {
4598
+ (0, import_react4.useEffect)(() => {
3812
4599
  if (pathname) client.screen(pathname);
3813
4600
  }, [client, pathname]);
3814
4601
  };
3815
4602
  var useNavigationTracking = () => {
3816
4603
  const client = useClient();
3817
- return (0, import_react3.useCallback)(
4604
+ return (0, import_react4.useCallback)(
3818
4605
  (state) => {
3819
4606
  const name = activeRouteName(state);
3820
4607
  if (name) client.screen(name);