@mrrocket/sdk 0.4.0 → 0.8.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 (53) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +102 -7
  3. package/dist/api.d.ts +23 -3
  4. package/dist/api.js +7 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/browser/mrrocket-flows.js +64 -0
  7. package/dist/browser/mrrocket-push.js +1 -0
  8. package/dist/browser/mrrocket-replay.js +3 -3
  9. package/dist/browser/mrrocket-sw.js +1 -0
  10. package/dist/browser/mrrocket.js +2 -1
  11. package/dist/browser.d.ts +1 -1
  12. package/dist/browser.js +3 -1
  13. package/dist/browser.js.map +1 -1
  14. package/dist/client.d.ts +36 -1
  15. package/dist/client.js +161 -6
  16. package/dist/client.js.map +1 -1
  17. package/dist/context.d.ts +10 -0
  18. package/dist/context.js.map +1 -1
  19. package/dist/core.d.ts +64 -3
  20. package/dist/core.js +54 -2
  21. package/dist/core.js.map +1 -1
  22. package/dist/feedback.d.ts +19 -4
  23. package/dist/feedback.js +652 -190
  24. package/dist/feedback.js.map +1 -1
  25. package/dist/flows-ui.d.ts +23 -0
  26. package/dist/flows-ui.js +384 -0
  27. package/dist/flows-ui.js.map +1 -0
  28. package/dist/flows-web.d.ts +19 -0
  29. package/dist/flows-web.js +42 -0
  30. package/dist/flows-web.js.map +1 -0
  31. package/dist/flows.d.ts +222 -0
  32. package/dist/flows.js +409 -0
  33. package/dist/flows.js.map +1 -0
  34. package/dist/index.d.ts +5 -2
  35. package/dist/index.js +9 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/proxy.d.ts +34 -1
  38. package/dist/proxy.js +14 -1
  39. package/dist/proxy.js.map +1 -1
  40. package/dist/push-web.d.ts +43 -0
  41. package/dist/push-web.js +81 -0
  42. package/dist/push-web.js.map +1 -0
  43. package/dist/react-native/index.d.cts +305 -3
  44. package/dist/react-native/index.d.ts +305 -3
  45. package/dist/react-native/index.js +852 -31
  46. package/dist/react-native/index.js.map +1 -1
  47. package/dist/styles.d.ts +1 -1
  48. package/dist/styles.js +127 -24
  49. package/dist/styles.js.map +1 -1
  50. package/dist/ui.d.ts +3 -0
  51. package/dist/ui.js +12 -0
  52. package/dist/ui.js.map +1 -1
  53. package/package.json +10 -2
@@ -20,23 +20,28 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/native/index.tsx
21
21
  var native_exports = {};
22
22
  __export(native_exports, {
23
+ MRRocketFlows: () => MRRocketFlows,
23
24
  MRRocketProvider: () => MRRocketProvider,
24
25
  SDK_VERSION: () => SDK_VERSION,
25
26
  activeRouteName: () => activeRouteName,
26
27
  createMRRocket: () => createMRRocket,
27
28
  getMRRocket: () => getMRRocket,
29
+ useCancelFlow: () => useCancelFlow,
28
30
  useExpoRouterTracking: () => useExpoRouterTracking,
29
31
  useFlag: () => useFlag,
30
32
  useFlagVariant: () => useFlagVariant,
31
33
  useMRRocket: () => useMRRocket,
32
34
  useNavigationTracking: () => useNavigationTracking,
35
+ useOnboarding: () => useOnboarding,
36
+ usePush: () => usePush,
37
+ useText: () => useText,
33
38
  useVariant: () => useVariant
34
39
  });
35
40
  module.exports = __toCommonJS(native_exports);
36
- var import_react = require("react");
41
+ var import_react2 = require("react");
37
42
 
38
43
  // src/core.ts
39
- var SDK_VERSION = "0.4.0";
44
+ var SDK_VERSION = "0.8.0";
40
45
  var propOf = (person, name) => {
41
46
  const want = name.trim().toLowerCase();
42
47
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -109,25 +114,25 @@ var parseQuery = (url) => {
109
114
  return out;
110
115
  };
111
116
  var hashUnit = (input) => {
112
- let h = 2166136261;
117
+ let h2 = 2166136261;
113
118
  for (let i = 0; i < input.length; i++) {
114
- h ^= input.charCodeAt(i);
115
- h = Math.imul(h, 16777619);
119
+ h2 ^= input.charCodeAt(i);
120
+ h2 = Math.imul(h2, 16777619);
116
121
  }
117
- return (h >>> 0) / 4294967296;
122
+ return (h2 >>> 0) / 4294967296;
118
123
  };
119
124
  var flagUnit = (input) => {
120
- let h = 2166136261;
125
+ let h2 = 2166136261;
121
126
  for (let i = 0; i < input.length; i++) {
122
- h ^= input.charCodeAt(i);
123
- h = Math.imul(h, 16777619);
127
+ h2 ^= input.charCodeAt(i);
128
+ h2 = Math.imul(h2, 16777619);
124
129
  }
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;
130
+ h2 ^= h2 >>> 16;
131
+ h2 = Math.imul(h2, 2246822507);
132
+ h2 ^= h2 >>> 13;
133
+ h2 = Math.imul(h2, 3266489909);
134
+ h2 ^= h2 >>> 16;
135
+ return (h2 >>> 0) / 4294967296;
131
136
  };
132
137
  var assign = (experiment, distinctId) => {
133
138
  const control = experiment.variants[0]?.id ?? "control";
@@ -141,11 +146,21 @@ var assign = (experiment, distinctId) => {
141
146
  }
142
147
  return experiment.variants[experiment.variants.length - 1]?.id ?? control;
143
148
  };
149
+ var experimentsOfText = (config, id) => (config?.experiments ?? []).filter((e) => e.variants.some((v) => v.changes && id in v.changes)).map((e) => e.key);
144
150
  var resolveVariant = (key, config, distinctId, preview) => {
145
151
  if (preview[key]) return preview[key];
146
152
  const experiment = config?.experiments.find((e) => e.key === key);
147
153
  return experiment ? assign(experiment, distinctId) : "control";
148
154
  };
155
+ var textChanges = (config, distinctId, preview) => {
156
+ const out = { ...config?.texts ?? {} };
157
+ for (const experiment of config?.experiments ?? []) {
158
+ const id = resolveVariant(experiment.key, config, distinctId, preview);
159
+ if (id === (experiment.variants[0]?.id ?? "control")) continue;
160
+ Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});
161
+ }
162
+ return out;
163
+ };
149
164
  var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
150
165
 
151
166
  // src/batch.ts
@@ -203,6 +218,400 @@ var describeError = (error) => {
203
218
  }
204
219
  };
205
220
 
221
+ // src/flows.ts
222
+ var CANCEL_EVENT = "$cancel_flow";
223
+ var ONBOARDING_EVENT = "$onboarding";
224
+ var INAPP_EVENT = "$inapp";
225
+ var PLAIN_CANCEL = {
226
+ version: 0,
227
+ question: "",
228
+ reasons: [],
229
+ offers: {},
230
+ confirm: { title: "Cancel your subscription?", body: "You keep access until the end of the billing period.", button: "Cancel subscription" },
231
+ offersFor: "business"
232
+ };
233
+ var FAILED = "Something went wrong. Please try again.";
234
+ var PROGRESS_KEY = "mrrocket:onboarding";
235
+ var eventSlug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
236
+ var SIGNUP = /* @__PURE__ */ new Set(["signed_up", "signup", "sign_up", "registered", "account_created", "user_signed_up"]);
237
+ var matchesTrigger = (trigger, event) => {
238
+ const t = eventSlug(trigger);
239
+ const e = eventSlug(event);
240
+ if (!t || !e) return false;
241
+ return t === e || SIGNUP.has(t) && SIGNUP.has(e);
242
+ };
243
+ 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());
244
+ var runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
245
+ var discounted = (amount, percent) => Math.round(amount * (1 - percent / 100) * 100) / 100;
246
+ var money = (amount, currency) => {
247
+ try {
248
+ return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
249
+ } catch {
250
+ return `${amount.toFixed(2)} ${currency}`;
251
+ }
252
+ };
253
+ var offerButton = (o) => o.kind === "contact" ? "Send to the team" : o.kind === "pause" ? "Pause instead" : o.kind === "downgrade" ? "Switch plan" : "Accept offer";
254
+ var safeUrl = (url) => {
255
+ const u = (url ?? "").trim();
256
+ return /^https?:\/\//i.test(u) || /^\/(?!\/)/.test(u) ? u : void 0;
257
+ };
258
+ var createFlows = (env) => {
259
+ let config = null;
260
+ let cancel = null;
261
+ let active = null;
262
+ let messages = [];
263
+ let known = false;
264
+ let loading = null;
265
+ let loadedFor;
266
+ let progressFor = null;
267
+ const progress = /* @__PURE__ */ new Map();
268
+ const handled = /* @__PURE__ */ new Set();
269
+ const shown = /* @__PURE__ */ new Set();
270
+ const listeners = /* @__PURE__ */ new Set();
271
+ const auto = env.auto !== false;
272
+ const flows = () => config?.onboarding ?? [];
273
+ const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
274
+ const prog = (id) => {
275
+ let p = progress.get(id);
276
+ if (!p) progress.set(id, p = { done: /* @__PURE__ */ new Set(), dismissed: false });
277
+ return p;
278
+ };
279
+ const finished = (f) => {
280
+ const p = progress.get(f.id);
281
+ return !!p && (p.dismissed || f.steps.every((s) => p.done.has(s.id)));
282
+ };
283
+ const keyOf = (pid) => `${PROGRESS_KEY}:${pid ?? "visitor"}`;
284
+ const save = () => {
285
+ const store = env.storage?.();
286
+ if (!store) return;
287
+ const out = {};
288
+ for (const [id, p] of progress) out[id] = { done: [...p.done], ...p.dismissed ? { dismissed: true } : {} };
289
+ try {
290
+ store.set(keyOf(env.personId()), JSON.stringify(out));
291
+ } catch {
292
+ }
293
+ };
294
+ const merge = (state) => {
295
+ for (const [id, s] of Object.entries(state ?? {})) {
296
+ const p = prog(id);
297
+ for (const d of s.done ?? []) p.done.add(d);
298
+ if (s.dismissed) p.dismissed = true;
299
+ }
300
+ };
301
+ const onboardingView = () => {
302
+ const flow = flowById(active);
303
+ if (!flow) return null;
304
+ const p = prog(flow.id);
305
+ const at = flow.steps.findIndex((s) => !p.done.has(s.id));
306
+ if (at < 0 || p.dismissed) return null;
307
+ const step = flow.steps[at];
308
+ if (step.kind === "checklist") {
309
+ let from = at;
310
+ while (from > 0 && flow.steps[from - 1].kind === "checklist") from--;
311
+ let to = at;
312
+ while (to + 1 < flow.steps.length && flow.steps[to + 1].kind === "checklist") to++;
313
+ return { type: "checklist", flow, items: flow.steps.slice(from, to + 1).map((s) => ({ step: s, done: p.done.has(s.id) })) };
314
+ }
315
+ return { type: "step", flow, step, index: at, total: flow.steps.length, last: at === flow.steps.length - 1 };
316
+ };
317
+ const view = () => {
318
+ if (cancel) {
319
+ const offer = cancel.reason ? cancel.flow.offers[cancel.reason] : void 0;
320
+ return {
321
+ type: "cancel",
322
+ screen: cancel.screen,
323
+ flow: cancel.flow,
324
+ reason: cancel.reason,
325
+ offer: cancel.screen === "offer" ? offer : void 0,
326
+ price: cancel.options.price,
327
+ busy: cancel.busy,
328
+ error: cancel.error,
329
+ step: cancel.plain ? 1 : cancel.screen === "survey" ? 1 : cancel.screen === "offer" ? 2 : 3,
330
+ steps: cancel.plain ? 1 : 3
331
+ };
332
+ }
333
+ const ob = onboardingView();
334
+ if (ob) return ob;
335
+ if (active) active = null;
336
+ const m = messages.find((x) => !handled.has(x.id));
337
+ return m ? { type: "message", message: m } : null;
338
+ };
339
+ const emit = () => {
340
+ const v = view();
341
+ if (v?.type === "step" && !shown.has(`s:${v.step.id}`)) {
342
+ shown.add(`s:${v.step.id}`);
343
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "shown" });
344
+ } else if (v?.type === "checklist") {
345
+ for (const i of v.items)
346
+ if (!i.done && !shown.has(`s:${i.step.id}`)) {
347
+ shown.add(`s:${i.step.id}`);
348
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: i.step.id, kind: "checklist", action: "shown" });
349
+ }
350
+ } else if (v?.type === "message" && !shown.has(`m:${v.message.id}`)) {
351
+ shown.add(`m:${v.message.id}`);
352
+ env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "shown" });
353
+ }
354
+ for (const l of [...listeners]) {
355
+ try {
356
+ l();
357
+ } catch {
358
+ }
359
+ }
360
+ };
361
+ const start = (flow) => {
362
+ if (active === flow.id) return true;
363
+ if (active && flowById(active) && !finished(flowById(active))) return false;
364
+ active = flow.id;
365
+ env.track(ONBOARDING_EVENT, { flow: flow.id, action: "started" });
366
+ emit();
367
+ return true;
368
+ };
369
+ const autoStart = () => {
370
+ if (!auto || active || !known) return;
371
+ if (!env.personId() && !env.storage?.()) return;
372
+ const next = flows().find((f) => startsOnLoad(f.trigger) && f.steps.length && !finished(f));
373
+ if (next) start(next);
374
+ };
375
+ const refresh = () => {
376
+ const pid = env.personId();
377
+ if (progressFor !== null && progressFor !== pid) {
378
+ progress.clear();
379
+ handled.clear();
380
+ active = null;
381
+ loadedFor = void 0;
382
+ }
383
+ progressFor = pid;
384
+ const run = (async () => {
385
+ const store = env.storage?.();
386
+ if (store) {
387
+ try {
388
+ const raw = await store.get(keyOf(pid));
389
+ if (raw) merge(JSON.parse(raw));
390
+ } catch {
391
+ }
392
+ }
393
+ const wants = flows().length > 0 || !!config?.messages;
394
+ if (pid && wants && loadedFor !== pid) {
395
+ loadedFor = pid;
396
+ const state = await env.fetchInApp(pid).catch(() => null);
397
+ if (state && env.personId() === pid) {
398
+ merge(state.onboarding);
399
+ messages = (state.messages ?? []).filter((m) => m && m.id && m.title);
400
+ }
401
+ }
402
+ if (!pid) messages = [];
403
+ known = true;
404
+ autoStart();
405
+ emit();
406
+ })();
407
+ loading = run.finally(() => {
408
+ if (loading === run) loading = null;
409
+ });
410
+ return loading;
411
+ };
412
+ const result = (c, outcome, comment) => {
413
+ const offer = c.reason ? c.flow.offers[c.reason] : void 0;
414
+ return {
415
+ outcome,
416
+ run: c.run,
417
+ ...c.reason ? { reason: c.reason, reasonLabel: c.flow.reasons.find((r) => r.id === c.reason)?.label } : {},
418
+ ...outcome === "saved" && offer ? { offer } : {},
419
+ ...comment?.trim() ? { comment: comment.trim().slice(0, 500) } : {}
420
+ };
421
+ };
422
+ const finish = (c, r) => {
423
+ env.track(CANCEL_EVENT, {
424
+ action: "finished",
425
+ run: c.run,
426
+ version: c.flow.version,
427
+ outcome: r.outcome,
428
+ customer: c.options.customer ?? "consumer",
429
+ ...r.reason ? { reason: r.reason } : {},
430
+ ...r.offer ? { offer: r.offer.kind } : {},
431
+ ...r.comment ? { comment: r.comment } : {}
432
+ });
433
+ if (cancel === c) cancel = null;
434
+ c.resolve(r);
435
+ emit();
436
+ };
437
+ const settle = (c, outcome, comment) => {
438
+ if (c.busy) return;
439
+ const r = result(c, outcome, comment);
440
+ const fn = outcome === "saved" ? c.options.onSave : c.options.onCancel;
441
+ c.busy = true;
442
+ c.error = void 0;
443
+ emit();
444
+ Promise.resolve().then(() => fn?.(r)).then(
445
+ () => finish(c, r),
446
+ () => {
447
+ c.busy = false;
448
+ c.error = FAILED;
449
+ emit();
450
+ }
451
+ );
452
+ };
453
+ const openCancelFlow = (options = {}) => {
454
+ if (cancel) return cancel.promise;
455
+ const flow = config?.cancelFlow ?? PLAIN_CANCEL;
456
+ const plain = options.customer !== "business" || flow.reasons.length < 2;
457
+ let resolve = () => {
458
+ };
459
+ const promise = new Promise((r) => resolve = r);
460
+ const c = { flow, options, plain, screen: plain ? "confirm" : "survey", run: runId(), busy: false, resolve, promise };
461
+ cancel = c;
462
+ env.track(CANCEL_EVENT, { action: "shown", run: c.run, version: flow.version, customer: options.customer ?? "consumer", plain });
463
+ emit();
464
+ return promise;
465
+ };
466
+ const controller = {
467
+ setConfig: (c) => {
468
+ config = c;
469
+ if (active && !flowById(active)) active = null;
470
+ emit();
471
+ },
472
+ refresh,
473
+ onEvent: (name) => {
474
+ if (!name || name.startsWith("$")) return;
475
+ const flow = flowById(active);
476
+ let ticked = false;
477
+ if (flow) {
478
+ const p = prog(flow.id);
479
+ for (const s of flow.steps)
480
+ if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
481
+ p.done.add(s.id);
482
+ ticked = true;
483
+ env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: "completed" });
484
+ }
485
+ }
486
+ if (ticked) {
487
+ save();
488
+ emit();
489
+ }
490
+ if (!auto || !env.personId() && !env.storage?.()) return;
491
+ const next = flows().find((f) => f.steps.length && matchesTrigger(f.trigger, name) && !finished(f));
492
+ if (next) start(next);
493
+ },
494
+ openCancelFlow,
495
+ startOnboarding: (id) => {
496
+ const f = id ? flowById(id) : flows().find((x) => x.steps.length && !finished(x));
497
+ if (!f || !f.steps.length) return false;
498
+ const p = prog(f.id);
499
+ p.dismissed = false;
500
+ return start(f);
501
+ },
502
+ view,
503
+ subscribe: (l) => {
504
+ listeners.add(l);
505
+ return () => listeners.delete(l);
506
+ },
507
+ cancel: {
508
+ pick: (reason) => {
509
+ if (!cancel || cancel.busy || !cancel.flow.reasons.some((r) => r.id === reason)) return;
510
+ cancel.reason = reason;
511
+ cancel.error = void 0;
512
+ emit();
513
+ },
514
+ next: () => {
515
+ const c = cancel;
516
+ if (!c || c.screen !== "survey" || !c.reason) return;
517
+ env.track(CANCEL_EVENT, { action: "answered", run: c.run, version: c.flow.version, reason: c.reason });
518
+ c.screen = c.flow.offers[c.reason] ? "offer" : "confirm";
519
+ emit();
520
+ },
521
+ accept: (comment) => {
522
+ const c = cancel;
523
+ if (!c || c.screen !== "offer" || !c.reason) return;
524
+ const offer = c.flow.offers[c.reason];
525
+ if (!c.busy)
526
+ env.track(CANCEL_EVENT, {
527
+ action: "offer",
528
+ run: c.run,
529
+ version: c.flow.version,
530
+ reason: c.reason,
531
+ offer: offer?.kind,
532
+ outcome: "accepted"
533
+ });
534
+ settle(c, "saved", comment);
535
+ },
536
+ decline: () => {
537
+ const c = cancel;
538
+ if (!c || c.screen !== "offer" || c.busy) return;
539
+ env.track(CANCEL_EVENT, {
540
+ action: "offer",
541
+ run: c.run,
542
+ version: c.flow.version,
543
+ reason: c.reason,
544
+ offer: c.reason ? c.flow.offers[c.reason]?.kind : void 0,
545
+ outcome: "declined"
546
+ });
547
+ c.screen = "confirm";
548
+ c.error = void 0;
549
+ emit();
550
+ },
551
+ confirm: (comment) => {
552
+ const c = cancel;
553
+ if (!c || c.screen !== "confirm") return;
554
+ settle(c, "cancelled", comment);
555
+ },
556
+ close: () => {
557
+ const c = cancel;
558
+ if (!c || c.busy) return;
559
+ const r = result(c, "kept");
560
+ c.options.onClose?.(r);
561
+ finish(c, r);
562
+ }
563
+ },
564
+ onboarding: {
565
+ next: () => {
566
+ const v = onboardingView();
567
+ if (v?.type !== "step") return;
568
+ prog(v.flow.id).done.add(v.step.id);
569
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
570
+ save();
571
+ emit();
572
+ },
573
+ check: (stepId) => {
574
+ const v = onboardingView();
575
+ if (v?.type !== "checklist") return;
576
+ const item = v.items.find((i) => i.step.id === stepId);
577
+ if (!item || item.done) return;
578
+ prog(v.flow.id).done.add(stepId);
579
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
580
+ save();
581
+ emit();
582
+ },
583
+ dismiss: () => {
584
+ const v = onboardingView();
585
+ if (!v || v.type !== "step" && v.type !== "checklist") return;
586
+ prog(v.flow.id).dismissed = true;
587
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, ...v.type === "step" ? { step: v.step.id } : {}, action: "dismissed" });
588
+ active = null;
589
+ save();
590
+ emit();
591
+ }
592
+ },
593
+ message: {
594
+ click: () => {
595
+ const v = view();
596
+ if (v?.type !== "message") return;
597
+ handled.add(v.message.id);
598
+ env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "clicked" });
599
+ const url = safeUrl(v.message.url);
600
+ emit();
601
+ if (url) env.openUrl?.(url);
602
+ },
603
+ dismiss: () => {
604
+ const v = view();
605
+ if (v?.type !== "message") return;
606
+ handled.add(v.message.id);
607
+ env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "dismissed" });
608
+ emit();
609
+ }
610
+ }
611
+ };
612
+ return controller;
613
+ };
614
+
206
615
  // src/referral.ts
207
616
  var REF_PARAM = "mrr_ref";
208
617
  var CLICK_PARAM = "mrr_click";
@@ -288,11 +697,37 @@ var createClient = (options, deps) => {
288
697
  let backgroundSince = null;
289
698
  const early = [];
290
699
  const exposed = /* @__PURE__ */ new Set();
700
+ let textsFor = null;
701
+ const texts = () => {
702
+ const id = distinctId();
703
+ if (!textsFor || textsFor.config !== config || textsFor.id !== id) textsFor = { config, id, texts: textChanges(config, id, {}) };
704
+ return textsFor.texts;
705
+ };
291
706
  const listeners = /* @__PURE__ */ new Set();
292
707
  const crumbs = [];
293
708
  const errorQueue = [];
294
709
  const errorsSeen = /* @__PURE__ */ new Set();
295
710
  let errorsSent = 0;
711
+ const flows = createFlows({
712
+ track: (event, props) => record(event, props),
713
+ personId: () => consent !== "off" ? identified : void 0,
714
+ auto: options.onboarding !== false,
715
+ storage: () => storage && consent === "persistent" ? {
716
+ get: (k) => get(k),
717
+ set: (k, v) => void set(k, v)
718
+ } : null,
719
+ openUrl: deps.openUrl,
720
+ fetchInApp: async (id) => {
721
+ if (!apiKey) return null;
722
+ const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}`, { method: "GET" });
723
+ return r.ok ? await r.json() : null;
724
+ }
725
+ });
726
+ const refreshFlows = () => {
727
+ if (!isReady || closed) return;
728
+ flows.setConfig(config);
729
+ void flows.refresh();
730
+ };
296
731
  const changed = () => {
297
732
  version++;
298
733
  for (const l of [...listeners]) {
@@ -526,6 +961,61 @@ var createClient = (options, deps) => {
526
961
  debug("Config not reachable; using the cached one.");
527
962
  }
528
963
  };
964
+ let pushToken = null;
965
+ let pushFor;
966
+ let pushOff = null;
967
+ const opened = /* @__PURE__ */ new Set();
968
+ const onPushOpen = (data) => {
969
+ const m = data?.mrrocket;
970
+ if (!m?.id || opened.has(m.id)) return;
971
+ opened.add(m.id);
972
+ record("$push", { action: "opened", message: m.id });
973
+ if (m.url && /^[a-z][a-z0-9+.-]*:/i.test(m.url)) deps.openUrl?.(m.url);
974
+ };
975
+ const listenForOpens = () => {
976
+ if (pushOff || !deps.push || closed) return;
977
+ try {
978
+ pushOff = deps.push.onOpen(onPushOpen);
979
+ void deps.push.lastOpen?.().then(onPushOpen).catch(() => void 0);
980
+ } catch {
981
+ pushOff = null;
982
+ }
983
+ };
984
+ const loadedNow = () => loaded ? Promise.resolve() : new Promise((resolve) => {
985
+ early.push(resolve);
986
+ });
987
+ const registerForPush = async (o = {}) => {
988
+ if (!deps.push) return { status: "unavailable", reason: "Install expo-notifications to receive push notifications." };
989
+ await loadedNow();
990
+ if (!apiKey || consent === "off") return { status: "error", reason: !apiKey ? "No public key." : "The consent is off." };
991
+ if (!identified) return { status: "not-signed-in" };
992
+ const who = identified;
993
+ try {
994
+ const permission = await deps.push.permission(o.ask !== false);
995
+ if (permission !== "granted") return { status: permission === "denied" ? "denied" : "not-asked" };
996
+ const token = await deps.push.token(o.projectId);
997
+ if (!token) return { status: "unavailable", reason: "This device has no push token (a simulator, or no EAS project id)." };
998
+ const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: who, platform: "expo", token, app: context }));
999
+ if (status < 200 || status >= 300) {
1000
+ warn(`Push registration was rejected with ${status || "no answer"}. Check the public key and the host.`);
1001
+ return { status: "error", reason: `MRRocket answered ${status || "nothing"}.` };
1002
+ }
1003
+ pushToken = token;
1004
+ pushFor = who;
1005
+ listenForOpens();
1006
+ debug("Registered for push.");
1007
+ return { status: "registered", token };
1008
+ } catch (e) {
1009
+ return { status: "error", reason: e instanceof Error ? e.message : String(e) };
1010
+ }
1011
+ };
1012
+ const unregisterForPush = async () => {
1013
+ const token = pushToken;
1014
+ if (!token || !apiKey) return false;
1015
+ pushToken = null;
1016
+ const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: pushFor, platform: "expo", token, remove: true }));
1017
+ return status >= 200 && status < 300;
1018
+ };
529
1019
  let resolveReady = () => {
530
1020
  };
531
1021
  const ready = new Promise((r) => {
@@ -601,10 +1091,12 @@ var createClient = (options, deps) => {
601
1091
  for (const run of early.splice(0)) run();
602
1092
  if (deps.appState) appStateSub = deps.appState.addEventListener("change", onAppState);
603
1093
  installErrorHandler();
1094
+ listenForOpens();
604
1095
  if (appState === "active" || !deps.appState) startTimer();
605
1096
  if (queue.length) void flush();
606
1097
  await fetchConfig();
607
1098
  markReady();
1099
+ refreshFlows();
608
1100
  };
609
1101
  void start().catch(() => {
610
1102
  loaded = true;
@@ -614,6 +1106,7 @@ var createClient = (options, deps) => {
614
1106
  track: (event, props) => {
615
1107
  if (typeof event !== "string" || !event) return;
616
1108
  record(event, props);
1109
+ flows.onEvent(event);
617
1110
  },
618
1111
  screen: (name, props) => {
619
1112
  if (typeof name !== "string" || !name.trim()) return;
@@ -636,12 +1129,14 @@ var createClient = (options, deps) => {
636
1129
  record("$identify", { ...props, id, ...previous && previous !== id ? { previousId: previous } : {} });
637
1130
  if (consent === "persistent") void set(KEYS.id, id);
638
1131
  changed();
1132
+ refreshFlows();
639
1133
  },
640
1134
  reset: () => {
641
1135
  if (!loaded) {
642
1136
  early.push(() => client.reset());
643
1137
  return;
644
1138
  }
1139
+ if (pushToken) void unregisterForPush();
645
1140
  identified = void 0;
646
1141
  person = void 0;
647
1142
  session = null;
@@ -653,6 +1148,7 @@ var createClient = (options, deps) => {
653
1148
  void set(KEYS.id, installId);
654
1149
  }
655
1150
  changed();
1151
+ refreshFlows();
656
1152
  },
657
1153
  setConsent: (next) => {
658
1154
  if (next !== "anonymous" && next !== "persistent" && next !== "off") {
@@ -682,6 +1178,7 @@ var createClient = (options, deps) => {
682
1178
  }
683
1179
  if (before === "off" && currentScreen && loaded) record("$screen", { path: currentScreen, screen: currentScreen });
684
1180
  changed();
1181
+ refreshFlows();
685
1182
  },
686
1183
  captureError: (error) => capture(error, "manual"),
687
1184
  getFlag: (key) => flagFor(key, config, distinctId(), { ...person }) !== false,
@@ -702,6 +1199,19 @@ var createClient = (options, deps) => {
702
1199
  exposed.add(mark);
703
1200
  record("$exposure", { experiment: key, variant });
704
1201
  },
1202
+ getText: (id, fallback) => {
1203
+ client.exposeText(id);
1204
+ return client.peekText(id) ?? fallback;
1205
+ },
1206
+ peekText: (id) => texts()[id],
1207
+ exposeText: (id) => {
1208
+ for (const key of experimentsOfText(config, id)) client.expose(key, resolveVariant(key, config, distinctId(), {}));
1209
+ },
1210
+ openCancelFlow: (o) => flows.openCancelFlow(o),
1211
+ startOnboarding: (id) => flows.startOnboarding(id),
1212
+ registerForPush,
1213
+ unregisterForPush,
1214
+ flows,
705
1215
  flush,
706
1216
  ready,
707
1217
  isReady: () => isReady,
@@ -717,6 +1227,8 @@ var createClient = (options, deps) => {
717
1227
  if (closed) return;
718
1228
  stopTimer();
719
1229
  appStateSub?.remove();
1230
+ pushOff?.();
1231
+ pushOff = null;
720
1232
  if (handlerInstalled && deps.errorUtils && previousHandler) deps.errorUtils.setGlobalHandler(previousHandler);
721
1233
  await flush();
722
1234
  await sendErrors();
@@ -739,8 +1251,252 @@ var activeRouteName = (state) => {
739
1251
  return name;
740
1252
  };
741
1253
 
742
- // src/native/platform.ts
1254
+ // src/native/flows-view.ts
1255
+ var import_react = require("react");
743
1256
  var RN = require("react-native");
1257
+ var LIGHT = { bg: "#ffffff", fg: "#1c1d20", muted: "#5d6068", soft: "#f4f5f7", line: "#e4e5e8" };
1258
+ var DARK = { bg: "#17181b", fg: "#f1f2f4", muted: "#a9acb3", soft: "#232428", line: "#2e2f35" };
1259
+ var months = (n) => `${n} ${n === 1 ? "month" : "months"}`;
1260
+ var MRRocketFlows = ({ flows, appearance = {} }) => {
1261
+ const scheme = RN.useColorScheme?.();
1262
+ const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
1263
+ const p = { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
1264
+ const [, setTick] = (0, import_react.useState)(0);
1265
+ (0, import_react.useEffect)(() => flows.subscribe(() => setTick((t) => t + 1)), [flows]);
1266
+ const v = flows.view();
1267
+ const [comment, setComment] = (0, import_react.useState)("");
1268
+ const screen = v?.type === "cancel" ? v.screen : null;
1269
+ (0, import_react.useEffect)(() => {
1270
+ if (!screen) setComment("");
1271
+ }, [screen]);
1272
+ if (!v) return null;
1273
+ const text2 = (children, style = {}) => (0, import_react.createElement)(RN.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
1274
+ const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
1275
+ const title = (children) => (0, import_react.createElement)(RN.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 18, fontWeight: "700" } }, children);
1276
+ const btn = (label, kind, onPress, disabled = false) => (0, import_react.createElement)(
1277
+ RN.Pressable,
1278
+ {
1279
+ key: label,
1280
+ onPress,
1281
+ disabled,
1282
+ accessibilityRole: "button",
1283
+ accessibilityState: { disabled },
1284
+ style: {
1285
+ height: kind === "quiet" ? 36 : 44,
1286
+ borderRadius: 10,
1287
+ alignItems: "center",
1288
+ justifyContent: "center",
1289
+ opacity: disabled ? 0.55 : 1,
1290
+ backgroundColor: kind === "primary" ? p.accent : kind === "danger" ? p.danger : kind === "secondary" ? p.bg : "transparent",
1291
+ borderWidth: kind === "secondary" ? 1 : 0,
1292
+ borderColor: p.line
1293
+ }
1294
+ },
1295
+ text2(label, {
1296
+ color: kind === "primary" || kind === "danger" ? "#ffffff" : kind === "quiet" ? p.muted : p.fg,
1297
+ fontWeight: kind === "quiet" ? "500" : "600"
1298
+ })
1299
+ );
1300
+ const close = (label, onPress) => (0, import_react.createElement)(
1301
+ RN.Pressable,
1302
+ { onPress, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 12, style: { marginLeft: "auto", padding: 4 } },
1303
+ text2("\xD7", { fontSize: 22, color: p.muted })
1304
+ );
1305
+ const card = (children, style = {}) => (0, import_react.createElement)(
1306
+ RN.View,
1307
+ {
1308
+ style: {
1309
+ backgroundColor: p.bg,
1310
+ borderRadius: 16,
1311
+ padding: 20,
1312
+ gap: 12,
1313
+ shadowColor: "#000",
1314
+ shadowOpacity: 0.18,
1315
+ shadowRadius: 20,
1316
+ elevation: 8,
1317
+ ...style
1318
+ }
1319
+ },
1320
+ ...children
1321
+ );
1322
+ const sheet = (children, onClose) => (0, import_react.createElement)(
1323
+ RN.Modal,
1324
+ { visible: true, transparent: true, animationType: "fade", onRequestClose: onClose },
1325
+ (0, import_react.createElement)(
1326
+ RN.View,
1327
+ { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.4)", justifyContent: "flex-end" } },
1328
+ card(children, { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, paddingBottom: 34 })
1329
+ )
1330
+ );
1331
+ const input = (placeholder) => (0, import_react.createElement)(RN.TextInput, {
1332
+ value: comment,
1333
+ onChangeText: setComment,
1334
+ placeholder,
1335
+ placeholderTextColor: p.muted,
1336
+ multiline: true,
1337
+ maxLength: 500,
1338
+ accessibilityLabel: placeholder,
1339
+ style: { minHeight: 72, borderWidth: 1, borderColor: p.line, borderRadius: 10, padding: 10, color: p.fg, textAlignVertical: "top" }
1340
+ });
1341
+ if (v.type === "cancel") {
1342
+ const top = (0, import_react.createElement)(
1343
+ RN.View,
1344
+ { style: { flexDirection: "row", alignItems: "center", gap: 4 } },
1345
+ ...v.steps > 1 ? Array.from(
1346
+ { length: v.steps },
1347
+ (_, i) => (0, import_react.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i < v.step ? p.accent : p.line } })
1348
+ ) : [],
1349
+ close("Close", flows.cancel.close)
1350
+ );
1351
+ const error = v.error ? text2(v.error, { color: p.danger, fontSize: 13 }) : null;
1352
+ let body = [];
1353
+ if (v.screen === "survey")
1354
+ body = [
1355
+ title(v.flow.question || "Why are you leaving?"),
1356
+ (0, import_react.createElement)(
1357
+ RN.ScrollView,
1358
+ { style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
1359
+ ...v.flow.reasons.map(
1360
+ (r) => (0, import_react.createElement)(
1361
+ RN.Pressable,
1362
+ {
1363
+ key: r.id,
1364
+ onPress: () => flows.cancel.pick(r.id),
1365
+ accessibilityRole: "radio",
1366
+ accessibilityState: { checked: v.reason === r.id },
1367
+ style: {
1368
+ minHeight: 44,
1369
+ paddingHorizontal: 12,
1370
+ borderRadius: 10,
1371
+ borderWidth: 1,
1372
+ borderColor: v.reason === r.id ? p.accent : p.line,
1373
+ backgroundColor: v.reason === r.id ? p.soft : p.bg,
1374
+ justifyContent: "center"
1375
+ }
1376
+ },
1377
+ text2(r.label, { fontWeight: "500" })
1378
+ )
1379
+ )
1380
+ ),
1381
+ btn("Continue", "primary", flows.cancel.next, !v.reason),
1382
+ btn("Never mind, keep my plan", "quiet", flows.cancel.close)
1383
+ ];
1384
+ else if (v.screen === "offer" && v.offer) {
1385
+ const o = v.offer;
1386
+ const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
1387
+ 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;
1388
+ body = [
1389
+ label ? muted(`You said: ${label}`) : null,
1390
+ title(o.headline),
1391
+ detail ? (0, import_react.createElement)(RN.View, { style: { backgroundColor: p.soft, borderRadius: 10, padding: 12 } }, text2(detail, { fontWeight: "600" })) : null,
1392
+ o.kind === "contact" ? input("What would make you stay?") : null,
1393
+ btn(v.busy ? "One moment\u2026" : offerButton(o), "primary", () => flows.cancel.accept(comment), v.busy),
1394
+ btn("No thanks, continue cancelling", "quiet", flows.cancel.decline, v.busy)
1395
+ ];
1396
+ } else
1397
+ body = [
1398
+ title(v.flow.confirm.title),
1399
+ v.flow.confirm.body ? muted(v.flow.confirm.body, { fontSize: 15 }) : null,
1400
+ v.steps > 1 ? input("Anything else we should know? (optional)") : null,
1401
+ btn(v.busy ? "One moment\u2026" : v.flow.confirm.button, "danger", () => flows.cancel.confirm(comment), v.busy),
1402
+ btn("Keep my plan", "secondary", flows.cancel.close, v.busy)
1403
+ ];
1404
+ return sheet([top, ...body, error], flows.cancel.close);
1405
+ }
1406
+ if (v.type === "step")
1407
+ return sheet(
1408
+ [
1409
+ (0, import_react.createElement)(
1410
+ RN.View,
1411
+ { style: { flexDirection: "row", alignItems: "center" } },
1412
+ v.total > 1 ? muted(`Step ${v.index + 1} of ${v.total}`) : null,
1413
+ close("Close the tour", flows.onboarding.dismiss)
1414
+ ),
1415
+ title(v.step.title),
1416
+ v.step.body ? muted(v.step.body, { fontSize: 15 }) : null,
1417
+ btn(v.step.button || (v.last ? "Done" : "Next"), "primary", flows.onboarding.next)
1418
+ ],
1419
+ flows.onboarding.dismiss
1420
+ );
1421
+ if (v.type === "checklist") {
1422
+ const done = v.items.filter((i) => i.done).length;
1423
+ return (0, import_react.createElement)(
1424
+ RN.View,
1425
+ { pointerEvents: "box-none", style: { position: "absolute", left: 12, right: 12, bottom: 28 } },
1426
+ card([
1427
+ (0, import_react.createElement)(
1428
+ RN.View,
1429
+ { key: "t", style: { flexDirection: "row", alignItems: "center" } },
1430
+ title(v.flow.name),
1431
+ close("Hide the checklist", flows.onboarding.dismiss)
1432
+ ),
1433
+ muted(`${done} of ${v.items.length} done`),
1434
+ ...v.items.map(
1435
+ ({ step, done: d }) => (0, import_react.createElement)(
1436
+ RN.Pressable,
1437
+ {
1438
+ key: step.id,
1439
+ disabled: d || !!step.event,
1440
+ onPress: () => flows.onboarding.check(step.id),
1441
+ accessibilityRole: "checkbox",
1442
+ accessibilityState: { checked: d },
1443
+ style: { flexDirection: "row", gap: 10, alignItems: "flex-start", paddingVertical: 6 }
1444
+ },
1445
+ (0, import_react.createElement)(
1446
+ RN.View,
1447
+ {
1448
+ style: {
1449
+ width: 20,
1450
+ height: 20,
1451
+ borderRadius: 10,
1452
+ borderWidth: 1.5,
1453
+ borderColor: d ? p.accent : p.line,
1454
+ backgroundColor: d ? p.accent : "transparent",
1455
+ alignItems: "center",
1456
+ justifyContent: "center"
1457
+ }
1458
+ },
1459
+ d ? text2("\u2713", { color: "#fff", fontSize: 12 }) : null
1460
+ ),
1461
+ (0, import_react.createElement)(
1462
+ RN.View,
1463
+ { style: { flex: 1 } },
1464
+ text2(step.title, { fontWeight: "600", color: d ? p.muted : p.fg, textDecorationLine: d ? "line-through" : "none" }),
1465
+ step.body ? muted(step.body) : null
1466
+ )
1467
+ )
1468
+ )
1469
+ ])
1470
+ );
1471
+ }
1472
+ const m = v.message;
1473
+ if (m.style === "banner")
1474
+ return (0, import_react.createElement)(
1475
+ RN.View,
1476
+ { pointerEvents: "box-none", style: { position: "absolute", left: 12, right: 12, top: 56 } },
1477
+ card([
1478
+ (0, import_react.createElement)(
1479
+ RN.View,
1480
+ { key: "t", style: { flexDirection: "row", alignItems: "center", gap: 8 } },
1481
+ (0, import_react.createElement)(RN.View, { style: { flex: 1 } }, text2(m.title, { fontWeight: "700" }), m.body ? muted(m.body) : null),
1482
+ close("Dismiss", flows.message.dismiss)
1483
+ ),
1484
+ m.button ? btn(m.button, "primary", flows.message.click) : null
1485
+ ])
1486
+ );
1487
+ return sheet(
1488
+ [
1489
+ (0, import_react.createElement)(RN.View, { key: "t", style: { flexDirection: "row" } }, close("Dismiss", flows.message.dismiss)),
1490
+ title(m.title),
1491
+ m.body ? muted(m.body, { fontSize: 15 }) : null,
1492
+ m.button ? btn(m.button, "primary", flows.message.click) : null
1493
+ ],
1494
+ flows.message.dismiss
1495
+ );
1496
+ };
1497
+
1498
+ // src/native/platform.ts
1499
+ var RN2 = require("react-native");
744
1500
  var asyncStorageModule = null;
745
1501
  try {
746
1502
  asyncStorageModule = require("@react-native-async-storage/async-storage");
@@ -765,6 +1521,45 @@ try {
765
1521
  } catch {
766
1522
  expoLocalization = null;
767
1523
  }
1524
+ var expoNotifications = null;
1525
+ try {
1526
+ expoNotifications = require("expo-notifications");
1527
+ } catch {
1528
+ expoNotifications = null;
1529
+ }
1530
+ var expoConstants = {};
1531
+ try {
1532
+ expoConstants = require("expo-constants");
1533
+ } catch {
1534
+ expoConstants = {};
1535
+ }
1536
+ var dataOf = (r) => r?.notification?.request?.content?.data ?? null;
1537
+ var expoPush = () => {
1538
+ const n = expoNotifications;
1539
+ if (!n || typeof n.getExpoPushTokenAsync !== "function") return null;
1540
+ const constants = expoConstants.default ?? expoConstants;
1541
+ return {
1542
+ permission: async (ask) => {
1543
+ const now = await n.getPermissionsAsync();
1544
+ if (now.granted || now.status === "granted") return "granted";
1545
+ if (now.status === "denied" || !ask) return now.status === "denied" ? "denied" : "undetermined";
1546
+ const asked = await n.requestPermissionsAsync();
1547
+ return asked.granted || asked.status === "granted" ? "granted" : asked.status === "denied" ? "denied" : "undetermined";
1548
+ },
1549
+ token: async (projectId) => {
1550
+ if (RN2.Platform.OS === "android" && n.setNotificationChannelAsync)
1551
+ await n.setNotificationChannelAsync("default", { name: "Default", importance: n.AndroidImportance?.HIGH ?? 4 });
1552
+ const id = projectId ?? constants.expoConfig?.extra?.eas?.projectId ?? constants.easConfig?.projectId;
1553
+ const t = await n.getExpoPushTokenAsync(id ? { projectId: id } : void 0);
1554
+ return typeof t?.data === "string" && t.data ? t.data : null;
1555
+ },
1556
+ onOpen: (listener) => {
1557
+ const sub = n.addNotificationResponseReceivedListener((r) => listener(dataOf(r) ?? {}));
1558
+ return () => sub.remove();
1559
+ },
1560
+ lastOpen: async () => dataOf(await n.getLastNotificationResponseAsync?.())
1561
+ };
1562
+ };
768
1563
  var asyncStorage = () => {
769
1564
  const m = asyncStorageModule;
770
1565
  const s = m?.default ?? m;
@@ -773,7 +1568,7 @@ var asyncStorage = () => {
773
1568
  var DEVICE_TYPES = { 1: "phone", 2: "tablet", 3: "desktop", 4: "tv" };
774
1569
  var text = (v, max = 80) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : typeof v === "number" ? String(v) : void 0;
775
1570
  var readContext = () => {
776
- const p = RN.Platform;
1571
+ const p = RN2.Platform;
777
1572
  let locale;
778
1573
  try {
779
1574
  locale = text(expoLocalization?.getLocales?.()[0]?.languageTag, 35);
@@ -802,21 +1597,24 @@ var readContext = () => {
802
1597
  var nativeDeps = (overrides = {}) => ({
803
1598
  storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
804
1599
  fetch: ((url, init) => fetch(url, init)),
805
- appState: RN.AppState,
1600
+ appState: RN2.AppState,
806
1601
  errorUtils: globalThis.ErrorUtils ?? null,
807
1602
  context: readContext,
808
1603
  appId: text(expoApplication?.applicationId, 150),
809
1604
  screen: () => {
810
- const s = RN.Dimensions.get("screen");
1605
+ const s = RN2.Dimensions.get("screen");
811
1606
  return s ? { w: s.width, h: s.height } : void 0;
812
1607
  },
813
- initialUrl: () => RN.Linking.getInitialURL()
1608
+ initialUrl: () => RN2.Linking.getInitialURL(),
1609
+ openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
1610
+ push: expoPush()
814
1611
  });
815
1612
  var optionalModules = () => ({
816
1613
  asyncStorage: !!asyncStorage(),
817
1614
  expoDevice: !!expoDevice,
818
1615
  expoApplication: !!expoApplication,
819
- expoLocalization: !!expoLocalization
1616
+ expoLocalization: !!expoLocalization,
1617
+ expoNotifications: !!expoNotifications
820
1618
  });
821
1619
 
822
1620
  // src/native/index.tsx
@@ -835,20 +1633,20 @@ var createMRRocket = (options) => {
835
1633
  return shared;
836
1634
  };
837
1635
  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)(() => {
1636
+ var Ctx = (0, import_react2.createContext)(null);
1637
+ var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, ...options }) => {
1638
+ const [client] = (0, import_react2.useState)(() => given ?? createMRRocket({ ...options, consent }));
1639
+ (0, import_react2.useEffect)(() => {
842
1640
  if (consent) client.setConsent(consent);
843
1641
  }, [client, consent]);
844
- return (0, import_react.createElement)(Ctx.Provider, { value: client }, children);
1642
+ return (0, import_react2.createElement)(Ctx.Provider, { value: client }, children, flows ? (0, import_react2.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null);
845
1643
  };
846
1644
  var useClient = () => {
847
- const c = (0, import_react.useContext)(Ctx);
1645
+ const c = (0, import_react2.useContext)(Ctx);
848
1646
  if (!c) throw new Error("MRRocket: wrap your app in <MRRocketProvider apiKey=\u2026>.");
849
1647
  return c;
850
1648
  };
851
- var useVersion = (client) => (0, import_react.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
1649
+ var useVersion = (client) => (0, import_react2.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
852
1650
  var useMRRocket = () => {
853
1651
  const client = useClient();
854
1652
  useVersion(client);
@@ -865,6 +1663,18 @@ var useMRRocket = () => {
865
1663
  ready: client.isReady()
866
1664
  };
867
1665
  };
1666
+ var useCancelFlow = () => {
1667
+ const client = useClient();
1668
+ return { open: client.openCancelFlow };
1669
+ };
1670
+ var usePush = () => {
1671
+ const client = useClient();
1672
+ return { register: client.registerForPush, unregister: client.unregisterForPush };
1673
+ };
1674
+ var useOnboarding = () => {
1675
+ const client = useClient();
1676
+ return { start: client.startOnboarding };
1677
+ };
868
1678
  var useFlag = (key) => {
869
1679
  const client = useClient();
870
1680
  useVersion(client);
@@ -880,11 +1690,22 @@ var useVariant = (key) => {
880
1690
  useVersion(client);
881
1691
  const config = client.getConfig();
882
1692
  const variant = client.peekVariant(key);
883
- (0, import_react.useEffect)(() => {
1693
+ (0, import_react2.useEffect)(() => {
884
1694
  if (config) client.expose(key, variant);
885
1695
  }, [client, config, key, variant]);
886
1696
  return variant;
887
1697
  };
1698
+ var useText = (id, fallback) => {
1699
+ const client = useClient();
1700
+ useVersion(client);
1701
+ const config = client.getConfig();
1702
+ const text2 = client.peekText(id);
1703
+ const person = client.getDistinctId();
1704
+ (0, import_react2.useEffect)(() => {
1705
+ if (config) client.exposeText(id);
1706
+ }, [client, config, id, person]);
1707
+ return text2 ?? fallback;
1708
+ };
888
1709
  var expoRouter;
889
1710
  var loadExpoRouter = () => {
890
1711
  if (expoRouter !== void 0) return expoRouter;
@@ -905,13 +1726,13 @@ var useExpoRouterTracking = () => {
905
1726
  console.warn("[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.");
906
1727
  }
907
1728
  const pathname = (router?.usePathname ?? noPathname)();
908
- (0, import_react.useEffect)(() => {
1729
+ (0, import_react2.useEffect)(() => {
909
1730
  if (pathname) client.screen(pathname);
910
1731
  }, [client, pathname]);
911
1732
  };
912
1733
  var useNavigationTracking = () => {
913
1734
  const client = useClient();
914
- return (0, import_react.useCallback)(
1735
+ return (0, import_react2.useCallback)(
915
1736
  (state) => {
916
1737
  const name = activeRouteName(state);
917
1738
  if (name) client.screen(name);