@mrrocket/sdk 0.4.0 → 0.9.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 (72) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +137 -8
  3. package/dist/api-client.d.ts +213 -0
  4. package/dist/api-client.js +67 -0
  5. package/dist/api-client.js.map +1 -0
  6. package/dist/api.d.ts +9 -109
  7. package/dist/api.js +7 -62
  8. package/dist/api.js.map +1 -1
  9. package/dist/browser/mrrocket-flows.js +64 -0
  10. package/dist/browser/mrrocket-messenger.js +313 -0
  11. package/dist/browser/mrrocket-push.js +1 -0
  12. package/dist/browser/mrrocket-replay.js +3 -3
  13. package/dist/browser/mrrocket-sw.js +1 -0
  14. package/dist/browser/mrrocket.js +2 -1
  15. package/dist/browser.d.ts +3 -2
  16. package/dist/browser.js +6 -1
  17. package/dist/browser.js.map +1 -1
  18. package/dist/capture.d.ts +1 -13
  19. package/dist/capture.js +1 -22
  20. package/dist/capture.js.map +1 -1
  21. package/dist/client.d.ts +36 -1
  22. package/dist/client.js +161 -6
  23. package/dist/client.js.map +1 -1
  24. package/dist/context.d.ts +10 -0
  25. package/dist/context.js.map +1 -1
  26. package/dist/core.d.ts +64 -3
  27. package/dist/core.js +54 -2
  28. package/dist/core.js.map +1 -1
  29. package/dist/feedback.d.ts +19 -4
  30. package/dist/feedback.js +655 -190
  31. package/dist/feedback.js.map +1 -1
  32. package/dist/flows-ui.d.ts +23 -0
  33. package/dist/flows-ui.js +384 -0
  34. package/dist/flows-ui.js.map +1 -0
  35. package/dist/flows-web.d.ts +19 -0
  36. package/dist/flows-web.js +42 -0
  37. package/dist/flows-web.js.map +1 -0
  38. package/dist/flows.d.ts +222 -0
  39. package/dist/flows.js +409 -0
  40. package/dist/flows.js.map +1 -0
  41. package/dist/index.d.ts +5 -2
  42. package/dist/index.js +13 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/messenger-ui.d.ts +47 -0
  45. package/dist/messenger-ui.js +583 -0
  46. package/dist/messenger-ui.js.map +1 -0
  47. package/dist/messenger.d.ts +37 -0
  48. package/dist/messenger.js +69 -0
  49. package/dist/messenger.js.map +1 -0
  50. package/dist/proxy.d.ts +34 -1
  51. package/dist/proxy.js +14 -1
  52. package/dist/proxy.js.map +1 -1
  53. package/dist/push-web.d.ts +43 -0
  54. package/dist/push-web.js +81 -0
  55. package/dist/push-web.js.map +1 -0
  56. package/dist/rage.d.ts +14 -0
  57. package/dist/rage.js +27 -0
  58. package/dist/rage.js.map +1 -0
  59. package/dist/react-native/index.d.cts +640 -3
  60. package/dist/react-native/index.d.ts +640 -3
  61. package/dist/react-native/index.js +2015 -34
  62. package/dist/react-native/index.js.map +1 -1
  63. package/dist/styles-css.d.ts +6 -0
  64. package/dist/styles-css.js +327 -0
  65. package/dist/styles-css.js.map +1 -0
  66. package/dist/styles.d.ts +1 -2
  67. package/dist/styles.js +3 -134
  68. package/dist/styles.js.map +1 -1
  69. package/dist/ui.d.ts +3 -0
  70. package/dist/ui.js +12 -0
  71. package/dist/ui.js.map +1 -1
  72. package/package.json +20 -4
@@ -3,6 +3,7 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
7
  var __export = (target, all) => {
7
8
  for (var name in all)
8
9
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -16,27 +17,39 @@ var __copyProps = (to, from, except, desc) => {
16
17
  return to;
17
18
  };
18
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
19
21
 
20
22
  // src/native/index.tsx
21
23
  var native_exports = {};
22
24
  __export(native_exports, {
25
+ MRRocketFeedback: () => MRRocketFeedback,
26
+ MRRocketFlows: () => MRRocketFlows,
27
+ MRRocketMask: () => MRRocketMask,
28
+ MRRocketMessenger: () => MRRocketMessenger,
23
29
  MRRocketProvider: () => MRRocketProvider,
30
+ MRRocketSurveys: () => MRRocketSurveys,
24
31
  SDK_VERSION: () => SDK_VERSION,
25
32
  activeRouteName: () => activeRouteName,
26
33
  createMRRocket: () => createMRRocket,
27
34
  getMRRocket: () => getMRRocket,
35
+ useCancelFlow: () => useCancelFlow,
28
36
  useExpoRouterTracking: () => useExpoRouterTracking,
37
+ useFeedback: () => useFeedback,
29
38
  useFlag: () => useFlag,
30
39
  useFlagVariant: () => useFlagVariant,
31
40
  useMRRocket: () => useMRRocket,
41
+ useMessenger: () => useMessenger,
32
42
  useNavigationTracking: () => useNavigationTracking,
43
+ useOnboarding: () => useOnboarding,
44
+ usePush: () => usePush,
45
+ useText: () => useText,
33
46
  useVariant: () => useVariant
34
47
  });
35
48
  module.exports = __toCommonJS(native_exports);
36
- var import_react = require("react");
49
+ var import_react3 = require("react");
37
50
 
38
51
  // src/core.ts
39
- var SDK_VERSION = "0.4.0";
52
+ var SDK_VERSION = "0.9.0";
40
53
  var propOf = (person, name) => {
41
54
  const want = name.trim().toLowerCase();
42
55
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -109,25 +122,25 @@ var parseQuery = (url) => {
109
122
  return out;
110
123
  };
111
124
  var hashUnit = (input) => {
112
- let h = 2166136261;
125
+ let h3 = 2166136261;
113
126
  for (let i = 0; i < input.length; i++) {
114
- h ^= input.charCodeAt(i);
115
- h = Math.imul(h, 16777619);
127
+ h3 ^= input.charCodeAt(i);
128
+ h3 = Math.imul(h3, 16777619);
116
129
  }
117
- return (h >>> 0) / 4294967296;
130
+ return (h3 >>> 0) / 4294967296;
118
131
  };
119
132
  var flagUnit = (input) => {
120
- let h = 2166136261;
133
+ let h3 = 2166136261;
121
134
  for (let i = 0; i < input.length; i++) {
122
- h ^= input.charCodeAt(i);
123
- h = Math.imul(h, 16777619);
135
+ h3 ^= input.charCodeAt(i);
136
+ h3 = Math.imul(h3, 16777619);
124
137
  }
125
- h ^= h >>> 16;
126
- h = Math.imul(h, 2246822507);
127
- h ^= h >>> 13;
128
- h = Math.imul(h, 3266489909);
129
- h ^= h >>> 16;
130
- return (h >>> 0) / 4294967296;
138
+ h3 ^= h3 >>> 16;
139
+ h3 = Math.imul(h3, 2246822507);
140
+ h3 ^= h3 >>> 13;
141
+ h3 = Math.imul(h3, 3266489909);
142
+ h3 ^= h3 >>> 16;
143
+ return (h3 >>> 0) / 4294967296;
131
144
  };
132
145
  var assign = (experiment, distinctId) => {
133
146
  const control = experiment.variants[0]?.id ?? "control";
@@ -141,13 +154,79 @@ var assign = (experiment, distinctId) => {
141
154
  }
142
155
  return experiment.variants[experiment.variants.length - 1]?.id ?? control;
143
156
  };
157
+ var experimentsOfText = (config, id) => (config?.experiments ?? []).filter((e) => e.variants.some((v) => v.changes && id in v.changes)).map((e) => e.key);
144
158
  var resolveVariant = (key, config, distinctId, preview) => {
145
159
  if (preview[key]) return preview[key];
146
160
  const experiment = config?.experiments.find((e) => e.key === key);
147
161
  return experiment ? assign(experiment, distinctId) : "control";
148
162
  };
163
+ var textChanges = (config, distinctId, preview) => {
164
+ const out = { ...config?.texts ?? {} };
165
+ for (const experiment of config?.experiments ?? []) {
166
+ const id = resolveVariant(experiment.key, config, distinctId, preview);
167
+ if (id === (experiment.variants[0]?.id ?? "control")) continue;
168
+ Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});
169
+ }
170
+ return out;
171
+ };
149
172
  var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
150
173
 
174
+ // src/api-client.ts
175
+ var ApiError = class extends Error {
176
+ constructor(message, status) {
177
+ super(message);
178
+ __publicField(this, "status");
179
+ this.status = status;
180
+ }
181
+ };
182
+ var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
183
+ var call = async (url, init, signal) => {
184
+ let res;
185
+ try {
186
+ res = await fetch(url, {
187
+ ...init,
188
+ signal,
189
+ headers: init?.body ? { "content-type": "application/json" } : void 0
190
+ });
191
+ } catch (e) {
192
+ if (e.name === "AbortError") throw e;
193
+ throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
194
+ }
195
+ if (!res.ok) {
196
+ let detail = "";
197
+ try {
198
+ detail = (await res.json()).error ?? "";
199
+ } catch {
200
+ }
201
+ throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
202
+ }
203
+ return await res.json();
204
+ };
205
+ var createApi = (c) => {
206
+ const base = `${c.host}/api/sdk`;
207
+ const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
208
+ const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
209
+ return {
210
+ list: (o, signal) => call(`${base}/feedback?${qs({ key: c.apiKey, distinctId: c.distinctId, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
211
+ post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
212
+ roadmap: (signal) => call(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
213
+ changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
214
+ submit: (p) => post("/feedback", p),
215
+ vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
216
+ comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
217
+ comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
218
+ survey: (url, signal) => call(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, void 0, signal),
219
+ seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
220
+ respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
221
+ chat: (signal) => call(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
222
+ sendChat: (m) => post("/messages", m),
223
+ articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
224
+ article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
225
+ messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
226
+ rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
227
+ };
228
+ };
229
+
151
230
  // src/batch.ts
152
231
  var MAX_BATCH = 100;
153
232
  var MAX_BYTES = 6e4;
@@ -203,6 +282,568 @@ var describeError = (error) => {
203
282
  }
204
283
  };
205
284
 
285
+ // src/flows.ts
286
+ var CANCEL_EVENT = "$cancel_flow";
287
+ var ONBOARDING_EVENT = "$onboarding";
288
+ var INAPP_EVENT = "$inapp";
289
+ var PLAIN_CANCEL = {
290
+ version: 0,
291
+ question: "",
292
+ reasons: [],
293
+ offers: {},
294
+ confirm: { title: "Cancel your subscription?", body: "You keep access until the end of the billing period.", button: "Cancel subscription" },
295
+ offersFor: "business"
296
+ };
297
+ var FAILED = "Something went wrong. Please try again.";
298
+ var PROGRESS_KEY = "mrrocket:onboarding";
299
+ var eventSlug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
300
+ var SIGNUP = /* @__PURE__ */ new Set(["signed_up", "signup", "sign_up", "registered", "account_created", "user_signed_up"]);
301
+ var matchesTrigger = (trigger, event) => {
302
+ const t = eventSlug(trigger);
303
+ const e = eventSlug(event);
304
+ if (!t || !e) return false;
305
+ return t === e || SIGNUP.has(t) && SIGNUP.has(e);
306
+ };
307
+ 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());
308
+ var runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
309
+ var discounted = (amount, percent) => Math.round(amount * (1 - percent / 100) * 100) / 100;
310
+ var money = (amount, currency) => {
311
+ try {
312
+ return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
313
+ } catch {
314
+ return `${amount.toFixed(2)} ${currency}`;
315
+ }
316
+ };
317
+ var offerButton = (o) => o.kind === "contact" ? "Send to the team" : o.kind === "pause" ? "Pause instead" : o.kind === "downgrade" ? "Switch plan" : "Accept offer";
318
+ var safeUrl = (url) => {
319
+ const u = (url ?? "").trim();
320
+ return /^https?:\/\//i.test(u) || /^\/(?!\/)/.test(u) ? u : void 0;
321
+ };
322
+ var createFlows = (env) => {
323
+ let config = null;
324
+ let cancel = null;
325
+ let active = null;
326
+ let messages = [];
327
+ let known = false;
328
+ let loading = null;
329
+ let loadedFor;
330
+ let progressFor = null;
331
+ const progress = /* @__PURE__ */ new Map();
332
+ const handled = /* @__PURE__ */ new Set();
333
+ const shown = /* @__PURE__ */ new Set();
334
+ const listeners = /* @__PURE__ */ new Set();
335
+ const auto = env.auto !== false;
336
+ const flows = () => config?.onboarding ?? [];
337
+ const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
338
+ const prog = (id) => {
339
+ let p = progress.get(id);
340
+ if (!p) progress.set(id, p = { done: /* @__PURE__ */ new Set(), dismissed: false });
341
+ return p;
342
+ };
343
+ const finished = (f) => {
344
+ const p = progress.get(f.id);
345
+ return !!p && (p.dismissed || f.steps.every((s) => p.done.has(s.id)));
346
+ };
347
+ const keyOf = (pid) => `${PROGRESS_KEY}:${pid ?? "visitor"}`;
348
+ const save = () => {
349
+ const store = env.storage?.();
350
+ if (!store) return;
351
+ const out = {};
352
+ for (const [id, p] of progress) out[id] = { done: [...p.done], ...p.dismissed ? { dismissed: true } : {} };
353
+ try {
354
+ store.set(keyOf(env.personId()), JSON.stringify(out));
355
+ } catch {
356
+ }
357
+ };
358
+ const merge = (state) => {
359
+ for (const [id, s] of Object.entries(state ?? {})) {
360
+ const p = prog(id);
361
+ for (const d of s.done ?? []) p.done.add(d);
362
+ if (s.dismissed) p.dismissed = true;
363
+ }
364
+ };
365
+ const onboardingView = () => {
366
+ const flow = flowById(active);
367
+ if (!flow) return null;
368
+ const p = prog(flow.id);
369
+ const at = flow.steps.findIndex((s) => !p.done.has(s.id));
370
+ if (at < 0 || p.dismissed) return null;
371
+ const step = flow.steps[at];
372
+ if (step.kind === "checklist") {
373
+ let from = at;
374
+ while (from > 0 && flow.steps[from - 1].kind === "checklist") from--;
375
+ let to = at;
376
+ while (to + 1 < flow.steps.length && flow.steps[to + 1].kind === "checklist") to++;
377
+ return { type: "checklist", flow, items: flow.steps.slice(from, to + 1).map((s) => ({ step: s, done: p.done.has(s.id) })) };
378
+ }
379
+ return { type: "step", flow, step, index: at, total: flow.steps.length, last: at === flow.steps.length - 1 };
380
+ };
381
+ const view = () => {
382
+ if (cancel) {
383
+ const offer = cancel.reason ? cancel.flow.offers[cancel.reason] : void 0;
384
+ return {
385
+ type: "cancel",
386
+ screen: cancel.screen,
387
+ flow: cancel.flow,
388
+ reason: cancel.reason,
389
+ offer: cancel.screen === "offer" ? offer : void 0,
390
+ price: cancel.options.price,
391
+ busy: cancel.busy,
392
+ error: cancel.error,
393
+ step: cancel.plain ? 1 : cancel.screen === "survey" ? 1 : cancel.screen === "offer" ? 2 : 3,
394
+ steps: cancel.plain ? 1 : 3
395
+ };
396
+ }
397
+ const ob = onboardingView();
398
+ if (ob) return ob;
399
+ if (active) active = null;
400
+ const m = messages.find((x) => !handled.has(x.id));
401
+ return m ? { type: "message", message: m } : null;
402
+ };
403
+ const emit = () => {
404
+ const v = view();
405
+ if (v?.type === "step" && !shown.has(`s:${v.step.id}`)) {
406
+ shown.add(`s:${v.step.id}`);
407
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "shown" });
408
+ } else if (v?.type === "checklist") {
409
+ for (const i of v.items)
410
+ if (!i.done && !shown.has(`s:${i.step.id}`)) {
411
+ shown.add(`s:${i.step.id}`);
412
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: i.step.id, kind: "checklist", action: "shown" });
413
+ }
414
+ } else if (v?.type === "message" && !shown.has(`m:${v.message.id}`)) {
415
+ shown.add(`m:${v.message.id}`);
416
+ env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "shown" });
417
+ }
418
+ for (const l of [...listeners]) {
419
+ try {
420
+ l();
421
+ } catch {
422
+ }
423
+ }
424
+ };
425
+ const start = (flow) => {
426
+ if (active === flow.id) return true;
427
+ if (active && flowById(active) && !finished(flowById(active))) return false;
428
+ active = flow.id;
429
+ env.track(ONBOARDING_EVENT, { flow: flow.id, action: "started" });
430
+ emit();
431
+ return true;
432
+ };
433
+ const autoStart = () => {
434
+ if (!auto || active || !known) return;
435
+ if (!env.personId() && !env.storage?.()) return;
436
+ const next = flows().find((f) => startsOnLoad(f.trigger) && f.steps.length && !finished(f));
437
+ if (next) start(next);
438
+ };
439
+ const refresh = () => {
440
+ const pid = env.personId();
441
+ if (progressFor !== null && progressFor !== pid) {
442
+ progress.clear();
443
+ handled.clear();
444
+ active = null;
445
+ loadedFor = void 0;
446
+ }
447
+ progressFor = pid;
448
+ const run = (async () => {
449
+ const store = env.storage?.();
450
+ if (store) {
451
+ try {
452
+ const raw = await store.get(keyOf(pid));
453
+ if (raw) merge(JSON.parse(raw));
454
+ } catch {
455
+ }
456
+ }
457
+ const wants = flows().length > 0 || !!config?.messages;
458
+ if (pid && wants && loadedFor !== pid) {
459
+ loadedFor = pid;
460
+ const state = await env.fetchInApp(pid).catch(() => null);
461
+ if (state && env.personId() === pid) {
462
+ merge(state.onboarding);
463
+ messages = (state.messages ?? []).filter((m) => m && m.id && m.title);
464
+ }
465
+ }
466
+ if (!pid) messages = [];
467
+ known = true;
468
+ autoStart();
469
+ emit();
470
+ })();
471
+ loading = run.finally(() => {
472
+ if (loading === run) loading = null;
473
+ });
474
+ return loading;
475
+ };
476
+ const result = (c, outcome, comment) => {
477
+ const offer = c.reason ? c.flow.offers[c.reason] : void 0;
478
+ return {
479
+ outcome,
480
+ run: c.run,
481
+ ...c.reason ? { reason: c.reason, reasonLabel: c.flow.reasons.find((r) => r.id === c.reason)?.label } : {},
482
+ ...outcome === "saved" && offer ? { offer } : {},
483
+ ...comment?.trim() ? { comment: comment.trim().slice(0, 500) } : {}
484
+ };
485
+ };
486
+ const finish = (c, r) => {
487
+ env.track(CANCEL_EVENT, {
488
+ action: "finished",
489
+ run: c.run,
490
+ version: c.flow.version,
491
+ outcome: r.outcome,
492
+ customer: c.options.customer ?? "consumer",
493
+ ...r.reason ? { reason: r.reason } : {},
494
+ ...r.offer ? { offer: r.offer.kind } : {},
495
+ ...r.comment ? { comment: r.comment } : {}
496
+ });
497
+ if (cancel === c) cancel = null;
498
+ c.resolve(r);
499
+ emit();
500
+ };
501
+ const settle = (c, outcome, comment) => {
502
+ if (c.busy) return;
503
+ const r = result(c, outcome, comment);
504
+ const fn = outcome === "saved" ? c.options.onSave : c.options.onCancel;
505
+ c.busy = true;
506
+ c.error = void 0;
507
+ emit();
508
+ Promise.resolve().then(() => fn?.(r)).then(
509
+ () => finish(c, r),
510
+ () => {
511
+ c.busy = false;
512
+ c.error = FAILED;
513
+ emit();
514
+ }
515
+ );
516
+ };
517
+ const openCancelFlow = (options = {}) => {
518
+ if (cancel) return cancel.promise;
519
+ const flow = config?.cancelFlow ?? PLAIN_CANCEL;
520
+ const plain = options.customer !== "business" || flow.reasons.length < 2;
521
+ let resolve = () => {
522
+ };
523
+ const promise = new Promise((r) => resolve = r);
524
+ const c = { flow, options, plain, screen: plain ? "confirm" : "survey", run: runId(), busy: false, resolve, promise };
525
+ cancel = c;
526
+ env.track(CANCEL_EVENT, { action: "shown", run: c.run, version: flow.version, customer: options.customer ?? "consumer", plain });
527
+ emit();
528
+ return promise;
529
+ };
530
+ const controller = {
531
+ setConfig: (c) => {
532
+ config = c;
533
+ if (active && !flowById(active)) active = null;
534
+ emit();
535
+ },
536
+ refresh,
537
+ onEvent: (name) => {
538
+ if (!name || name.startsWith("$")) return;
539
+ const flow = flowById(active);
540
+ let ticked = false;
541
+ if (flow) {
542
+ const p = prog(flow.id);
543
+ for (const s of flow.steps)
544
+ if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
545
+ p.done.add(s.id);
546
+ ticked = true;
547
+ env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: "completed" });
548
+ }
549
+ }
550
+ if (ticked) {
551
+ save();
552
+ emit();
553
+ }
554
+ if (!auto || !env.personId() && !env.storage?.()) return;
555
+ const next = flows().find((f) => f.steps.length && matchesTrigger(f.trigger, name) && !finished(f));
556
+ if (next) start(next);
557
+ },
558
+ openCancelFlow,
559
+ startOnboarding: (id) => {
560
+ const f = id ? flowById(id) : flows().find((x) => x.steps.length && !finished(x));
561
+ if (!f || !f.steps.length) return false;
562
+ const p = prog(f.id);
563
+ p.dismissed = false;
564
+ return start(f);
565
+ },
566
+ view,
567
+ subscribe: (l) => {
568
+ listeners.add(l);
569
+ return () => listeners.delete(l);
570
+ },
571
+ cancel: {
572
+ pick: (reason) => {
573
+ if (!cancel || cancel.busy || !cancel.flow.reasons.some((r) => r.id === reason)) return;
574
+ cancel.reason = reason;
575
+ cancel.error = void 0;
576
+ emit();
577
+ },
578
+ next: () => {
579
+ const c = cancel;
580
+ if (!c || c.screen !== "survey" || !c.reason) return;
581
+ env.track(CANCEL_EVENT, { action: "answered", run: c.run, version: c.flow.version, reason: c.reason });
582
+ c.screen = c.flow.offers[c.reason] ? "offer" : "confirm";
583
+ emit();
584
+ },
585
+ accept: (comment) => {
586
+ const c = cancel;
587
+ if (!c || c.screen !== "offer" || !c.reason) return;
588
+ const offer = c.flow.offers[c.reason];
589
+ if (!c.busy)
590
+ env.track(CANCEL_EVENT, {
591
+ action: "offer",
592
+ run: c.run,
593
+ version: c.flow.version,
594
+ reason: c.reason,
595
+ offer: offer?.kind,
596
+ outcome: "accepted"
597
+ });
598
+ settle(c, "saved", comment);
599
+ },
600
+ decline: () => {
601
+ const c = cancel;
602
+ if (!c || c.screen !== "offer" || c.busy) return;
603
+ env.track(CANCEL_EVENT, {
604
+ action: "offer",
605
+ run: c.run,
606
+ version: c.flow.version,
607
+ reason: c.reason,
608
+ offer: c.reason ? c.flow.offers[c.reason]?.kind : void 0,
609
+ outcome: "declined"
610
+ });
611
+ c.screen = "confirm";
612
+ c.error = void 0;
613
+ emit();
614
+ },
615
+ confirm: (comment) => {
616
+ const c = cancel;
617
+ if (!c || c.screen !== "confirm") return;
618
+ settle(c, "cancelled", comment);
619
+ },
620
+ close: () => {
621
+ const c = cancel;
622
+ if (!c || c.busy) return;
623
+ const r = result(c, "kept");
624
+ c.options.onClose?.(r);
625
+ finish(c, r);
626
+ }
627
+ },
628
+ onboarding: {
629
+ next: () => {
630
+ const v = onboardingView();
631
+ if (v?.type !== "step") return;
632
+ prog(v.flow.id).done.add(v.step.id);
633
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
634
+ save();
635
+ emit();
636
+ },
637
+ check: (stepId) => {
638
+ const v = onboardingView();
639
+ if (v?.type !== "checklist") return;
640
+ const item = v.items.find((i) => i.step.id === stepId);
641
+ if (!item || item.done) return;
642
+ prog(v.flow.id).done.add(stepId);
643
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
644
+ save();
645
+ emit();
646
+ },
647
+ dismiss: () => {
648
+ const v = onboardingView();
649
+ if (!v || v.type !== "step" && v.type !== "checklist") return;
650
+ prog(v.flow.id).dismissed = true;
651
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, ...v.type === "step" ? { step: v.step.id } : {}, action: "dismissed" });
652
+ active = null;
653
+ save();
654
+ emit();
655
+ }
656
+ },
657
+ message: {
658
+ click: () => {
659
+ const v = view();
660
+ if (v?.type !== "message") return;
661
+ handled.add(v.message.id);
662
+ env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "clicked" });
663
+ const url = safeUrl(v.message.url);
664
+ emit();
665
+ if (url) env.openUrl?.(url);
666
+ },
667
+ dismiss: () => {
668
+ const v = view();
669
+ if (v?.type !== "message") return;
670
+ handled.add(v.message.id);
671
+ env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "dismissed" });
672
+ emit();
673
+ }
674
+ }
675
+ };
676
+ return controller;
677
+ };
678
+
679
+ // src/rage.ts
680
+ var RageDetector = class {
681
+ constructor(windowMs = 1e3, radius = 30, count = 3) {
682
+ __publicField(this, "windowMs", windowMs);
683
+ __publicField(this, "radius", radius);
684
+ __publicField(this, "count", count);
685
+ __publicField(this, "clicks", []);
686
+ __publicField(this, "fired", false);
687
+ }
688
+ push(x, y, t) {
689
+ const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
690
+ if (near.length !== this.clicks.length) this.fired = false;
691
+ this.clicks = [...near, { x, y, t }];
692
+ if (this.clicks.length >= this.count && !this.fired) {
693
+ this.fired = true;
694
+ return true;
695
+ }
696
+ return false;
697
+ }
698
+ };
699
+
700
+ // src/native/recorder.ts
701
+ var FRAME_MS = 1e3;
702
+ var WIDTH = 360;
703
+ var BUFFER_MS = 6e4;
704
+ var CHUNK_FRAMES = 12;
705
+ var CHUNK_CHARS = 7e5;
706
+ var MAX_MS = 30 * 6e4;
707
+ var shareOf = (s) => {
708
+ let h3 = 2166136261;
709
+ for (let i = 0; i < s.length; i++) h3 = Math.imul(h3 ^ s.charCodeAt(i), 16777619);
710
+ return (h3 >>> 0) / 4294967296;
711
+ };
712
+ var createRecorder = (deps, st) => {
713
+ let timer = null;
714
+ let busy = false;
715
+ let last = "";
716
+ let rec = null;
717
+ let frames = [];
718
+ let marks = [];
719
+ const ensure = () => {
720
+ const session = st.session();
721
+ const cfg = st.config();
722
+ if (!rec || rec.session !== session) {
723
+ frames = [];
724
+ marks = [];
725
+ last = "";
726
+ const sampled = !!cfg && shareOf(session) < (cfg.sampleRate ?? 0.1);
727
+ rec = { session, start: deps.now(), seq: 0, uploading: sampled, trigger: "sample", stopped: false };
728
+ }
729
+ return rec;
730
+ };
731
+ const send = async (r) => {
732
+ if (!frames.length || r.stopped) return;
733
+ const batch = frames.splice(0, frames.length);
734
+ const ms = marks.splice(0, marks.length);
735
+ const size = deps.screen();
736
+ const { distinctId, identified } = st.who();
737
+ const body = JSON.stringify({
738
+ key: st.apiKey,
739
+ consent: { replay: true },
740
+ sessionId: r.session,
741
+ start: r.start,
742
+ seq: r.seq++,
743
+ enc: "frames",
744
+ data: JSON.stringify(batch),
745
+ meta: { first: batch[0].t, last: batch[batch.length - 1].t, vw: size ? Math.round(size.w) : null, vh: size ? Math.round(size.h) : null, marks: ms },
746
+ distinctId,
747
+ identified,
748
+ trigger: r.trigger,
749
+ app: st.app
750
+ });
751
+ const status = await deps.send(body);
752
+ if (status === 403 || status === 409 || status === 413) r.stopped = true;
753
+ };
754
+ const tick = async () => {
755
+ if (busy || !st.allowed() || !st.config()) return;
756
+ const r = ensure();
757
+ if (r.stopped || deps.now() - r.start > MAX_MS) return;
758
+ if (deps.keyboardOpen()) return;
759
+ busy = true;
760
+ try {
761
+ const shot = await deps.capture(WIDTH);
762
+ if (!shot || shot.img === last) return;
763
+ last = shot.img;
764
+ const size = deps.screen();
765
+ const scale = size && size.w ? shot.w / size.w : 1;
766
+ const measured = await deps.masks();
767
+ if (measured === null) {
768
+ last = "";
769
+ return;
770
+ }
771
+ const masks = measured.map(([x, y, w, h3]) => [x * scale, y * scale, w * scale, h3 * scale].map(Math.round));
772
+ frames.push({ t: deps.now(), w: shot.w, h: shot.h, img: shot.img, masks });
773
+ if (!r.uploading) {
774
+ const since = deps.now() - BUFFER_MS;
775
+ frames = frames.filter((f) => f.t >= since);
776
+ marks = marks.filter((m) => m.t >= since);
777
+ return;
778
+ }
779
+ const chars = frames.reduce((a, f) => a + f.img.length, 0);
780
+ if (frames.length >= CHUNK_FRAMES || chars >= CHUNK_CHARS) await send(r);
781
+ } catch {
782
+ } finally {
783
+ busy = false;
784
+ }
785
+ };
786
+ return {
787
+ /** Starts or stops the frame loop to match the app state (active) and whether recording is allowed. */
788
+ sync(active) {
789
+ const on = active && st.allowed() && !!st.config();
790
+ if (on && timer === null) timer = deps.setInterval(() => void tick(), FRAME_MS);
791
+ if (!on && timer !== null) {
792
+ deps.clearInterval(timer);
793
+ timer = null;
794
+ if (rec?.uploading) void send(rec);
795
+ }
796
+ },
797
+ /** A screen, tap, rage tap or error, for the timeline. */
798
+ mark(m) {
799
+ if (!st.allowed() || !st.config()) return;
800
+ ensure();
801
+ marks.push({ t: deps.now(), ...m });
802
+ if (marks.length > 300) marks.splice(0, marks.length - 300);
803
+ },
804
+ /** A rage tap or an error: a buffering recording uploads its last minute and keeps recording. */
805
+ trigger(kind) {
806
+ if (!rec || rec.uploading || rec.stopped || !st.allowed()) return;
807
+ rec.uploading = true;
808
+ rec.trigger = kind;
809
+ void send(rec);
810
+ },
811
+ /** Sends what waits (going to the background). */
812
+ flush() {
813
+ if (rec?.uploading) return send(rec);
814
+ return Promise.resolve();
815
+ },
816
+ /** Consent withdrawn: forget everything not sent. */
817
+ drop() {
818
+ frames = [];
819
+ marks = [];
820
+ rec = null;
821
+ last = "";
822
+ },
823
+ /** For tests: one frame now. */
824
+ tick
825
+ };
826
+ };
827
+ var maskedViews = /* @__PURE__ */ new Set();
828
+ var measureMasks = () => Promise.all(
829
+ [...maskedViews].map(
830
+ (ref) => new Promise((resolve) => {
831
+ const v = ref.current;
832
+ if (!v) return resolve("none");
833
+ const t = setTimeout(() => resolve("failed"), 100);
834
+ try {
835
+ v.measureInWindow((x, y, w, h3) => {
836
+ clearTimeout(t);
837
+ resolve(w > 0 && h3 > 0 ? [x, y, w, h3] : "none");
838
+ });
839
+ } catch {
840
+ clearTimeout(t);
841
+ resolve("failed");
842
+ }
843
+ })
844
+ )
845
+ ).then((rs) => rs.includes("failed") ? null : rs.filter((r) => Array.isArray(r)));
846
+
206
847
  // src/referral.ts
207
848
  var REF_PARAM = "mrr_ref";
208
849
  var CLICK_PARAM = "mrr_click";
@@ -225,7 +866,8 @@ var KEYS = {
225
866
  queue: "mrrocket:queue",
226
867
  config: "mrrocket:config",
227
868
  installed: "mrrocket:installed",
228
- referral: "mrrocket:referral"
869
+ referral: "mrrocket:referral",
870
+ surveys: "mrrocket:surveys"
229
871
  };
230
872
  var MAX_ERRORS = 20;
231
873
  var MAX_CRUMBS = 20;
@@ -288,13 +930,43 @@ var createClient = (options, deps) => {
288
930
  let backgroundSince = null;
289
931
  const early = [];
290
932
  const exposed = /* @__PURE__ */ new Set();
933
+ const surveysDone = /* @__PURE__ */ new Set();
934
+ const rage = new RageDetector();
935
+ let textsFor = null;
936
+ const texts = () => {
937
+ const id = distinctId();
938
+ if (!textsFor || textsFor.config !== config || textsFor.id !== id) textsFor = { config, id, texts: textChanges(config, id, {}) };
939
+ return textsFor.texts;
940
+ };
291
941
  const listeners = /* @__PURE__ */ new Set();
292
942
  const crumbs = [];
293
943
  const errorQueue = [];
294
944
  const errorsSeen = /* @__PURE__ */ new Set();
295
945
  let errorsSent = 0;
946
+ const flows = createFlows({
947
+ track: (event, props) => record(event, props),
948
+ personId: () => consent !== "off" ? identified : void 0,
949
+ auto: options.onboarding !== false,
950
+ storage: () => storage && consent === "persistent" ? {
951
+ get: (k) => get(k),
952
+ set: (k, v) => void set(k, v)
953
+ } : null,
954
+ openUrl: deps.openUrl,
955
+ fetchInApp: async (id) => {
956
+ if (!apiKey) return null;
957
+ const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}`, { method: "GET" });
958
+ return r.ok ? await r.json() : null;
959
+ }
960
+ });
961
+ const refreshFlows = () => {
962
+ if (!isReady || closed) return;
963
+ flows.setConfig(config);
964
+ void flows.refresh();
965
+ };
966
+ let recorder = null;
296
967
  const changed = () => {
297
968
  version++;
969
+ recorder?.sync(appState === "active");
298
970
  for (const l of [...listeners]) {
299
971
  try {
300
972
  l();
@@ -469,6 +1141,8 @@ var createClient = (options, deps) => {
469
1141
  ...who ? { distinctId: who } : {},
470
1142
  ...session ? { sessionId: session.id } : {}
471
1143
  });
1144
+ recorder?.mark({ kind: "error", path: currentScreen ?? "/", label: clip2(d.message, 120) });
1145
+ recorder?.trigger("error");
472
1146
  if (fatal) void sendErrors();
473
1147
  else if (errorTimer === null) errorTimer = setTimeoutFn(() => void sendErrors(), 2e3);
474
1148
  };
@@ -486,11 +1160,34 @@ var createClient = (options, deps) => {
486
1160
  previousHandler?.(error, isFatal);
487
1161
  });
488
1162
  };
1163
+ let replayOn = options.replay === true;
1164
+ recorder = deps.recorder ? createRecorder(
1165
+ {
1166
+ now,
1167
+ setInterval: setIntervalFn,
1168
+ clearInterval: clearIntervalFn,
1169
+ capture: deps.recorder.capture,
1170
+ keyboardOpen: deps.recorder.keyboardOpen,
1171
+ masks: deps.recorder.masks,
1172
+ screen: () => deps.screen?.(),
1173
+ send: (body) => post("/api/sdk/replay", body)
1174
+ },
1175
+ {
1176
+ allowed: () => replayOn && loaded && !closed && consent !== "off" && !!apiKey,
1177
+ config: () => config?.replay,
1178
+ session: () => session?.id ?? touchSession(),
1179
+ who: () => ({ distinctId: sendId(), identified: !!identified }),
1180
+ apiKey,
1181
+ app: context
1182
+ }
1183
+ ) : null;
1184
+ if (options.replay && !deps.recorder) warn("Session recordings need react-native-view-shot: npx expo install react-native-view-shot");
489
1185
  let appStateSub;
490
1186
  const onAppState = (next) => {
491
1187
  if (closed) return;
492
1188
  const prev = appState;
493
1189
  appState = next;
1190
+ recorder?.sync(next === "active");
494
1191
  if (next === "active" && prev !== "active") {
495
1192
  startTimer();
496
1193
  if (backgroundSince !== null) {
@@ -503,6 +1200,7 @@ var createClient = (options, deps) => {
503
1200
  crumb("lifecycle", "app_background");
504
1201
  if (options.lifecycle !== false) record("$app_background");
505
1202
  stopTimer();
1203
+ void recorder?.flush();
506
1204
  void persistQueue();
507
1205
  void flush();
508
1206
  if (errorQueue.length) void sendErrors();
@@ -526,6 +1224,61 @@ var createClient = (options, deps) => {
526
1224
  debug("Config not reachable; using the cached one.");
527
1225
  }
528
1226
  };
1227
+ let pushToken = null;
1228
+ let pushFor;
1229
+ let pushOff = null;
1230
+ const opened = /* @__PURE__ */ new Set();
1231
+ const onPushOpen = (data) => {
1232
+ const m = data?.mrrocket;
1233
+ if (!m?.id || opened.has(m.id)) return;
1234
+ opened.add(m.id);
1235
+ record("$push", { action: "opened", message: m.id });
1236
+ if (m.url && /^[a-z][a-z0-9+.-]*:/i.test(m.url)) deps.openUrl?.(m.url);
1237
+ };
1238
+ const listenForOpens = () => {
1239
+ if (pushOff || !deps.push || closed) return;
1240
+ try {
1241
+ pushOff = deps.push.onOpen(onPushOpen);
1242
+ void deps.push.lastOpen?.().then(onPushOpen).catch(() => void 0);
1243
+ } catch {
1244
+ pushOff = null;
1245
+ }
1246
+ };
1247
+ const loadedNow = () => loaded ? Promise.resolve() : new Promise((resolve) => {
1248
+ early.push(resolve);
1249
+ });
1250
+ const registerForPush = async (o = {}) => {
1251
+ if (!deps.push) return { status: "unavailable", reason: "Install expo-notifications to receive push notifications." };
1252
+ await loadedNow();
1253
+ if (!apiKey || consent === "off") return { status: "error", reason: !apiKey ? "No public key." : "The consent is off." };
1254
+ if (!identified) return { status: "not-signed-in" };
1255
+ const who = identified;
1256
+ try {
1257
+ const permission = await deps.push.permission(o.ask !== false);
1258
+ if (permission !== "granted") return { status: permission === "denied" ? "denied" : "not-asked" };
1259
+ const token = await deps.push.token(o.projectId);
1260
+ if (!token) return { status: "unavailable", reason: "This device has no push token (a simulator, or no EAS project id)." };
1261
+ const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: who, platform: "expo", token, app: context }));
1262
+ if (status < 200 || status >= 300) {
1263
+ warn(`Push registration was rejected with ${status || "no answer"}. Check the public key and the host.`);
1264
+ return { status: "error", reason: `MRRocket answered ${status || "nothing"}.` };
1265
+ }
1266
+ pushToken = token;
1267
+ pushFor = who;
1268
+ listenForOpens();
1269
+ debug("Registered for push.");
1270
+ return { status: "registered", token };
1271
+ } catch (e) {
1272
+ return { status: "error", reason: e instanceof Error ? e.message : String(e) };
1273
+ }
1274
+ };
1275
+ const unregisterForPush = async () => {
1276
+ const token = pushToken;
1277
+ if (!token || !apiKey) return false;
1278
+ pushToken = null;
1279
+ const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: pushFor, platform: "expo", token, remove: true }));
1280
+ return status >= 200 && status < 300;
1281
+ };
529
1282
  let resolveReady = () => {
530
1283
  };
531
1284
  const ready = new Promise((r) => {
@@ -540,12 +1293,13 @@ var createClient = (options, deps) => {
540
1293
  const start = async () => {
541
1294
  let firstOpen = false;
542
1295
  if (consent !== "off") {
543
- const [id, stored, cached, installed, ref] = await Promise.all([
1296
+ const [id, stored, cached, installed, ref, surveys] = await Promise.all([
544
1297
  get(KEYS.id),
545
1298
  get(KEYS.queue),
546
1299
  get(KEYS.config),
547
1300
  get(KEYS.installed),
548
- get(KEYS.referral)
1301
+ get(KEYS.referral),
1302
+ get(KEYS.surveys)
549
1303
  ]);
550
1304
  if (consent === "persistent") {
551
1305
  installId = id || newDistinctId();
@@ -568,6 +1322,14 @@ var createClient = (options, deps) => {
568
1322
  if (r && typeof r.code === "string") referral = r;
569
1323
  } catch {
570
1324
  }
1325
+ try {
1326
+ const done = surveys && consent === "persistent" ? JSON.parse(surveys) : null;
1327
+ if (Array.isArray(done)) {
1328
+ for (const x of done) if (typeof x === "string") surveysDone.add(x);
1329
+ }
1330
+ } catch {
1331
+ await remove(KEYS.surveys);
1332
+ }
571
1333
  firstOpen = !!storage && !installed;
572
1334
  if (firstOpen) await set(KEYS.installed, String(now()));
573
1335
  } else {
@@ -601,10 +1363,12 @@ var createClient = (options, deps) => {
601
1363
  for (const run of early.splice(0)) run();
602
1364
  if (deps.appState) appStateSub = deps.appState.addEventListener("change", onAppState);
603
1365
  installErrorHandler();
1366
+ listenForOpens();
604
1367
  if (appState === "active" || !deps.appState) startTimer();
605
1368
  if (queue.length) void flush();
606
1369
  await fetchConfig();
607
1370
  markReady();
1371
+ refreshFlows();
608
1372
  };
609
1373
  void start().catch(() => {
610
1374
  loaded = true;
@@ -614,6 +1378,25 @@ var createClient = (options, deps) => {
614
1378
  track: (event, props) => {
615
1379
  if (typeof event !== "string" || !event) return;
616
1380
  record(event, props);
1381
+ flows.onEvent(event);
1382
+ },
1383
+ tap: (x, y) => {
1384
+ if (options.heatmaps === false || consent === "off") return;
1385
+ const size = deps.screen?.();
1386
+ if (!size || !Number.isFinite(x) || !Number.isFinite(y)) return;
1387
+ const props = { x: Math.round(x), y: Math.round(y), vw: Math.round(size.w), vh: Math.round(size.h), docH: Math.round(size.h), selector: "", label: "", dead: false };
1388
+ record("$click", props);
1389
+ recorder?.mark({ kind: "click", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
1390
+ if (rage.push(x, y, now())) {
1391
+ record("$rageclick", props);
1392
+ recorder?.mark({ kind: "rage", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
1393
+ recorder?.trigger("rage");
1394
+ }
1395
+ },
1396
+ setReplay: (on) => {
1397
+ replayOn = on;
1398
+ if (!on) recorder?.drop();
1399
+ recorder?.sync(appState === "active");
617
1400
  },
618
1401
  screen: (name, props) => {
619
1402
  if (typeof name !== "string" || !name.trim()) return;
@@ -623,6 +1406,7 @@ var createClient = (options, deps) => {
623
1406
  currentScreen = path;
624
1407
  crumb("navigation", path);
625
1408
  record("$screen", { ...props, path, screen: name, ...previous ? { previous } : {} });
1409
+ recorder?.mark({ kind: "pageview", path });
626
1410
  },
627
1411
  identify: (id, props) => {
628
1412
  if (typeof id !== "string" || !id) return;
@@ -636,12 +1420,14 @@ var createClient = (options, deps) => {
636
1420
  record("$identify", { ...props, id, ...previous && previous !== id ? { previousId: previous } : {} });
637
1421
  if (consent === "persistent") void set(KEYS.id, id);
638
1422
  changed();
1423
+ refreshFlows();
639
1424
  },
640
1425
  reset: () => {
641
1426
  if (!loaded) {
642
1427
  early.push(() => client.reset());
643
1428
  return;
644
1429
  }
1430
+ if (pushToken) void unregisterForPush();
645
1431
  identified = void 0;
646
1432
  person = void 0;
647
1433
  session = null;
@@ -653,6 +1439,7 @@ var createClient = (options, deps) => {
653
1439
  void set(KEYS.id, installId);
654
1440
  }
655
1441
  changed();
1442
+ refreshFlows();
656
1443
  },
657
1444
  setConsent: (next) => {
658
1445
  if (next !== "anonymous" && next !== "persistent" && next !== "off") {
@@ -667,6 +1454,7 @@ var createClient = (options, deps) => {
667
1454
  const before = consent;
668
1455
  consent = next;
669
1456
  if (next === "off") {
1457
+ recorder?.drop();
670
1458
  queue = [];
671
1459
  session = null;
672
1460
  errorQueue.length = 0;
@@ -675,13 +1463,16 @@ var createClient = (options, deps) => {
675
1463
  installId = null;
676
1464
  void remove(KEYS.id);
677
1465
  void remove(KEYS.referral);
1466
+ void remove(KEYS.surveys);
678
1467
  } else {
679
1468
  installId = identified ?? newDistinctId();
680
1469
  void set(KEYS.id, installId);
681
1470
  if (referral) void set(KEYS.referral, JSON.stringify(referral));
1471
+ if (surveysDone.size) void set(KEYS.surveys, JSON.stringify([...surveysDone]));
682
1472
  }
683
1473
  if (before === "off" && currentScreen && loaded) record("$screen", { path: currentScreen, screen: currentScreen });
684
1474
  changed();
1475
+ refreshFlows();
685
1476
  },
686
1477
  captureError: (error) => capture(error, "manual"),
687
1478
  getFlag: (key) => flagFor(key, config, distinctId(), { ...person }) !== false,
@@ -702,11 +1493,40 @@ var createClient = (options, deps) => {
702
1493
  exposed.add(mark);
703
1494
  record("$exposure", { experiment: key, variant });
704
1495
  },
1496
+ getText: (id, fallback) => {
1497
+ client.exposeText(id);
1498
+ return client.peekText(id) ?? fallback;
1499
+ },
1500
+ peekText: (id) => texts()[id],
1501
+ exposeText: (id) => {
1502
+ for (const key of experimentsOfText(config, id)) client.expose(key, resolveVariant(key, config, distinctId(), {}));
1503
+ },
1504
+ openCancelFlow: (o) => flows.openCancelFlow(o),
1505
+ startOnboarding: (id) => flows.startOnboarding(id),
1506
+ registerForPush,
1507
+ unregisterForPush,
1508
+ flows,
705
1509
  flush,
706
1510
  ready,
707
1511
  isReady: () => isReady,
708
1512
  getDistinctId: distinctId,
709
1513
  getConsent: () => consent,
1514
+ getUser: () => {
1515
+ if (!identified) return void 0;
1516
+ const p = person;
1517
+ return { id: identified, ...typeof p?.name === "string" ? { name: p.name } : {}, ...typeof p?.email === "string" ? { email: p.email } : {} };
1518
+ },
1519
+ api: (as) => {
1520
+ if (!apiKey || consent === "off") return null;
1521
+ const known = client.getUser();
1522
+ const user = as?.name || as?.email ? { ...known, ...as.name ? { name: as.name } : {}, ...as.email ? { email: as.email } : {} } : known;
1523
+ return createApi({ host, apiKey, distinctId: distinctId(), user });
1524
+ },
1525
+ isSurveyDone: (id) => surveysDone.has(id),
1526
+ markSurveyDone: (id) => {
1527
+ surveysDone.add(id);
1528
+ if (consent === "persistent") void set(KEYS.surveys, JSON.stringify([...surveysDone].slice(-100)));
1529
+ },
710
1530
  getConfig: () => config,
711
1531
  subscribe: (listener) => {
712
1532
  listeners.add(listener);
@@ -716,7 +1536,10 @@ var createClient = (options, deps) => {
716
1536
  close: async () => {
717
1537
  if (closed) return;
718
1538
  stopTimer();
1539
+ recorder?.sync(false);
719
1540
  appStateSub?.remove();
1541
+ pushOff?.();
1542
+ pushOff = null;
720
1543
  if (handlerInstalled && deps.errorUtils && previousHandler) deps.errorUtils.setGlobalHandler(previousHandler);
721
1544
  await flush();
722
1545
  await sendErrors();
@@ -739,8 +1562,252 @@ var activeRouteName = (state) => {
739
1562
  return name;
740
1563
  };
741
1564
 
742
- // src/native/platform.ts
1565
+ // src/native/flows-view.ts
1566
+ var import_react = require("react");
743
1567
  var RN = require("react-native");
1568
+ var LIGHT = { bg: "#ffffff", fg: "#1c1d20", muted: "#5d6068", soft: "#f4f5f7", line: "#e4e5e8" };
1569
+ var DARK = { bg: "#17181b", fg: "#f1f2f4", muted: "#a9acb3", soft: "#232428", line: "#2e2f35" };
1570
+ var months = (n) => `${n} ${n === 1 ? "month" : "months"}`;
1571
+ var MRRocketFlows = ({ flows, appearance = {} }) => {
1572
+ const scheme = RN.useColorScheme?.();
1573
+ const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
1574
+ const p = { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
1575
+ const [, setTick] = (0, import_react.useState)(0);
1576
+ (0, import_react.useEffect)(() => flows.subscribe(() => setTick((t) => t + 1)), [flows]);
1577
+ const v = flows.view();
1578
+ const [comment, setComment] = (0, import_react.useState)("");
1579
+ const screen = v?.type === "cancel" ? v.screen : null;
1580
+ (0, import_react.useEffect)(() => {
1581
+ if (!screen) setComment("");
1582
+ }, [screen]);
1583
+ if (!v) return null;
1584
+ const text2 = (children, style = {}) => (0, import_react.createElement)(RN.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
1585
+ const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
1586
+ const title = (children) => (0, import_react.createElement)(RN.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 18, fontWeight: "700" } }, children);
1587
+ const btn = (label, kind, onPress, disabled = false) => (0, import_react.createElement)(
1588
+ RN.Pressable,
1589
+ {
1590
+ key: label,
1591
+ onPress,
1592
+ disabled,
1593
+ accessibilityRole: "button",
1594
+ accessibilityState: { disabled },
1595
+ style: {
1596
+ height: kind === "quiet" ? 36 : 44,
1597
+ borderRadius: 10,
1598
+ alignItems: "center",
1599
+ justifyContent: "center",
1600
+ opacity: disabled ? 0.55 : 1,
1601
+ backgroundColor: kind === "primary" ? p.accent : kind === "danger" ? p.danger : kind === "secondary" ? p.bg : "transparent",
1602
+ borderWidth: kind === "secondary" ? 1 : 0,
1603
+ borderColor: p.line
1604
+ }
1605
+ },
1606
+ text2(label, {
1607
+ color: kind === "primary" || kind === "danger" ? "#ffffff" : kind === "quiet" ? p.muted : p.fg,
1608
+ fontWeight: kind === "quiet" ? "500" : "600"
1609
+ })
1610
+ );
1611
+ const close = (label, onPress) => (0, import_react.createElement)(
1612
+ RN.Pressable,
1613
+ { onPress, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 12, style: { marginLeft: "auto", padding: 4 } },
1614
+ text2("\xD7", { fontSize: 22, color: p.muted })
1615
+ );
1616
+ const card = (children, style = {}) => (0, import_react.createElement)(
1617
+ RN.View,
1618
+ {
1619
+ style: {
1620
+ backgroundColor: p.bg,
1621
+ borderRadius: 16,
1622
+ padding: 20,
1623
+ gap: 12,
1624
+ shadowColor: "#000",
1625
+ shadowOpacity: 0.18,
1626
+ shadowRadius: 20,
1627
+ elevation: 8,
1628
+ ...style
1629
+ }
1630
+ },
1631
+ ...children
1632
+ );
1633
+ const sheet = (children, onClose) => (0, import_react.createElement)(
1634
+ RN.Modal,
1635
+ { visible: true, transparent: true, animationType: "fade", onRequestClose: onClose },
1636
+ (0, import_react.createElement)(
1637
+ RN.View,
1638
+ { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.4)", justifyContent: "flex-end" } },
1639
+ card(children, { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, paddingBottom: 34 })
1640
+ )
1641
+ );
1642
+ const input = (placeholder) => (0, import_react.createElement)(RN.TextInput, {
1643
+ value: comment,
1644
+ onChangeText: setComment,
1645
+ placeholder,
1646
+ placeholderTextColor: p.muted,
1647
+ multiline: true,
1648
+ maxLength: 500,
1649
+ accessibilityLabel: placeholder,
1650
+ style: { minHeight: 72, borderWidth: 1, borderColor: p.line, borderRadius: 10, padding: 10, color: p.fg, textAlignVertical: "top" }
1651
+ });
1652
+ if (v.type === "cancel") {
1653
+ const top = (0, import_react.createElement)(
1654
+ RN.View,
1655
+ { style: { flexDirection: "row", alignItems: "center", gap: 4 } },
1656
+ ...v.steps > 1 ? Array.from(
1657
+ { length: v.steps },
1658
+ (_, i) => (0, import_react.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i < v.step ? p.accent : p.line } })
1659
+ ) : [],
1660
+ close("Close", flows.cancel.close)
1661
+ );
1662
+ const error = v.error ? text2(v.error, { color: p.danger, fontSize: 13 }) : null;
1663
+ let body = [];
1664
+ if (v.screen === "survey")
1665
+ body = [
1666
+ title(v.flow.question || "Why are you leaving?"),
1667
+ (0, import_react.createElement)(
1668
+ RN.ScrollView,
1669
+ { style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
1670
+ ...v.flow.reasons.map(
1671
+ (r) => (0, import_react.createElement)(
1672
+ RN.Pressable,
1673
+ {
1674
+ key: r.id,
1675
+ onPress: () => flows.cancel.pick(r.id),
1676
+ accessibilityRole: "radio",
1677
+ accessibilityState: { checked: v.reason === r.id },
1678
+ style: {
1679
+ minHeight: 44,
1680
+ paddingHorizontal: 12,
1681
+ borderRadius: 10,
1682
+ borderWidth: 1,
1683
+ borderColor: v.reason === r.id ? p.accent : p.line,
1684
+ backgroundColor: v.reason === r.id ? p.soft : p.bg,
1685
+ justifyContent: "center"
1686
+ }
1687
+ },
1688
+ text2(r.label, { fontWeight: "500" })
1689
+ )
1690
+ )
1691
+ ),
1692
+ btn("Continue", "primary", flows.cancel.next, !v.reason),
1693
+ btn("Never mind, keep my plan", "quiet", flows.cancel.close)
1694
+ ];
1695
+ else if (v.screen === "offer" && v.offer) {
1696
+ const o = v.offer;
1697
+ const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
1698
+ 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;
1699
+ body = [
1700
+ label ? muted(`You said: ${label}`) : null,
1701
+ title(o.headline),
1702
+ detail ? (0, import_react.createElement)(RN.View, { style: { backgroundColor: p.soft, borderRadius: 10, padding: 12 } }, text2(detail, { fontWeight: "600" })) : null,
1703
+ o.kind === "contact" ? input("What would make you stay?") : null,
1704
+ btn(v.busy ? "One moment\u2026" : offerButton(o), "primary", () => flows.cancel.accept(comment), v.busy),
1705
+ btn("No thanks, continue cancelling", "quiet", flows.cancel.decline, v.busy)
1706
+ ];
1707
+ } else
1708
+ body = [
1709
+ title(v.flow.confirm.title),
1710
+ v.flow.confirm.body ? muted(v.flow.confirm.body, { fontSize: 15 }) : null,
1711
+ v.steps > 1 ? input("Anything else we should know? (optional)") : null,
1712
+ btn(v.busy ? "One moment\u2026" : v.flow.confirm.button, "danger", () => flows.cancel.confirm(comment), v.busy),
1713
+ btn("Keep my plan", "secondary", flows.cancel.close, v.busy)
1714
+ ];
1715
+ return sheet([top, ...body, error], flows.cancel.close);
1716
+ }
1717
+ if (v.type === "step")
1718
+ return sheet(
1719
+ [
1720
+ (0, import_react.createElement)(
1721
+ RN.View,
1722
+ { style: { flexDirection: "row", alignItems: "center" } },
1723
+ v.total > 1 ? muted(`Step ${v.index + 1} of ${v.total}`) : null,
1724
+ close("Close the tour", flows.onboarding.dismiss)
1725
+ ),
1726
+ title(v.step.title),
1727
+ v.step.body ? muted(v.step.body, { fontSize: 15 }) : null,
1728
+ btn(v.step.button || (v.last ? "Done" : "Next"), "primary", flows.onboarding.next)
1729
+ ],
1730
+ flows.onboarding.dismiss
1731
+ );
1732
+ if (v.type === "checklist") {
1733
+ const done = v.items.filter((i) => i.done).length;
1734
+ return (0, import_react.createElement)(
1735
+ RN.View,
1736
+ { pointerEvents: "box-none", style: { position: "absolute", left: 12, right: 12, bottom: 28 } },
1737
+ card([
1738
+ (0, import_react.createElement)(
1739
+ RN.View,
1740
+ { key: "t", style: { flexDirection: "row", alignItems: "center" } },
1741
+ title(v.flow.name),
1742
+ close("Hide the checklist", flows.onboarding.dismiss)
1743
+ ),
1744
+ muted(`${done} of ${v.items.length} done`),
1745
+ ...v.items.map(
1746
+ ({ step, done: d }) => (0, import_react.createElement)(
1747
+ RN.Pressable,
1748
+ {
1749
+ key: step.id,
1750
+ disabled: d || !!step.event,
1751
+ onPress: () => flows.onboarding.check(step.id),
1752
+ accessibilityRole: "checkbox",
1753
+ accessibilityState: { checked: d },
1754
+ style: { flexDirection: "row", gap: 10, alignItems: "flex-start", paddingVertical: 6 }
1755
+ },
1756
+ (0, import_react.createElement)(
1757
+ RN.View,
1758
+ {
1759
+ style: {
1760
+ width: 20,
1761
+ height: 20,
1762
+ borderRadius: 10,
1763
+ borderWidth: 1.5,
1764
+ borderColor: d ? p.accent : p.line,
1765
+ backgroundColor: d ? p.accent : "transparent",
1766
+ alignItems: "center",
1767
+ justifyContent: "center"
1768
+ }
1769
+ },
1770
+ d ? text2("\u2713", { color: "#fff", fontSize: 12 }) : null
1771
+ ),
1772
+ (0, import_react.createElement)(
1773
+ RN.View,
1774
+ { style: { flex: 1 } },
1775
+ text2(step.title, { fontWeight: "600", color: d ? p.muted : p.fg, textDecorationLine: d ? "line-through" : "none" }),
1776
+ step.body ? muted(step.body) : null
1777
+ )
1778
+ )
1779
+ )
1780
+ ])
1781
+ );
1782
+ }
1783
+ const m = v.message;
1784
+ if (m.style === "banner")
1785
+ return (0, import_react.createElement)(
1786
+ RN.View,
1787
+ { pointerEvents: "box-none", style: { position: "absolute", left: 12, right: 12, top: 56 } },
1788
+ card([
1789
+ (0, import_react.createElement)(
1790
+ RN.View,
1791
+ { key: "t", style: { flexDirection: "row", alignItems: "center", gap: 8 } },
1792
+ (0, import_react.createElement)(RN.View, { style: { flex: 1 } }, text2(m.title, { fontWeight: "700" }), m.body ? muted(m.body) : null),
1793
+ close("Dismiss", flows.message.dismiss)
1794
+ ),
1795
+ m.button ? btn(m.button, "primary", flows.message.click) : null
1796
+ ])
1797
+ );
1798
+ return sheet(
1799
+ [
1800
+ (0, import_react.createElement)(RN.View, { key: "t", style: { flexDirection: "row" } }, close("Dismiss", flows.message.dismiss)),
1801
+ title(m.title),
1802
+ m.body ? muted(m.body, { fontSize: 15 }) : null,
1803
+ m.button ? btn(m.button, "primary", flows.message.click) : null
1804
+ ],
1805
+ flows.message.dismiss
1806
+ );
1807
+ };
1808
+
1809
+ // src/native/platform.ts
1810
+ var RN2 = require("react-native");
744
1811
  var asyncStorageModule = null;
745
1812
  try {
746
1813
  asyncStorageModule = require("@react-native-async-storage/async-storage");
@@ -765,6 +1832,51 @@ try {
765
1832
  } catch {
766
1833
  expoLocalization = null;
767
1834
  }
1835
+ var viewShot = null;
1836
+ try {
1837
+ viewShot = require("react-native-view-shot");
1838
+ } catch {
1839
+ viewShot = null;
1840
+ }
1841
+ var expoNotifications = null;
1842
+ try {
1843
+ expoNotifications = require("expo-notifications");
1844
+ } catch {
1845
+ expoNotifications = null;
1846
+ }
1847
+ var expoConstants = {};
1848
+ try {
1849
+ expoConstants = require("expo-constants");
1850
+ } catch {
1851
+ expoConstants = {};
1852
+ }
1853
+ var dataOf = (r) => r?.notification?.request?.content?.data ?? null;
1854
+ var expoPush = () => {
1855
+ const n = expoNotifications;
1856
+ if (!n || typeof n.getExpoPushTokenAsync !== "function") return null;
1857
+ const constants = expoConstants.default ?? expoConstants;
1858
+ return {
1859
+ permission: async (ask) => {
1860
+ const now = await n.getPermissionsAsync();
1861
+ if (now.granted || now.status === "granted") return "granted";
1862
+ if (now.status === "denied" || !ask) return now.status === "denied" ? "denied" : "undetermined";
1863
+ const asked = await n.requestPermissionsAsync();
1864
+ return asked.granted || asked.status === "granted" ? "granted" : asked.status === "denied" ? "denied" : "undetermined";
1865
+ },
1866
+ token: async (projectId) => {
1867
+ if (RN2.Platform.OS === "android" && n.setNotificationChannelAsync)
1868
+ await n.setNotificationChannelAsync("default", { name: "Default", importance: n.AndroidImportance?.HIGH ?? 4 });
1869
+ const id = projectId ?? constants.expoConfig?.extra?.eas?.projectId ?? constants.easConfig?.projectId;
1870
+ const t = await n.getExpoPushTokenAsync(id ? { projectId: id } : void 0);
1871
+ return typeof t?.data === "string" && t.data ? t.data : null;
1872
+ },
1873
+ onOpen: (listener) => {
1874
+ const sub = n.addNotificationResponseReceivedListener((r) => listener(dataOf(r) ?? {}));
1875
+ return () => sub.remove();
1876
+ },
1877
+ lastOpen: async () => dataOf(await n.getLastNotificationResponseAsync?.())
1878
+ };
1879
+ };
768
1880
  var asyncStorage = () => {
769
1881
  const m = asyncStorageModule;
770
1882
  const s = m?.default ?? m;
@@ -773,7 +1885,7 @@ var asyncStorage = () => {
773
1885
  var DEVICE_TYPES = { 1: "phone", 2: "tablet", 3: "desktop", 4: "tv" };
774
1886
  var text = (v, max = 80) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : typeof v === "number" ? String(v) : void 0;
775
1887
  var readContext = () => {
776
- const p = RN.Platform;
1888
+ const p = RN2.Platform;
777
1889
  let locale;
778
1890
  try {
779
1891
  locale = text(expoLocalization?.getLocales?.()[0]?.languageTag, 35);
@@ -799,27 +1911,852 @@ var readContext = () => {
799
1911
  for (const k of Object.keys(ctx)) if (ctx[k] === void 0) delete ctx[k];
800
1912
  return ctx;
801
1913
  };
1914
+ var recorderDeps = () => {
1915
+ const capture = viewShot?.captureScreen;
1916
+ if (!capture) return null;
1917
+ let keyboard = false;
1918
+ try {
1919
+ RN2.Keyboard.addListener("keyboardWillShow", () => keyboard = true);
1920
+ RN2.Keyboard.addListener("keyboardDidShow", () => keyboard = true);
1921
+ RN2.Keyboard.addListener("keyboardDidHide", () => keyboard = false);
1922
+ } catch {
1923
+ return null;
1924
+ }
1925
+ return {
1926
+ capture: async (width) => {
1927
+ const s = RN2.Dimensions.get("screen");
1928
+ if (!s?.width) return null;
1929
+ const w = Math.round(width);
1930
+ const h3 = Math.round(s.height / s.width * width);
1931
+ const img = await capture({ format: "jpg", quality: 0.5, result: "base64", width: w, height: h3 });
1932
+ return img ? { img: img.replace(/^data:image\/\w+;base64,/, ""), w, h: h3 } : null;
1933
+ },
1934
+ keyboardOpen: () => keyboard,
1935
+ masks: measureMasks
1936
+ };
1937
+ };
802
1938
  var nativeDeps = (overrides = {}) => ({
803
1939
  storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
804
1940
  fetch: ((url, init) => fetch(url, init)),
805
- appState: RN.AppState,
1941
+ appState: RN2.AppState,
806
1942
  errorUtils: globalThis.ErrorUtils ?? null,
807
1943
  context: readContext,
808
1944
  appId: text(expoApplication?.applicationId, 150),
809
1945
  screen: () => {
810
- const s = RN.Dimensions.get("screen");
1946
+ const s = RN2.Dimensions.get("screen");
811
1947
  return s ? { w: s.width, h: s.height } : void 0;
812
1948
  },
813
- initialUrl: () => RN.Linking.getInitialURL()
1949
+ initialUrl: () => RN2.Linking.getInitialURL(),
1950
+ openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
1951
+ push: expoPush(),
1952
+ recorder: recorderDeps()
814
1953
  });
815
1954
  var optionalModules = () => ({
816
1955
  asyncStorage: !!asyncStorage(),
817
1956
  expoDevice: !!expoDevice,
818
1957
  expoApplication: !!expoApplication,
819
- expoLocalization: !!expoLocalization
1958
+ expoLocalization: !!expoLocalization,
1959
+ expoNotifications: !!expoNotifications,
1960
+ viewShot: !!viewShot?.captureScreen
1961
+ });
1962
+
1963
+ // src/native/widgets-view.ts
1964
+ var import_react2 = require("react");
1965
+ var RN3 = require("react-native");
1966
+ var usePalette = (appearance = {}) => {
1967
+ const scheme = RN3.useColorScheme?.();
1968
+ const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
1969
+ return { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
1970
+ };
1971
+ var kit = (p) => {
1972
+ const text2 = (children, style = {}) => (0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
1973
+ const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
1974
+ const title = (children) => (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700", flex: 1 } }, children);
1975
+ const btn = (label, kind, onPress, disabled = false, style = {}) => (0, import_react2.createElement)(
1976
+ RN3.Pressable,
1977
+ {
1978
+ key: label,
1979
+ onPress,
1980
+ disabled,
1981
+ accessibilityRole: "button",
1982
+ accessibilityState: { disabled },
1983
+ style: {
1984
+ height: 44,
1985
+ paddingHorizontal: 16,
1986
+ borderRadius: 10,
1987
+ alignItems: "center",
1988
+ justifyContent: "center",
1989
+ opacity: disabled ? 0.55 : 1,
1990
+ backgroundColor: kind === "primary" ? p.accent : p.bg,
1991
+ borderWidth: kind === "secondary" ? 1 : 0,
1992
+ borderColor: p.line,
1993
+ ...style
1994
+ }
1995
+ },
1996
+ text2(label, { color: kind === "primary" ? "#ffffff" : p.fg, fontWeight: "600" })
1997
+ );
1998
+ 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 }));
1999
+ const field = (o) => (0, import_react2.createElement)(RN3.TextInput, {
2000
+ value: o.value,
2001
+ onChangeText: o.onChange,
2002
+ placeholder: o.placeholder,
2003
+ placeholderTextColor: p.muted,
2004
+ multiline: !!o.multiline,
2005
+ maxLength: o.max ?? 2e3,
2006
+ accessibilityLabel: o.placeholder,
2007
+ ...o.email ? { keyboardType: "email-address", autoCapitalize: "none", autoComplete: "email", textContentType: "emailAddress" } : {},
2008
+ ...o.onSubmit ? { onSubmitEditing: o.onSubmit, returnKeyType: "send" } : {},
2009
+ style: {
2010
+ minHeight: o.multiline ? 72 : 44,
2011
+ borderWidth: 1,
2012
+ borderColor: p.line,
2013
+ borderRadius: 10,
2014
+ paddingHorizontal: 12,
2015
+ paddingVertical: 10,
2016
+ color: p.fg,
2017
+ backgroundColor: p.bg,
2018
+ textAlignVertical: o.multiline ? "top" : "center",
2019
+ ...o.style
2020
+ }
2021
+ });
2022
+ const chip = (label, on, onPress, key = label) => (0, import_react2.createElement)(
2023
+ RN3.Pressable,
2024
+ {
2025
+ key,
2026
+ onPress,
2027
+ accessibilityRole: "button",
2028
+ accessibilityState: { selected: on },
2029
+ style: { paddingHorizontal: 12, height: 32, borderRadius: 16, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
2030
+ },
2031
+ text2(label, { fontSize: 13, fontWeight: "600", color: on ? p.accent : p.fg })
2032
+ );
2033
+ const page = (heading, onClose, children) => (0, import_react2.createElement)(
2034
+ RN3.Modal,
2035
+ { visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: onClose },
2036
+ (0, import_react2.createElement)(
2037
+ RN3.KeyboardAvoidingView,
2038
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
2039
+ (0, import_react2.createElement)(
2040
+ RN3.View,
2041
+ { style: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: p.line } },
2042
+ title(heading),
2043
+ closeX("Close", onClose)
2044
+ ),
2045
+ ...children
2046
+ )
2047
+ );
2048
+ return { text: text2, muted, title, btn, closeX, field, chip, page };
2049
+ };
2050
+ var timeAgo = (iso) => {
2051
+ const t = new Date(iso).getTime();
2052
+ if (Number.isNaN(t)) return "";
2053
+ const d = Math.max(0, Date.now() - t) / 1e3;
2054
+ if (d < 60) return "just now";
2055
+ if (d < 3600) return `${Math.floor(d / 60)}m ago`;
2056
+ if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
2057
+ return `${Math.floor(d / 86400)}d ago`;
2058
+ };
2059
+ var opener = () => {
2060
+ let open = false;
2061
+ const listeners = /* @__PURE__ */ new Set();
2062
+ const set = (v) => {
2063
+ open = v;
2064
+ listeners.forEach((l) => l(v));
2065
+ };
2066
+ const useOpen = () => {
2067
+ const [v, setV] = (0, import_react2.useState)(open);
2068
+ (0, import_react2.useEffect)(() => {
2069
+ listeners.add(setV);
2070
+ setV(open);
2071
+ return () => {
2072
+ listeners.delete(setV);
2073
+ };
2074
+ }, []);
2075
+ return v;
2076
+ };
2077
+ return { set, get: () => open, useOpen };
2078
+ };
2079
+ var isAnswered = (a) => Array.isArray(a) ? a.length > 0 : typeof a === "number" ? true : !!a?.trim();
2080
+ var SurveySheet = ({ survey, api, p, onDone }) => {
2081
+ const k = kit(p);
2082
+ const [step, setStep] = (0, import_react2.useState)(0);
2083
+ const [answers, setAnswers] = (0, import_react2.useState)({});
2084
+ const [done, setDone] = (0, import_react2.useState)(false);
2085
+ const [error, setError] = (0, import_react2.useState)("");
2086
+ const started = (0, import_react2.useRef)(Date.now());
2087
+ const finished = (0, import_react2.useRef)(false);
2088
+ const q = survey.questions[step];
2089
+ const last = step === survey.questions.length - 1;
2090
+ const value = answers[q.key];
2091
+ (0, import_react2.useEffect)(() => {
2092
+ api.seen(survey.id).catch(() => void 0);
2093
+ }, [api, survey.id]);
2094
+ (0, import_react2.useEffect)(() => {
2095
+ if (!done) return;
2096
+ const t = setTimeout(onDone, 2500);
2097
+ return () => clearTimeout(t);
2098
+ }, [done, onDone]);
2099
+ const finish = (completed, all = answers) => {
2100
+ if (finished.current) return;
2101
+ finished.current = true;
2102
+ api.respond(survey.id, { answers: all, completed, seconds: Math.round((Date.now() - started.current) / 1e3) }).catch(() => void 0);
2103
+ };
2104
+ const dismiss = () => {
2105
+ if (Object.values(answers).some(isAnswered)) finish(false);
2106
+ onDone();
2107
+ };
2108
+ const next = () => {
2109
+ if (q.required && !isAnswered(value)) return setError("Please answer this question.");
2110
+ setError("");
2111
+ if (!last) return setStep(step + 1);
2112
+ finish(true);
2113
+ setDone(true);
2114
+ };
2115
+ const pick = (v) => {
2116
+ setAnswers((a) => ({ ...a, [q.key]: v }));
2117
+ setError("");
2118
+ };
2119
+ let input;
2120
+ if (q.kind === "nps" || q.kind === "rating") {
2121
+ const scale = q.kind === "nps" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);
2122
+ const start = q.kind === "nps" ? 0 : 1;
2123
+ input = (0, import_react2.createElement)(
2124
+ RN3.View,
2125
+ { style: { gap: 6 } },
2126
+ (0, import_react2.createElement)(
2127
+ RN3.View,
2128
+ { style: { flexDirection: "row", flexWrap: "wrap", gap: 6 }, accessibilityRole: "radiogroup", accessibilityLabel: q.title },
2129
+ ...Array.from({ length: scale }, (_, i) => {
2130
+ const n = i + start;
2131
+ const on = value === n;
2132
+ return (0, import_react2.createElement)(
2133
+ RN3.Pressable,
2134
+ {
2135
+ key: n,
2136
+ onPress: () => pick(n),
2137
+ accessibilityRole: "radio",
2138
+ accessibilityState: { checked: on },
2139
+ accessibilityLabel: `${n} of ${scale - 1 + start}`,
2140
+ 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 }
2141
+ },
2142
+ k.text(String(n), { color: on ? "#ffffff" : p.fg, fontWeight: "600" })
2143
+ );
2144
+ })
2145
+ ),
2146
+ 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
2147
+ );
2148
+ } else if (q.kind === "single" || q.kind === "multi") {
2149
+ const multi = q.kind === "multi";
2150
+ const picked = multi ? value ?? [] : [];
2151
+ input = (0, import_react2.createElement)(
2152
+ RN3.View,
2153
+ { style: { gap: 6 } },
2154
+ ...(q.options ?? []).map((o) => {
2155
+ const on = multi ? picked.includes(o) : value === o;
2156
+ return (0, import_react2.createElement)(
2157
+ RN3.Pressable,
2158
+ {
2159
+ key: o,
2160
+ onPress: () => pick(multi ? on ? picked.filter((x) => x !== o) : [...picked, o] : o),
2161
+ accessibilityRole: multi ? "checkbox" : "radio",
2162
+ accessibilityState: { checked: on },
2163
+ style: { minHeight: 44, paddingHorizontal: 12, borderRadius: 10, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
2164
+ },
2165
+ k.text(o, { fontWeight: "500" })
2166
+ );
2167
+ })
2168
+ );
2169
+ } else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? "Type your answer", multiline: true });
2170
+ const body = done ? [k.text(survey.thanks || "Thanks for your feedback.", { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
2171
+ (0, import_react2.createElement)(
2172
+ RN3.View,
2173
+ { key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
2174
+ ...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 } })),
2175
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
2176
+ k.closeX("Close survey", dismiss)
2177
+ ),
2178
+ (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")),
2179
+ q.description ? k.muted(q.description) : null,
2180
+ input,
2181
+ error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2182
+ k.btn(last ? "Send" : "Next", "primary", next)
2183
+ ];
2184
+ return (0, import_react2.createElement)(
2185
+ RN3.Modal,
2186
+ { visible: true, transparent: true, animationType: "slide", onRequestClose: dismiss },
2187
+ (0, import_react2.createElement)(
2188
+ RN3.KeyboardAvoidingView,
2189
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: "rgba(0,0,0,0.35)", justifyContent: "flex-end" } },
2190
+ (0, import_react2.createElement)(
2191
+ RN3.View,
2192
+ {
2193
+ accessibilityViewIsModal: true,
2194
+ accessibilityLabel: survey.name,
2195
+ style: { backgroundColor: p.bg, borderTopLeftRadius: 16, borderTopRightRadius: 16, padding: 20, paddingBottom: 34, gap: 12 }
2196
+ },
2197
+ ...body
2198
+ )
2199
+ )
2200
+ );
2201
+ };
2202
+ var MRRocketSurveys = ({ client, appearance }) => {
2203
+ const p = usePalette(appearance);
2204
+ const [survey, setSurvey] = (0, import_react2.useState)(null);
2205
+ const [who, setWho] = (0, import_react2.useState)(() => client.getDistinctId());
2206
+ (0, import_react2.useEffect)(() => client.subscribe(() => setWho(client.getDistinctId())), [client]);
2207
+ (0, import_react2.useEffect)(() => {
2208
+ let stop = false;
2209
+ const ctrl = new AbortController();
2210
+ const timer = setTimeout(() => {
2211
+ void client.ready.then(() => {
2212
+ const api = client.api();
2213
+ if (stop || !api) return;
2214
+ api.survey("", ctrl.signal).then((r) => {
2215
+ const s = r.survey;
2216
+ if (stop || !s || !s.questions.length || client.isSurveyDone(s.id)) return;
2217
+ setSurvey({ s, api });
2218
+ }).catch(() => void 0);
2219
+ });
2220
+ }, 3e3);
2221
+ return () => {
2222
+ stop = true;
2223
+ clearTimeout(timer);
2224
+ ctrl.abort();
2225
+ };
2226
+ }, [client, who]);
2227
+ const done = (0, import_react2.useCallback)(() => {
2228
+ setSurvey((cur) => {
2229
+ if (cur) client.markSurveyDone(cur.s.id);
2230
+ return null;
2231
+ });
2232
+ }, [client]);
2233
+ if (!survey) return null;
2234
+ return (0, import_react2.createElement)(SurveySheet, { key: survey.s.id, survey: survey.s, api: survey.api, p, onDone: done });
2235
+ };
2236
+ var feedbackOpen = opener();
2237
+ var useFeedback = () => ({ open: () => feedbackOpen.set(true), close: () => feedbackOpen.set(false) });
2238
+ var FeedbackSheet = ({ client, p, title }) => {
2239
+ const k = kit(p);
2240
+ const [tab, setTab] = (0, import_react2.useState)("top");
2241
+ const [posts, setPosts] = (0, import_react2.useState)(null);
2242
+ const [boards, setBoards] = (0, import_react2.useState)([]);
2243
+ const [error, setError] = (0, import_react2.useState)("");
2244
+ const [board, setBoard] = (0, import_react2.useState)("feature");
2245
+ const [subject, setSubject] = (0, import_react2.useState)("");
2246
+ const [details, setDetails] = (0, import_react2.useState)("");
2247
+ const [name, setName] = (0, import_react2.useState)("");
2248
+ const [email, setEmail] = (0, import_react2.useState)("");
2249
+ const [sending, setSending] = (0, import_react2.useState)(false);
2250
+ const [sent, setSent] = (0, import_react2.useState)(false);
2251
+ const close = () => feedbackOpen.set(false);
2252
+ const user = client.getUser();
2253
+ (0, import_react2.useEffect)(() => {
2254
+ const api = client.api();
2255
+ if (!api) return setError("Feedback is off while tracking is off.");
2256
+ const ctrl = new AbortController();
2257
+ api.list({ sort: "top" }, ctrl.signal).then((l) => {
2258
+ setPosts(l.posts);
2259
+ setBoards(l.boards);
2260
+ if (l.boards.length && !l.boards.some((b) => b.id === "feature")) setBoard(l.boards[0].id);
2261
+ }).catch((e) => e.name !== "AbortError" && setError(e.message));
2262
+ return () => ctrl.abort();
2263
+ }, [client]);
2264
+ const vote = (post) => {
2265
+ const api = client.api();
2266
+ if (!api) return;
2267
+ setPosts((all) => all?.map((x) => x.id === post.id ? { ...x, voted: !x.voted, votes: x.votes + (x.voted ? -1 : 1) } : x) ?? null);
2268
+ api.vote(post.id).then((r) => setPosts((all) => all?.map((x) => x.id === post.id ? { ...x, voted: r.voted, votes: r.votes } : x) ?? null)).catch((e) => {
2269
+ setError(e.message);
2270
+ setPosts((all) => all?.map((x) => x.id === post.id ? post : x) ?? null);
2271
+ });
2272
+ };
2273
+ const submit = () => {
2274
+ const api = client.api({ name: name.trim(), email: email.trim() });
2275
+ if (!api || subject.trim().length < 3 || sending) return;
2276
+ setSending(true);
2277
+ setError("");
2278
+ api.submit({ title: subject.trim(), body: details.trim() || void 0, board }).then((post) => {
2279
+ setPosts((all) => [post, ...all ?? []]);
2280
+ setSubject("");
2281
+ setDetails("");
2282
+ setSent(true);
2283
+ }).catch((e) => setError(e.message)).finally(() => setSending(false));
2284
+ };
2285
+ const tabs = (0, import_react2.createElement)(
2286
+ RN3.View,
2287
+ { style: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingTop: 12 } },
2288
+ k.chip("Top requests", tab === "top", () => setTab("top")),
2289
+ k.chip("Suggest", tab === "new", () => {
2290
+ setTab("new");
2291
+ setSent(false);
2292
+ })
2293
+ );
2294
+ const list = (0, import_react2.createElement)(
2295
+ RN3.ScrollView,
2296
+ { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
2297
+ error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2298
+ posts === null && !error ? k.muted("Loading\u2026") : null,
2299
+ posts?.length === 0 ? k.muted("No requests yet. Be the first to suggest one.") : null,
2300
+ ...(posts ?? []).map(
2301
+ (post) => (0, import_react2.createElement)(
2302
+ RN3.View,
2303
+ { key: post.id, style: { flexDirection: "row", gap: 12, alignItems: "flex-start", paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: p.line } },
2304
+ (0, import_react2.createElement)(
2305
+ RN3.Pressable,
2306
+ {
2307
+ onPress: () => vote(post),
2308
+ accessibilityRole: "button",
2309
+ accessibilityState: { selected: post.voted },
2310
+ accessibilityLabel: `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
2311
+ style: { width: 44, minHeight: 48, borderRadius: 10, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: post.voted ? p.accent : p.line }
2312
+ },
2313
+ k.text("\u25B2", { fontSize: 11, color: post.voted ? p.accent : p.muted }),
2314
+ k.text(String(post.votes), { fontWeight: "700", fontSize: 14, color: post.voted ? p.accent : p.fg })
2315
+ ),
2316
+ (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)
2317
+ )
2318
+ )
2319
+ );
2320
+ const form = (0, import_react2.createElement)(
2321
+ RN3.ScrollView,
2322
+ { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 }, keyboardShouldPersistTaps: "handled" },
2323
+ sent ? k.text("Thanks! Your request is on the board.", { fontWeight: "600" }) : null,
2324
+ 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,
2325
+ k.field({ value: subject, onChange: setSubject, placeholder: "Short title", max: 120 }),
2326
+ k.field({ value: details, onChange: setDetails, placeholder: "Details (optional)", multiline: true }),
2327
+ ...user?.email ? [] : [
2328
+ k.muted("Your name and email are optional; they let the team follow up."),
2329
+ k.field({ value: name, onChange: setName, placeholder: "Your name (optional)", max: 100 }),
2330
+ k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional)", max: 200, email: true })
2331
+ ],
2332
+ error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
2333
+ k.btn(sending ? "Sending\u2026" : "Send request", "primary", submit, subject.trim().length < 3 || sending)
2334
+ );
2335
+ return k.page(title, close, [tabs, tab === "top" ? list : form]);
2336
+ };
2337
+ var MRRocketFeedback = ({ client, appearance, options = {} }) => {
2338
+ const p = usePalette(appearance);
2339
+ const open = feedbackOpen.useOpen();
2340
+ return open ? (0, import_react2.createElement)(FeedbackSheet, { client, p, title: options.title ?? "Feedback" }) : null;
2341
+ };
2342
+ var POLL_OPEN_MS = 4e3;
2343
+ var POLL_WAITING_MS = 8e3;
2344
+ var POLL_IDLE_MS = 3e4;
2345
+ var WAITING_FOR_MS = 10 * 6e4;
2346
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
2347
+ var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
2348
+ var msgView = "home";
2349
+ var messengerOpen = opener();
2350
+ var openAt = (v = "home") => {
2351
+ msgView = v;
2352
+ messengerOpen.set(true);
2353
+ };
2354
+ var useMessenger = () => ({
2355
+ open: (view) => openAt(view),
2356
+ close: () => messengerOpen.set(false),
2357
+ toggle: () => messengerOpen.get() ? messengerOpen.set(false) : openAt()
820
2358
  });
2359
+ var haptics = null;
2360
+ try {
2361
+ haptics = require("expo-haptics");
2362
+ } catch {
2363
+ haptics = null;
2364
+ }
2365
+ var buzz = () => {
2366
+ try {
2367
+ void haptics?.notificationAsync?.(haptics.NotificationFeedbackType?.Success)?.catch(() => void 0);
2368
+ } catch {
2369
+ }
2370
+ };
2371
+ var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
2372
+ var useChat = (client, o, open, view) => {
2373
+ const [thread, setThread] = (0, import_react2.useState)(null);
2374
+ const [loaded, setLoaded] = (0, import_react2.useState)(false);
2375
+ const [error, setError] = (0, import_react2.useState)("");
2376
+ const [unseen, setUnseen] = (0, import_react2.useState)([]);
2377
+ const known = (0, import_react2.useRef)(null);
2378
+ const watching = open && view === "messages";
2379
+ const watch = (0, import_react2.useRef)(watching);
2380
+ watch.current = watching;
2381
+ const [active, setActive] = (0, import_react2.useState)(RN3.AppState.currentState === "active");
2382
+ (0, import_react2.useEffect)(() => {
2383
+ const sub = RN3.AppState.addEventListener("change", (st) => setActive(st === "active"));
2384
+ return () => sub.remove();
2385
+ }, []);
2386
+ const take = (0, import_react2.useCallback)(
2387
+ (t) => {
2388
+ const team = teamOf(t);
2389
+ if (known.current !== null) {
2390
+ const i = team.findIndex((m) => m.id === known.current);
2391
+ const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
2392
+ if (fresh.length) {
2393
+ if (o.haptics !== false) buzz();
2394
+ if (!watch.current) setUnseen((u) => [...u, ...fresh]);
2395
+ }
2396
+ }
2397
+ known.current = team.at(-1)?.id ?? "";
2398
+ setThread(t);
2399
+ },
2400
+ [o.haptics]
2401
+ );
2402
+ (0, import_react2.useEffect)(() => {
2403
+ if (watching) setUnseen([]);
2404
+ }, [watching]);
2405
+ (0, import_react2.useEffect)(() => {
2406
+ if (!active) return;
2407
+ let stop = false;
2408
+ let timer;
2409
+ let ctrl = null;
2410
+ const tick = async () => {
2411
+ const api = client.api();
2412
+ if (!api) {
2413
+ setLoaded(true);
2414
+ return;
2415
+ }
2416
+ ctrl = new AbortController();
2417
+ let t = null;
2418
+ try {
2419
+ await client.ready;
2420
+ t = await api.chat(ctrl.signal);
2421
+ if (stop) return;
2422
+ take(t);
2423
+ setError("");
2424
+ } catch (e) {
2425
+ if (e.name === "AbortError" || stop) return;
2426
+ if (watch.current) setError(e.message);
2427
+ }
2428
+ setLoaded(true);
2429
+ const waiting = !!t?.messages.some((m) => m.from === "visitor") && t?.conversation?.status !== "solved";
2430
+ const wrote = (t?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
2431
+ const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
2432
+ const ms = watch.current ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : waiting || open ? POLL_IDLE_MS : 0;
2433
+ if (!stop && ms) timer = setTimeout(tick, ms);
2434
+ };
2435
+ void tick();
2436
+ return () => {
2437
+ stop = true;
2438
+ ctrl?.abort();
2439
+ clearTimeout(timer);
2440
+ };
2441
+ }, [client, take, active, open, watching]);
2442
+ return { thread, loaded, error, setError, unseen, take };
2443
+ };
2444
+ var useArticles = (client, on, query) => {
2445
+ const [list, setList] = (0, import_react2.useState)(null);
2446
+ const [any, setAny] = (0, import_react2.useState)(null);
2447
+ (0, import_react2.useEffect)(() => {
2448
+ if (!on) return;
2449
+ let stop = false;
2450
+ const t = setTimeout(
2451
+ () => void client.ready.then(
2452
+ () => client.api()?.articles(query.trim() || void 0).then((r) => {
2453
+ if (stop) return;
2454
+ setList(r.articles);
2455
+ if (!query.trim()) setAny(r.articles.length > 0);
2456
+ }).catch(() => !stop && setList((l) => l ?? []))
2457
+ ),
2458
+ query ? 250 : 0
2459
+ );
2460
+ return () => {
2461
+ stop = true;
2462
+ clearTimeout(t);
2463
+ };
2464
+ }, [client, on, query]);
2465
+ return { list, any };
2466
+ };
2467
+ var spansOf = (p, spans, base) => spans.map(
2468
+ (s, i) => (0, import_react2.createElement)(
2469
+ RN3.Text,
2470
+ {
2471
+ key: i,
2472
+ onPress: s.href ? () => void RN3.Linking.openURL(s.href).catch(() => void 0) : void 0,
2473
+ accessibilityRole: s.href ? "link" : void 0,
2474
+ style: {
2475
+ ...base,
2476
+ ...s.bold ? { fontWeight: "700" } : {},
2477
+ ...s.italic ? { fontStyle: "italic" } : {},
2478
+ ...s.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
2479
+ ...s.href ? { color: p.accent, textDecorationLine: "underline" } : {}
2480
+ }
2481
+ },
2482
+ s.text
2483
+ )
2484
+ );
2485
+ var ArticleView = ({ client, p, id, onAsk }) => {
2486
+ const k = kit(p);
2487
+ const [a, setA] = (0, import_react2.useState)(null);
2488
+ const [error, setError] = (0, import_react2.useState)("");
2489
+ const [rated, setRated] = (0, import_react2.useState)(false);
2490
+ (0, import_react2.useEffect)(() => {
2491
+ const ctrl = new AbortController();
2492
+ client.api()?.article(id, ctrl.signal).then(setA).catch((e) => e.name !== "AbortError" && setError(e.message));
2493
+ return () => ctrl.abort();
2494
+ }, [client, id]);
2495
+ if (error) return k.text(error, { color: p.danger });
2496
+ if (!a) return k.muted("Loading\u2026");
2497
+ const base = { color: p.fg, fontSize: 15, lineHeight: 22 };
2498
+ const blocks = a.blocks.map((b, i) => {
2499
+ 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, {}));
2500
+ if (b.type === "p") return (0, import_react2.createElement)(RN3.Text, { key: i, style: base }, ...spansOf(p, b.spans, {}));
2501
+ 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, {})));
2502
+ if (b.type === "ul" || b.type === "ol")
2503
+ return (0, import_react2.createElement)(
2504
+ RN3.View,
2505
+ { key: i, style: { gap: 4 } },
2506
+ ...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, {}))))
2507
+ );
2508
+ 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 }));
2509
+ return (0, import_react2.createElement)(RN3.View, { key: i, style: { height: 1, backgroundColor: p.line } });
2510
+ });
2511
+ const rate = (helpful) => {
2512
+ setRated(true);
2513
+ client.api()?.rateArticle(id, helpful).catch(() => void 0);
2514
+ if (!helpful) onAsk();
2515
+ };
2516
+ return (0, import_react2.createElement)(
2517
+ RN3.View,
2518
+ { style: { gap: 10 } },
2519
+ k.muted(a.collection),
2520
+ (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 20, fontWeight: "700" } }, a.title),
2521
+ ...blocks,
2522
+ (0, import_react2.createElement)(
2523
+ RN3.View,
2524
+ { style: { flexDirection: "row", alignItems: "center", gap: 8, padding: 12, borderRadius: 10, backgroundColor: p.soft, marginTop: 8 } },
2525
+ rated ? k.muted("Thanks for letting us know.") : k.muted("Did this answer your question?", { flex: 1 }),
2526
+ rated ? null : k.btn("Yes", "secondary", () => rate(true), false, { height: 34 }),
2527
+ rated ? null : k.btn("No", "secondary", () => rate(false), false, { height: 34 })
2528
+ ),
2529
+ k.btn("Still need help? Send us a message", "secondary", onAsk)
2530
+ );
2531
+ };
2532
+ var MessengerSheet = ({
2533
+ client,
2534
+ p,
2535
+ o,
2536
+ chat,
2537
+ view,
2538
+ setView
2539
+ }) => {
2540
+ const k = kit(p);
2541
+ const [body, setBody] = (0, import_react2.useState)("");
2542
+ const [email, setEmail] = (0, import_react2.useState)("");
2543
+ const [pending, setPending] = (0, import_react2.useState)(null);
2544
+ const [query, setQuery] = (0, import_react2.useState)("");
2545
+ const helpOn = o.help !== false;
2546
+ const arts = useArticles(client, helpOn, query);
2547
+ const scroll = (0, import_react2.useRef)(null);
2548
+ const close = () => messengerOpen.set(false);
2549
+ const greeting = o.greeting ?? "Hi! How can we help?";
2550
+ const { thread } = chat;
2551
+ const count = (thread?.messages.length ?? 0) + (pending ? 1 : 0);
2552
+ (0, import_react2.useEffect)(() => {
2553
+ if (view === "messages") setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 50);
2554
+ }, [count, view]);
2555
+ const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
2556
+ const emailOk = !email.trim() || EMAIL.test(email.trim());
2557
+ const send = async () => {
2558
+ const text2 = body.trim();
2559
+ const api = client.api();
2560
+ if (!text2 || pending || !emailOk || !api) return;
2561
+ setPending({ id: "pending", from: "visitor", body: text2, at: (/* @__PURE__ */ new Date()).toISOString() });
2562
+ setBody("");
2563
+ chat.setError("");
2564
+ try {
2565
+ chat.take(await api.sendChat({ body: text2, email: needEmail && email.trim() ? email.trim() : void 0 }));
2566
+ } catch (e) {
2567
+ setBody(text2);
2568
+ chat.setError(e.message);
2569
+ } finally {
2570
+ setPending(null);
2571
+ }
2572
+ };
2573
+ const bubble = (m, isPending = false) => {
2574
+ const mine = m.from === "visitor";
2575
+ return (0, import_react2.createElement)(
2576
+ RN3.View,
2577
+ { key: m.id, style: { alignSelf: mine ? "flex-end" : "flex-start", maxWidth: "82%", gap: 4 } },
2578
+ (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: mine ? p.accent : p.soft, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8, opacity: isPending ? 0.7 : 1 } }, k.text(m.body, { color: mine ? "#ffffff" : p.fg })),
2579
+ ...(m.articles ?? []).map(
2580
+ (a) => (0, import_react2.createElement)(
2581
+ RN3.Pressable,
2582
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 6 } },
2583
+ k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
2584
+ )
2585
+ ),
2586
+ m.at ? k.muted(`${mine ? "" : `${whoLabel[m.from]} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left" }) : null
2587
+ );
2588
+ };
2589
+ const row = (a) => (0, import_react2.createElement)(
2590
+ RN3.Pressable,
2591
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "button", style: { flexDirection: "row", alignItems: "center", paddingVertical: 11, paddingHorizontal: 4 } },
2592
+ k.text(a.title, { flex: 1 }),
2593
+ k.muted("\u203A", { fontSize: 18 })
2594
+ );
2595
+ const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.bg, borderRadius: 12, padding: 12, borderWidth: 1, borderColor: p.line, ...style } }, ...children);
2596
+ const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200 });
2597
+ const last = thread?.messages.at(-1);
2598
+ const showHelp = helpOn && arts.any !== false;
2599
+ let content;
2600
+ if (view === "home")
2601
+ content = (0, import_react2.createElement)(
2602
+ RN3.ScrollView,
2603
+ { key: "home", style: { flex: 1, backgroundColor: p.soft }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
2604
+ (0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 22, fontWeight: "700", paddingVertical: 6 } }, greeting),
2605
+ (0, import_react2.createElement)(
2606
+ RN3.Pressable,
2607
+ { onPress: () => setView("messages"), accessibilityRole: "button" },
2608
+ card([
2609
+ k.text(last ? "Recent message" : "Send us a message", { fontWeight: "700" }),
2610
+ k.muted(last ? `${last.from === "visitor" ? "You" : whoLabel[last.from]}: ${last.body}` : "We usually answer within a few hours", { marginTop: 2 })
2611
+ ])
2612
+ ),
2613
+ showHelp ? card([(0, import_react2.createElement)(RN3.Pressable, { key: "s", onPress: () => setView("help") }, (0, import_react2.createElement)(RN3.View, { pointerEvents: "none" }, k.muted("\u{1F50D} Search for help", { paddingVertical: 8 }))), ...(arts.list ?? []).slice(0, 4).map(row)]) : null
2614
+ );
2615
+ else if (view === "messages")
2616
+ content = (0, import_react2.createElement)(
2617
+ RN3.View,
2618
+ { key: "messages", style: { flex: 1 } },
2619
+ (0, import_react2.createElement)(
2620
+ RN3.ScrollView,
2621
+ { ref: scroll, style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
2622
+ bubble({ id: "greeting", from: "team", body: greeting, at: "" }),
2623
+ !chat.loaded ? k.muted("Loading\u2026") : null,
2624
+ ...(thread?.messages ?? []).map((m) => bubble(m)),
2625
+ pending ? bubble(pending, true) : null,
2626
+ thread?.conversation?.status === "solved" && !pending ? k.muted("This conversation is closed. Write to start a new one.", { textAlign: "center" }) : null
2627
+ ),
2628
+ (0, import_react2.createElement)(
2629
+ RN3.View,
2630
+ { style: { padding: 12, gap: 8, borderTopWidth: 1, borderTopColor: p.line } },
2631
+ chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
2632
+ needEmail ? k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional), so we can follow up", email: true, max: 200, style: emailOk ? {} : { borderColor: p.danger } }) : null,
2633
+ (0, import_react2.createElement)(
2634
+ RN3.View,
2635
+ { style: { flexDirection: "row", gap: 8, alignItems: "flex-end" } },
2636
+ k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { flex: 1, minHeight: 44, maxHeight: 120 } }),
2637
+ k.btn("Send", "primary", () => void send(), !body.trim() || !!pending || !emailOk)
2638
+ )
2639
+ )
2640
+ );
2641
+ else if (view === "help") {
2642
+ const groups = /* @__PURE__ */ new Map();
2643
+ for (const a of arts.list ?? []) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
2644
+ content = (0, import_react2.createElement)(
2645
+ RN3.ScrollView,
2646
+ { key: "help", style: { flex: 1, backgroundColor: p.soft }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
2647
+ search,
2648
+ arts.list === null ? k.muted("Loading\u2026") : null,
2649
+ 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,
2650
+ ...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row))] : [] : [...groups].map(([name, items]) => (0, import_react2.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`), card(items.map(row))))
2651
+ );
2652
+ } else
2653
+ content = (0, import_react2.createElement)(
2654
+ RN3.ScrollView,
2655
+ { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { padding: 16, paddingBottom: 32 } },
2656
+ (0, import_react2.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
2657
+ );
2658
+ const current = typeof view === "string" ? view : "help";
2659
+ const tab = (v, name, glyph, badge = 0) => (0, import_react2.createElement)(
2660
+ RN3.Pressable,
2661
+ { key: v, onPress: () => setView(v), accessibilityRole: "tab", accessibilityState: { selected: current === v }, style: { flex: 1, alignItems: "center", paddingTop: 8, paddingBottom: 6, gap: 2 } },
2662
+ k.text(glyph, { fontSize: 18, color: current === v ? p.fg : p.muted }),
2663
+ k.text(name, { fontSize: 12, fontWeight: "600", color: current === v ? p.fg : p.muted }),
2664
+ badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 4, right: "30%", minWidth: 18, height: 18, borderRadius: 9, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(Math.min(badge, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
2665
+ );
2666
+ const titleOf = typeof view !== "string" || view === "help" ? "Help" : view === "messages" ? o.title ?? "Chat with us" : o.title ?? "Chat with us";
2667
+ return k.page(titleOf, close, [
2668
+ typeof view !== "string" ? (0, import_react2.createElement)(RN3.Pressable, { key: "back", onPress: () => setView("help"), accessibilityRole: "button", style: { paddingHorizontal: 16, paddingVertical: 8 } }, k.text("\u2039 All articles", { color: p.accent, fontWeight: "600" })) : null,
2669
+ content,
2670
+ (0, import_react2.createElement)(
2671
+ RN3.View,
2672
+ { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", borderTopWidth: 1, borderTopColor: p.line, paddingBottom: 20, backgroundColor: p.bg } },
2673
+ tab("home", "Home", "\u2302"),
2674
+ tab("messages", "Messages", "\u2709\uFE0E", view === "messages" ? 0 : chat.unseen.length),
2675
+ showHelp ? tab("help", "Help", "?") : null
2676
+ )
2677
+ ]);
2678
+ };
2679
+ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
2680
+ const [look, setLook] = (0, import_react2.useState)(null);
2681
+ (0, import_react2.useEffect)(() => {
2682
+ const ctrl = new AbortController();
2683
+ void client.ready.then(
2684
+ () => client.api()?.messengerLook(ctrl.signal).then(setLook).catch(() => void 0)
2685
+ );
2686
+ return () => ctrl.abort();
2687
+ }, [client]);
2688
+ const defined = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== void 0));
2689
+ const options = look ? { title: look.title, greeting: look.greeting, help: look.help, preview: look.preview, askEmail: look.askEmail, ...defined } : given;
2690
+ const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
2691
+ const k = kit(p);
2692
+ const open = messengerOpen.useOpen();
2693
+ const [view, setViewState] = (0, import_react2.useState)(msgView);
2694
+ const [banner, setBanner] = (0, import_react2.useState)(null);
2695
+ (0, import_react2.useEffect)(() => {
2696
+ if (open) setViewState(msgView);
2697
+ }, [open]);
2698
+ const setView = (v) => {
2699
+ msgView = v;
2700
+ setViewState(v);
2701
+ };
2702
+ const chat = useChat(client, options, open, view);
2703
+ const latest = chat.unseen.at(-1);
2704
+ (0, import_react2.useEffect)(() => {
2705
+ if (!latest || open || options.preview === false) return;
2706
+ setBanner(latest);
2707
+ const t = setTimeout(() => setBanner(null), 6e3);
2708
+ return () => clearTimeout(t);
2709
+ }, [latest, open, options.preview]);
2710
+ (0, import_react2.useEffect)(() => {
2711
+ if (open) setBanner(null);
2712
+ }, [open]);
2713
+ const unseen = chat.unseen.length;
2714
+ const launcher = options.launcher ? (0, import_react2.createElement)(
2715
+ RN3.Pressable,
2716
+ {
2717
+ key: "launcher",
2718
+ onPress: () => openAt(unseen ? "messages" : "home"),
2719
+ accessibilityRole: "button",
2720
+ accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
2721
+ style: { position: "absolute", right: 20, bottom: 34, width: 54, height: 54, borderRadius: 27, backgroundColor: p.accent, alignItems: "center", justifyContent: "center", shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, elevation: 6 }
2722
+ },
2723
+ k.text("\u{1F4AC}", { fontSize: 22 }),
2724
+ 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
2725
+ ) : null;
2726
+ const toast = banner ? (0, import_react2.createElement)(
2727
+ RN3.Pressable,
2728
+ {
2729
+ key: "banner",
2730
+ onPress: () => openAt("messages"),
2731
+ accessibilityRole: "button",
2732
+ accessibilityLiveRegion: "polite",
2733
+ style: { position: "absolute", top: 54, left: 16, right: 16, backgroundColor: p.bg, borderRadius: 14, padding: 14, gap: 2, borderWidth: 1, borderColor: p.line, shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 16, elevation: 8 }
2734
+ },
2735
+ k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
2736
+ (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15 } }, banner.body)
2737
+ ) : null;
2738
+ if (!launcher && !toast && !open) return null;
2739
+ return (0, import_react2.createElement)(
2740
+ RN3.View,
2741
+ { pointerEvents: "box-none", style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 } },
2742
+ launcher,
2743
+ toast,
2744
+ open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView }) : null
2745
+ );
2746
+ };
821
2747
 
822
2748
  // src/native/index.tsx
2749
+ var RN4 = require("react-native");
2750
+ var MRRocketMask = ({ children, style }) => {
2751
+ const ref = (0, import_react3.useRef)(null);
2752
+ (0, import_react3.useEffect)(() => {
2753
+ maskedViews.add(ref);
2754
+ return () => {
2755
+ maskedViews.delete(ref);
2756
+ };
2757
+ }, []);
2758
+ return (0, import_react3.createElement)(RN4.View, { ref, collapsable: false, style }, children);
2759
+ };
823
2760
  var shared = null;
824
2761
  var sharedKey = "";
825
2762
  var createMRRocket = (options) => {
@@ -835,20 +2772,40 @@ var createMRRocket = (options) => {
835
2772
  return shared;
836
2773
  };
837
2774
  var getMRRocket = () => shared;
838
- var Ctx = (0, import_react.createContext)(null);
839
- var MRRocketProvider = ({ client: given, children, consent, ...options }) => {
840
- const [client] = (0, import_react.useState)(() => given ?? createMRRocket({ ...options, consent }));
841
- (0, import_react.useEffect)(() => {
2775
+ var Ctx = (0, import_react3.createContext)(null);
2776
+ var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, surveys = true, messenger = {}, feedback = {}, heatmaps = true, ...options }) => {
2777
+ const [client] = (0, import_react3.useState)(() => given ?? createMRRocket({ ...options, consent }));
2778
+ (0, import_react3.useEffect)(() => {
842
2779
  if (consent) client.setConsent(consent);
843
2780
  }, [client, consent]);
844
- return (0, import_react.createElement)(Ctx.Provider, { value: client }, children);
2781
+ (0, import_react3.useEffect)(() => {
2782
+ if (options.replay !== void 0) client.setReplay(options.replay);
2783
+ }, [client, options.replay]);
2784
+ const start = (0, import_react3.useRef)(null);
2785
+ const touch = (e, end) => {
2786
+ const n = e.nativeEvent;
2787
+ const x = n.pageX ?? n.changedTouches?.[0]?.pageX;
2788
+ const y = n.pageY ?? n.changedTouches?.[0]?.pageY;
2789
+ if (x === void 0 || y === void 0) return;
2790
+ if (!end) start.current = { x, y, t: Date.now() };
2791
+ 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);
2792
+ };
2793
+ return (0, import_react3.createElement)(
2794
+ Ctx.Provider,
2795
+ { value: client },
2796
+ heatmaps ? (0, import_react3.createElement)(RN4.View, { style: { flex: 1 }, onTouchStartCapture: (e) => touch(e, false), onTouchEndCapture: (e) => touch(e, true) }, children) : children,
2797
+ flows ? (0, import_react3.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null,
2798
+ surveys ? (0, import_react3.createElement)(MRRocketSurveys, { client, appearance }) : null,
2799
+ feedback ? (0, import_react3.createElement)(MRRocketFeedback, { client, appearance, options: feedback }) : null,
2800
+ messenger ? (0, import_react3.createElement)(MRRocketMessenger, { client, appearance, options: messenger }) : null
2801
+ );
845
2802
  };
846
2803
  var useClient = () => {
847
- const c = (0, import_react.useContext)(Ctx);
2804
+ const c = (0, import_react3.useContext)(Ctx);
848
2805
  if (!c) throw new Error("MRRocket: wrap your app in <MRRocketProvider apiKey=\u2026>.");
849
2806
  return c;
850
2807
  };
851
- var useVersion = (client) => (0, import_react.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
2808
+ var useVersion = (client) => (0, import_react3.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
852
2809
  var useMRRocket = () => {
853
2810
  const client = useClient();
854
2811
  useVersion(client);
@@ -858,6 +2815,7 @@ var useMRRocket = () => {
858
2815
  identify: client.identify,
859
2816
  reset: client.reset,
860
2817
  setConsent: client.setConsent,
2818
+ setReplay: client.setReplay,
861
2819
  captureError: client.captureError,
862
2820
  flush: client.flush,
863
2821
  distinctId: client.getDistinctId(),
@@ -865,6 +2823,18 @@ var useMRRocket = () => {
865
2823
  ready: client.isReady()
866
2824
  };
867
2825
  };
2826
+ var useCancelFlow = () => {
2827
+ const client = useClient();
2828
+ return { open: client.openCancelFlow };
2829
+ };
2830
+ var usePush = () => {
2831
+ const client = useClient();
2832
+ return { register: client.registerForPush, unregister: client.unregisterForPush };
2833
+ };
2834
+ var useOnboarding = () => {
2835
+ const client = useClient();
2836
+ return { start: client.startOnboarding };
2837
+ };
868
2838
  var useFlag = (key) => {
869
2839
  const client = useClient();
870
2840
  useVersion(client);
@@ -880,11 +2850,22 @@ var useVariant = (key) => {
880
2850
  useVersion(client);
881
2851
  const config = client.getConfig();
882
2852
  const variant = client.peekVariant(key);
883
- (0, import_react.useEffect)(() => {
2853
+ (0, import_react3.useEffect)(() => {
884
2854
  if (config) client.expose(key, variant);
885
2855
  }, [client, config, key, variant]);
886
2856
  return variant;
887
2857
  };
2858
+ var useText = (id, fallback) => {
2859
+ const client = useClient();
2860
+ useVersion(client);
2861
+ const config = client.getConfig();
2862
+ const text2 = client.peekText(id);
2863
+ const person = client.getDistinctId();
2864
+ (0, import_react3.useEffect)(() => {
2865
+ if (config) client.exposeText(id);
2866
+ }, [client, config, id, person]);
2867
+ return text2 ?? fallback;
2868
+ };
888
2869
  var expoRouter;
889
2870
  var loadExpoRouter = () => {
890
2871
  if (expoRouter !== void 0) return expoRouter;
@@ -905,13 +2886,13 @@ var useExpoRouterTracking = () => {
905
2886
  console.warn("[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.");
906
2887
  }
907
2888
  const pathname = (router?.usePathname ?? noPathname)();
908
- (0, import_react.useEffect)(() => {
2889
+ (0, import_react3.useEffect)(() => {
909
2890
  if (pathname) client.screen(pathname);
910
2891
  }, [client, pathname]);
911
2892
  };
912
2893
  var useNavigationTracking = () => {
913
2894
  const client = useClient();
914
- return (0, import_react.useCallback)(
2895
+ return (0, import_react3.useCallback)(
915
2896
  (state) => {
916
2897
  const name = activeRouteName(state);
917
2898
  if (name) client.screen(name);