@mrrocket/sdk 0.3.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 (64) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +240 -5
  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/batch.d.ts +33 -0
  7. package/dist/batch.js +50 -0
  8. package/dist/batch.js.map +1 -0
  9. package/dist/browser/mrrocket-flows.js +64 -0
  10. package/dist/browser/mrrocket-push.js +1 -0
  11. package/dist/browser/mrrocket-replay.js +78 -0
  12. package/dist/browser/mrrocket-sw.js +1 -0
  13. package/dist/browser/mrrocket.js +2 -0
  14. package/dist/browser.d.ts +1 -1
  15. package/dist/browser.js +3 -1
  16. package/dist/browser.js.map +1 -1
  17. package/dist/client.d.ts +36 -1
  18. package/dist/client.js +163 -83
  19. package/dist/client.js.map +1 -1
  20. package/dist/context.d.ts +10 -0
  21. package/dist/context.js.map +1 -1
  22. package/dist/core.d.ts +91 -4
  23. package/dist/core.js +83 -6
  24. package/dist/core.js.map +1 -1
  25. package/dist/feedback.d.ts +19 -4
  26. package/dist/feedback.js +652 -190
  27. package/dist/feedback.js.map +1 -1
  28. package/dist/flows-ui.d.ts +23 -0
  29. package/dist/flows-ui.js +384 -0
  30. package/dist/flows-ui.js.map +1 -0
  31. package/dist/flows-web.d.ts +19 -0
  32. package/dist/flows-web.js +42 -0
  33. package/dist/flows-web.js.map +1 -0
  34. package/dist/flows.d.ts +222 -0
  35. package/dist/flows.js +409 -0
  36. package/dist/flows.js.map +1 -0
  37. package/dist/index.d.ts +5 -2
  38. package/dist/index.js +9 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/proxy.d.ts +34 -1
  41. package/dist/proxy.js +14 -1
  42. package/dist/proxy.js.map +1 -1
  43. package/dist/push-web.d.ts +43 -0
  44. package/dist/push-web.js +81 -0
  45. package/dist/push-web.js.map +1 -0
  46. package/dist/react-native/index.d.cts +561 -0
  47. package/dist/react-native/index.d.ts +561 -0
  48. package/dist/react-native/index.js +1743 -0
  49. package/dist/react-native/index.js.map +1 -0
  50. package/dist/react-native/package.json +3 -0
  51. package/dist/referral.d.ts +6 -0
  52. package/dist/referral.js +7 -3
  53. package/dist/referral.js.map +1 -1
  54. package/dist/styles.d.ts +1 -1
  55. package/dist/styles.js +127 -24
  56. package/dist/styles.js.map +1 -1
  57. package/dist/ui.d.ts +3 -0
  58. package/dist/ui.js +12 -0
  59. package/dist/ui.js.map +1 -1
  60. package/dist/web-state.d.ts +36 -0
  61. package/dist/web-state.js +64 -0
  62. package/dist/web-state.js.map +1 -0
  63. package/package.json +64 -6
  64. package/react-native/package.json +7 -0
@@ -0,0 +1,1743 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/native/index.tsx
21
+ var native_exports = {};
22
+ __export(native_exports, {
23
+ MRRocketFlows: () => MRRocketFlows,
24
+ MRRocketProvider: () => MRRocketProvider,
25
+ SDK_VERSION: () => SDK_VERSION,
26
+ activeRouteName: () => activeRouteName,
27
+ createMRRocket: () => createMRRocket,
28
+ getMRRocket: () => getMRRocket,
29
+ useCancelFlow: () => useCancelFlow,
30
+ useExpoRouterTracking: () => useExpoRouterTracking,
31
+ useFlag: () => useFlag,
32
+ useFlagVariant: () => useFlagVariant,
33
+ useMRRocket: () => useMRRocket,
34
+ useNavigationTracking: () => useNavigationTracking,
35
+ useOnboarding: () => useOnboarding,
36
+ usePush: () => usePush,
37
+ useText: () => useText,
38
+ useVariant: () => useVariant
39
+ });
40
+ module.exports = __toCommonJS(native_exports);
41
+ var import_react2 = require("react");
42
+
43
+ // src/core.ts
44
+ var SDK_VERSION = "0.8.0";
45
+ var propOf = (person, name) => {
46
+ const want = name.trim().toLowerCase();
47
+ for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
48
+ return void 0;
49
+ };
50
+ var same = (actual, value) => {
51
+ const a = actual.trim().toLowerCase();
52
+ const v = value.trim().toLowerCase();
53
+ return v.startsWith("@") ? a.endsWith(v) : a === v;
54
+ };
55
+ var matchesCondition = (c, person) => {
56
+ if (c.op === "in cohort") return false;
57
+ const raw = propOf(person, c.property);
58
+ const actual = raw === void 0 || raw === null ? "" : String(raw);
59
+ if (c.op === "\u2265") {
60
+ const a = Number(actual);
61
+ const b = Number(c.values[0]);
62
+ return actual !== "" && Number.isFinite(a) && Number.isFinite(b) && a >= b;
63
+ }
64
+ const hit = actual !== "" && c.values.some((v) => same(actual, v));
65
+ return c.op === "is" || c.op === "in" ? hit : !hit;
66
+ };
67
+ var evaluateFlag = (flag, distinctId, person = {}) => {
68
+ if (!flag.on) return false;
69
+ const subject = { id: distinctId, ...person };
70
+ const set = flag.sets.length ? flag.sets.find((s) => s.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };
71
+ if (!set) return false;
72
+ if (flagUnit(`${flag.key}:rollout:${distinctId}`) * 100 >= set.rollout) return false;
73
+ if (!flag.variants?.length) return true;
74
+ const u = flagUnit(`${flag.key}:variant:${distinctId}`) * 100;
75
+ let acc = 0;
76
+ for (const v of flag.variants) {
77
+ acc += v.split;
78
+ if (u < acc) return v.key;
79
+ }
80
+ return flag.variants[flag.variants.length - 1].key;
81
+ };
82
+ var flagFor = (key, config, distinctId, person = {}) => {
83
+ const flag = config?.flags?.find((f) => f.key === key);
84
+ return flag ? evaluateFlag(flag, distinctId, person) : false;
85
+ };
86
+ var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
87
+ var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
88
+ var utmFrom = (get) => {
89
+ const out = {};
90
+ for (const key of UTM_KEYS) {
91
+ const value = get(key)?.trim();
92
+ if (value) out[key] = value.slice(0, 150);
93
+ }
94
+ return out;
95
+ };
96
+ var parseQuery = (url) => {
97
+ const out = {};
98
+ const q = url.split("#")[0];
99
+ const at = q.indexOf("?");
100
+ if (at < 0) return out;
101
+ const dec = (s) => {
102
+ try {
103
+ return decodeURIComponent(s.replace(/\+/g, " "));
104
+ } catch {
105
+ return s;
106
+ }
107
+ };
108
+ for (const pair of q.slice(at + 1).split("&")) {
109
+ if (!pair) continue;
110
+ const eq = pair.indexOf("=");
111
+ const k = dec(eq < 0 ? pair : pair.slice(0, eq));
112
+ if (k && !(k in out)) out[k] = eq < 0 ? "" : dec(pair.slice(eq + 1));
113
+ }
114
+ return out;
115
+ };
116
+ var hashUnit = (input) => {
117
+ let h2 = 2166136261;
118
+ for (let i = 0; i < input.length; i++) {
119
+ h2 ^= input.charCodeAt(i);
120
+ h2 = Math.imul(h2, 16777619);
121
+ }
122
+ return (h2 >>> 0) / 4294967296;
123
+ };
124
+ var flagUnit = (input) => {
125
+ let h2 = 2166136261;
126
+ for (let i = 0; i < input.length; i++) {
127
+ h2 ^= input.charCodeAt(i);
128
+ h2 = Math.imul(h2, 16777619);
129
+ }
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;
136
+ };
137
+ var assign = (experiment, distinctId) => {
138
+ const control = experiment.variants[0]?.id ?? "control";
139
+ if (experiment.winner) return experiment.winner;
140
+ if (experiment.status !== "running") return control;
141
+ const u = hashUnit(`${experiment.key}:${distinctId}`);
142
+ let acc = 0;
143
+ for (const v of experiment.variants) {
144
+ acc += v.weight;
145
+ if (u < acc) return v.id;
146
+ }
147
+ return experiment.variants[experiment.variants.length - 1]?.id ?? control;
148
+ };
149
+ var experimentsOfText = (config, id) => (config?.experiments ?? []).filter((e) => e.variants.some((v) => v.changes && id in v.changes)).map((e) => e.key);
150
+ var resolveVariant = (key, config, distinctId, preview) => {
151
+ if (preview[key]) return preview[key];
152
+ const experiment = config?.experiments.find((e) => e.key === key);
153
+ return experiment ? assign(experiment, distinctId) : "control";
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
+ };
164
+ var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
165
+
166
+ // src/batch.ts
167
+ var MAX_BATCH = 100;
168
+ var MAX_BYTES = 6e4;
169
+ var byteLength = (s) => {
170
+ let n = 0;
171
+ for (let i = 0; i < s.length; i++) {
172
+ const c = s.charCodeAt(i);
173
+ if (c < 128) n += 1;
174
+ else if (c < 2048) n += 2;
175
+ else if (c >= 55296 && c <= 56319 && i + 1 < s.length && (s.charCodeAt(i + 1) & 64512) === 56320) {
176
+ n += 4;
177
+ i++;
178
+ } else n += 3;
179
+ }
180
+ return n;
181
+ };
182
+ var batchRequests = (key, events, sdk) => {
183
+ const head = `{"key":${JSON.stringify(key)},"sdk":${JSON.stringify(sdk)},"events":[`;
184
+ const empty = byteLength(head) + 2;
185
+ const out = [];
186
+ let chunk = [];
187
+ let items = [];
188
+ let size = empty;
189
+ const close = () => {
190
+ if (chunk.length) out.push({ body: `${head}${chunk.join(",")}]}`, events: items });
191
+ chunk = [];
192
+ items = [];
193
+ size = empty;
194
+ };
195
+ for (const event of events) {
196
+ const json = JSON.stringify(event);
197
+ const n = byteLength(json) + 1;
198
+ if (empty + n > MAX_BYTES) continue;
199
+ if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();
200
+ chunk.push(json);
201
+ items.push(event);
202
+ size += n;
203
+ }
204
+ close();
205
+ return out;
206
+ };
207
+
208
+ // src/errors.ts
209
+ var MAX_STACK = 8 * 1024;
210
+ var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
211
+ var describeError = (error) => {
212
+ if (error instanceof Error) return { type: error.name || "Error", message: clip(error.message || String(error), 1e3), stack: error.stack ? clip(error.stack, MAX_STACK) : void 0 };
213
+ if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
214
+ try {
215
+ return { type: "Error", message: clip(JSON.stringify(error) ?? String(error), 1e3) };
216
+ } catch {
217
+ return { type: "Error", message: String(error) };
218
+ }
219
+ };
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
+
615
+ // src/referral.ts
616
+ var REF_PARAM = "mrr_ref";
617
+ var CLICK_PARAM = "mrr_click";
618
+ var CODE = /^[A-Za-z0-9_-]{1,64}$/;
619
+ var referralFrom = (get, param = "ref") => {
620
+ const marked = get(REF_PARAM);
621
+ if (marked && CODE.test(marked)) {
622
+ const click = get(CLICK_PARAM);
623
+ return { code: marked, clickId: click && CODE.test(click) ? click : null, direct: false };
624
+ }
625
+ const direct = get(param);
626
+ return direct && CODE.test(direct) ? { code: direct, clickId: null, direct: true } : null;
627
+ };
628
+ var carriesReferral = (event) => event === "$identify" || !event.startsWith("$");
629
+
630
+ // src/native/client.ts
631
+ var DEFAULT_HOST = "https://app.mrrocket.dev";
632
+ var KEYS = {
633
+ id: "mrrocket:id",
634
+ queue: "mrrocket:queue",
635
+ config: "mrrocket:config",
636
+ installed: "mrrocket:installed",
637
+ referral: "mrrocket:referral"
638
+ };
639
+ var MAX_ERRORS = 20;
640
+ var MAX_CRUMBS = 20;
641
+ var MAX_BACKOFF_MS = 5 * 60 * 1e3;
642
+ var REQUEST_TIMEOUT_MS = 15e3;
643
+ var EAGER_COUNT = 20;
644
+ var clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
645
+ var createClient = (options, deps) => {
646
+ const apiKey = (options.apiKey ?? "").trim();
647
+ const host = (options.host ?? DEFAULT_HOST).trim().replace(/\/+$/, "");
648
+ const now = deps.now ?? (() => Date.now());
649
+ const setIntervalFn = deps.setInterval ?? ((fn, ms) => setInterval(fn, ms));
650
+ const clearIntervalFn = deps.clearInterval ?? ((id) => clearInterval(id));
651
+ const setTimeoutFn = deps.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
652
+ const clearTimeoutFn = deps.clearTimeout ?? ((id) => clearTimeout(id));
653
+ const flushEvery = Math.max(1e3, options.flushIntervalMs ?? 1e4);
654
+ const maxQueue = Math.max(10, options.maxQueue ?? 1e3);
655
+ const errorsOn = options.errors !== false;
656
+ const storage = deps.storage;
657
+ const warned = /* @__PURE__ */ new Set();
658
+ const warn = (message) => {
659
+ if (warned.has(message)) return;
660
+ warned.add(message);
661
+ (deps.log ?? ((m) => console.warn(m)))(`[MRRocket] ${message}`);
662
+ };
663
+ const debug = (message) => {
664
+ if (options.debug) (deps.log ?? ((m) => console.log(m)))(`[MRRocket] ${message}`);
665
+ };
666
+ if (!apiKey) warn("No public key. Pass apiKey to MRRocketProvider; nothing is sent until then.");
667
+ let context = { platform: "unknown" };
668
+ try {
669
+ context = deps.context();
670
+ } catch {
671
+ }
672
+ const appId = (deps.appId || "app").replace(/[^A-Za-z0-9._-]/g, "") || "app";
673
+ const release = options.release ?? (context.appVersion ? `${context.appVersion}${context.build ? ` (${context.build})` : ""}` : void 0);
674
+ const sdk = { v: SDK_VERSION, errors: errorsOn, platform: context.platform };
675
+ let consent = options.consent ?? "anonymous";
676
+ let installId = null;
677
+ let memoryId = newDistinctId();
678
+ let identified;
679
+ let person;
680
+ let config = null;
681
+ let queue = [];
682
+ let session = null;
683
+ let utm = null;
684
+ let referral = null;
685
+ let currentScreen;
686
+ let loaded = false;
687
+ let closed = false;
688
+ let isReady = false;
689
+ let version = 0;
690
+ let failures = 0;
691
+ let backoffUntil = 0;
692
+ let inFlight = null;
693
+ let persistTimer = null;
694
+ let flushTimer = null;
695
+ let errorTimer = null;
696
+ let appState = deps.appState?.currentState ?? "active";
697
+ let backgroundSince = null;
698
+ const early = [];
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
+ };
706
+ const listeners = /* @__PURE__ */ new Set();
707
+ const crumbs = [];
708
+ const errorQueue = [];
709
+ const errorsSeen = /* @__PURE__ */ new Set();
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
+ };
731
+ const changed = () => {
732
+ version++;
733
+ for (const l of [...listeners]) {
734
+ try {
735
+ l();
736
+ } catch {
737
+ }
738
+ }
739
+ };
740
+ const crumb = (type, message) => {
741
+ crumbs.push({ ts: now(), type, message: clip2(message, 120) });
742
+ if (crumbs.length > MAX_CRUMBS) crumbs.shift();
743
+ };
744
+ const get = async (key) => {
745
+ if (!storage) return null;
746
+ try {
747
+ return await storage.getItem(key);
748
+ } catch {
749
+ return null;
750
+ }
751
+ };
752
+ const set = async (key, value) => {
753
+ if (!storage) return;
754
+ try {
755
+ await storage.setItem(key, value);
756
+ } catch {
757
+ warn("Could not write to the device storage; events wait in memory only.");
758
+ }
759
+ };
760
+ const remove = async (key) => {
761
+ if (!storage) return;
762
+ try {
763
+ await storage.removeItem(key);
764
+ } catch {
765
+ }
766
+ };
767
+ const persistQueue = async () => {
768
+ if (persistTimer !== null) {
769
+ clearTimeoutFn(persistTimer);
770
+ persistTimer = null;
771
+ }
772
+ if (!storage || consent === "off") return;
773
+ if (queue.length) await set(KEYS.queue, JSON.stringify(queue));
774
+ else await remove(KEYS.queue);
775
+ };
776
+ const schedulePersist = () => {
777
+ if (!storage || persistTimer !== null) return;
778
+ persistTimer = setTimeoutFn(() => {
779
+ persistTimer = null;
780
+ void persistQueue();
781
+ }, 1e3);
782
+ };
783
+ const distinctId = () => identified ?? (consent === "persistent" && installId ? installId : memoryId);
784
+ const sendId = () => consent === "persistent" ? distinctId() : identified;
785
+ const touchSession = () => {
786
+ const t = now();
787
+ if (!session || t - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t };
788
+ else session.last = t;
789
+ return session.id;
790
+ };
791
+ const urlOf = (path = currentScreen) => `app://${appId}${path ?? "/"}`;
792
+ const record = (event, props, ts = now()) => {
793
+ if (closed) return;
794
+ if (!loaded) {
795
+ early.push(() => record(event, props, ts));
796
+ return;
797
+ }
798
+ if (consent === "off" || !apiKey) return;
799
+ const sessionId = touchSession();
800
+ const ref = referral && carriesReferral(event) ? referral : null;
801
+ const size = deps.screen?.();
802
+ queue.push({
803
+ event,
804
+ props: ref ? { ...props, $referral: ref } : props,
805
+ distinctId: sendId(),
806
+ sessionId,
807
+ ts,
808
+ url: urlOf(),
809
+ ...utm && utm.session === sessionId ? utm.values : {},
810
+ ...size ? { screen: { w: Math.round(size.w), h: Math.round(size.h) } } : {},
811
+ app: context
812
+ });
813
+ if (queue.length > maxQueue) queue.splice(0, queue.length - maxQueue);
814
+ schedulePersist();
815
+ if (queue.length >= EAGER_COUNT) void flush();
816
+ };
817
+ const post = async (path, body) => {
818
+ const ctl = typeof AbortController !== "undefined" ? new AbortController() : null;
819
+ const timer = ctl ? setTimeoutFn(() => ctl.abort(), REQUEST_TIMEOUT_MS) : null;
820
+ try {
821
+ const r = await deps.fetch(`${host}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body, signal: ctl?.signal });
822
+ return r.status;
823
+ } catch {
824
+ return 0;
825
+ } finally {
826
+ if (timer !== null) clearTimeoutFn(timer);
827
+ }
828
+ };
829
+ const sendQueue = async () => {
830
+ if (!queue.length || consent === "off" || !apiKey) return;
831
+ if (now() < backoffUntil) return;
832
+ const snapshot = queue.slice();
833
+ const requests = batchRequests(apiKey, snapshot, sdk);
834
+ const included = new Set(requests.flatMap((r) => r.events));
835
+ const done = /* @__PURE__ */ new Set();
836
+ for (const r of requests) {
837
+ const status = await post("/api/sdk/events", r.body);
838
+ if (status >= 200 && status < 300) {
839
+ r.events.forEach((e) => done.add(e));
840
+ failures = 0;
841
+ backoffUntil = 0;
842
+ } else if (status >= 400 && status < 500 && status !== 408 && status !== 429) {
843
+ r.events.forEach((e) => done.add(e));
844
+ warn(`Events were rejected with ${status}. Check the public key (MRRocket \u203A Settings \u203A Products) and the host.`);
845
+ } else {
846
+ failures++;
847
+ backoffUntil = now() + Math.min(MAX_BACKOFF_MS, 1e3 * 2 ** failures);
848
+ debug(`Sending failed (${status || "offline"}); next try in ${Math.round((backoffUntil - now()) / 1e3)} s.`);
849
+ break;
850
+ }
851
+ }
852
+ queue = queue.filter((e) => !done.has(e) && (included.has(e) || !snapshot.includes(e)));
853
+ await persistQueue();
854
+ };
855
+ let nextFlush = null;
856
+ const flush = () => {
857
+ if (inFlight)
858
+ return nextFlush ?? (nextFlush = inFlight.then(() => {
859
+ nextFlush = null;
860
+ return flush();
861
+ }));
862
+ inFlight = sendQueue().finally(() => {
863
+ inFlight = null;
864
+ });
865
+ return inFlight;
866
+ };
867
+ const startTimer = () => {
868
+ if (flushTimer !== null || closed) return;
869
+ flushTimer = setIntervalFn(() => void flush(), flushEvery);
870
+ };
871
+ const stopTimer = () => {
872
+ if (flushTimer === null) return;
873
+ clearIntervalFn(flushTimer);
874
+ flushTimer = null;
875
+ };
876
+ const sendErrors = async () => {
877
+ if (errorTimer !== null) {
878
+ clearTimeoutFn(errorTimer);
879
+ errorTimer = null;
880
+ }
881
+ while (errorQueue.length) {
882
+ const batch = errorQueue.splice(0, 4);
883
+ const status = await post("/api/sdk/errors", JSON.stringify({ key: apiKey, sdk, errors: batch }));
884
+ if (status >= 400 && status < 500 && status !== 429) warn(`Error reports were rejected with ${status}. Check the public key and the host.`);
885
+ }
886
+ };
887
+ const capture = (error, mechanism, fatal = false) => {
888
+ if (!errorsOn || closed || consent === "off" || !apiKey || errorsSent >= MAX_ERRORS) return;
889
+ const d = describeError(error);
890
+ if (/\/api\/sdk\/errors/.test(d.message)) return;
891
+ const mark = `${d.type}:${d.message}`;
892
+ if (errorsSeen.has(mark)) return;
893
+ errorsSeen.add(mark);
894
+ errorsSent++;
895
+ const who = sendId();
896
+ errorQueue.push({
897
+ ...d,
898
+ mechanism,
899
+ handled: mechanism === "manual",
900
+ url: urlOf(),
901
+ ts: now(),
902
+ crumbs: crumbs.slice(),
903
+ ...release ? { release } : {},
904
+ ...who ? { distinctId: who } : {},
905
+ ...session ? { sessionId: session.id } : {}
906
+ });
907
+ if (fatal) void sendErrors();
908
+ else if (errorTimer === null) errorTimer = setTimeoutFn(() => void sendErrors(), 2e3);
909
+ };
910
+ let previousHandler;
911
+ let handlerInstalled = false;
912
+ const installErrorHandler = () => {
913
+ if (!errorsOn || !deps.errorUtils || handlerInstalled) return;
914
+ handlerInstalled = true;
915
+ previousHandler = deps.errorUtils.getGlobalHandler();
916
+ deps.errorUtils.setGlobalHandler((error, isFatal) => {
917
+ try {
918
+ capture(error, "onerror", !!isFatal);
919
+ } catch {
920
+ }
921
+ previousHandler?.(error, isFatal);
922
+ });
923
+ };
924
+ let appStateSub;
925
+ const onAppState = (next) => {
926
+ if (closed) return;
927
+ const prev = appState;
928
+ appState = next;
929
+ if (next === "active" && prev !== "active") {
930
+ startTimer();
931
+ if (backgroundSince !== null) {
932
+ backgroundSince = null;
933
+ crumb("lifecycle", "app_open");
934
+ if (options.lifecycle !== false) record("$app_open", { cold: false });
935
+ }
936
+ } else if (next === "background" && prev !== "background") {
937
+ backgroundSince = now();
938
+ crumb("lifecycle", "app_background");
939
+ if (options.lifecycle !== false) record("$app_background");
940
+ stopTimer();
941
+ void persistQueue();
942
+ void flush();
943
+ if (errorQueue.length) void sendErrors();
944
+ }
945
+ };
946
+ const fetchConfig = async () => {
947
+ if (!apiKey) return;
948
+ try {
949
+ const r = await deps.fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}`, { method: "GET" });
950
+ if (!r.ok) {
951
+ warn(`Config request answered ${r.status}. Check the public key and the host.`);
952
+ return;
953
+ }
954
+ const c = await r.json();
955
+ if (c && Array.isArray(c.experiments)) {
956
+ config = c;
957
+ if (consent !== "off") await set(KEYS.config, JSON.stringify(c));
958
+ changed();
959
+ }
960
+ } catch {
961
+ debug("Config not reachable; using the cached one.");
962
+ }
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
+ };
1019
+ let resolveReady = () => {
1020
+ };
1021
+ const ready = new Promise((r) => {
1022
+ resolveReady = r;
1023
+ });
1024
+ const markReady = () => {
1025
+ if (isReady) return;
1026
+ isReady = true;
1027
+ resolveReady();
1028
+ changed();
1029
+ };
1030
+ const start = async () => {
1031
+ let firstOpen = false;
1032
+ if (consent !== "off") {
1033
+ const [id, stored, cached, installed, ref] = await Promise.all([
1034
+ get(KEYS.id),
1035
+ get(KEYS.queue),
1036
+ get(KEYS.config),
1037
+ get(KEYS.installed),
1038
+ get(KEYS.referral)
1039
+ ]);
1040
+ if (consent === "persistent") {
1041
+ installId = id || newDistinctId();
1042
+ if (!id) await set(KEYS.id, installId);
1043
+ } else if (id) await remove(KEYS.id);
1044
+ try {
1045
+ const old = stored ? JSON.parse(stored) : [];
1046
+ if (Array.isArray(old)) queue = [...old.filter((e) => e && typeof e.event === "string"), ...queue].slice(-maxQueue);
1047
+ } catch {
1048
+ await remove(KEYS.queue);
1049
+ }
1050
+ try {
1051
+ const c = cached ? JSON.parse(cached) : null;
1052
+ if (c && Array.isArray(c.experiments) && !config) config = c;
1053
+ } catch {
1054
+ await remove(KEYS.config);
1055
+ }
1056
+ try {
1057
+ const r = ref && consent === "persistent" ? JSON.parse(ref) : null;
1058
+ if (r && typeof r.code === "string") referral = r;
1059
+ } catch {
1060
+ }
1061
+ firstOpen = !!storage && !installed;
1062
+ if (firstOpen) await set(KEYS.installed, String(now()));
1063
+ } else {
1064
+ await Promise.all(Object.values(KEYS).map(remove));
1065
+ }
1066
+ if (closed) return;
1067
+ loaded = true;
1068
+ if (config) markReady();
1069
+ let install = null;
1070
+ if (firstOpen) {
1071
+ install = {};
1072
+ if (options.attribution !== false && deps.initialUrl) {
1073
+ const url = await deps.initialUrl().catch(() => null);
1074
+ if (url) {
1075
+ const q = parseQuery(url);
1076
+ const values = utmFrom((k) => q[k]);
1077
+ if (Object.keys(values).length) utm = { session: touchSession(), values };
1078
+ const found = referralFrom((k) => q[k]);
1079
+ if (found) {
1080
+ referral = { code: found.code, clickId: found.clickId };
1081
+ install.$referral = referral;
1082
+ if (consent === "persistent") await set(KEYS.referral, JSON.stringify(referral));
1083
+ }
1084
+ install.link = clip2(url.split("?")[0], 300);
1085
+ }
1086
+ }
1087
+ }
1088
+ crumb("lifecycle", "app_open");
1089
+ if (options.lifecycle !== false) record("$app_open", { cold: true });
1090
+ if (install) record("$install", install);
1091
+ for (const run of early.splice(0)) run();
1092
+ if (deps.appState) appStateSub = deps.appState.addEventListener("change", onAppState);
1093
+ installErrorHandler();
1094
+ listenForOpens();
1095
+ if (appState === "active" || !deps.appState) startTimer();
1096
+ if (queue.length) void flush();
1097
+ await fetchConfig();
1098
+ markReady();
1099
+ refreshFlows();
1100
+ };
1101
+ void start().catch(() => {
1102
+ loaded = true;
1103
+ markReady();
1104
+ });
1105
+ const client = {
1106
+ track: (event, props) => {
1107
+ if (typeof event !== "string" || !event) return;
1108
+ record(event, props);
1109
+ flows.onEvent(event);
1110
+ },
1111
+ screen: (name, props) => {
1112
+ if (typeof name !== "string" || !name.trim()) return;
1113
+ const path = (name.startsWith("/") ? name : `/${name}`).slice(0, 300);
1114
+ if (path === currentScreen && !props) return;
1115
+ const previous = currentScreen;
1116
+ currentScreen = path;
1117
+ crumb("navigation", path);
1118
+ record("$screen", { ...props, path, screen: name, ...previous ? { previous } : {} });
1119
+ },
1120
+ identify: (id, props) => {
1121
+ if (typeof id !== "string" || !id) return;
1122
+ if (!loaded) {
1123
+ early.push(() => client.identify(id, props));
1124
+ return;
1125
+ }
1126
+ const previous = sendId();
1127
+ identified = id;
1128
+ person = { ...props, id };
1129
+ record("$identify", { ...props, id, ...previous && previous !== id ? { previousId: previous } : {} });
1130
+ if (consent === "persistent") void set(KEYS.id, id);
1131
+ changed();
1132
+ refreshFlows();
1133
+ },
1134
+ reset: () => {
1135
+ if (!loaded) {
1136
+ early.push(() => client.reset());
1137
+ return;
1138
+ }
1139
+ if (pushToken) void unregisterForPush();
1140
+ identified = void 0;
1141
+ person = void 0;
1142
+ session = null;
1143
+ utm = null;
1144
+ exposed.clear();
1145
+ memoryId = newDistinctId();
1146
+ if (consent === "persistent") {
1147
+ installId = newDistinctId();
1148
+ void set(KEYS.id, installId);
1149
+ }
1150
+ changed();
1151
+ refreshFlows();
1152
+ },
1153
+ setConsent: (next) => {
1154
+ if (next !== "anonymous" && next !== "persistent" && next !== "off") {
1155
+ warn(`setConsent: "${String(next)}" is not a consent mode. Use "anonymous", "persistent" or "off".`);
1156
+ return;
1157
+ }
1158
+ if (!loaded) {
1159
+ early.push(() => client.setConsent(next));
1160
+ return;
1161
+ }
1162
+ if (next === consent) return;
1163
+ const before = consent;
1164
+ consent = next;
1165
+ if (next === "off") {
1166
+ queue = [];
1167
+ session = null;
1168
+ errorQueue.length = 0;
1169
+ void Promise.all(Object.values(KEYS).map(remove));
1170
+ } else if (next === "anonymous") {
1171
+ installId = null;
1172
+ void remove(KEYS.id);
1173
+ void remove(KEYS.referral);
1174
+ } else {
1175
+ installId = identified ?? newDistinctId();
1176
+ void set(KEYS.id, installId);
1177
+ if (referral) void set(KEYS.referral, JSON.stringify(referral));
1178
+ }
1179
+ if (before === "off" && currentScreen && loaded) record("$screen", { path: currentScreen, screen: currentScreen });
1180
+ changed();
1181
+ refreshFlows();
1182
+ },
1183
+ captureError: (error) => capture(error, "manual"),
1184
+ getFlag: (key) => flagFor(key, config, distinctId(), { ...person }) !== false,
1185
+ getFlagVariant: (key) => {
1186
+ const v = flagFor(key, config, distinctId(), { ...person });
1187
+ return typeof v === "string" ? v : null;
1188
+ },
1189
+ getVariant: (key) => {
1190
+ const variant = resolveVariant(key, config, distinctId(), {});
1191
+ client.expose(key, variant);
1192
+ return variant;
1193
+ },
1194
+ peekVariant: (key) => resolveVariant(key, config, distinctId(), {}),
1195
+ expose: (key, variant) => {
1196
+ if (!config || consent === "off") return;
1197
+ const mark = `${currentScreen ?? "/"}|${key}`;
1198
+ if (exposed.has(mark)) return;
1199
+ exposed.add(mark);
1200
+ record("$exposure", { experiment: key, variant });
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,
1215
+ flush,
1216
+ ready,
1217
+ isReady: () => isReady,
1218
+ getDistinctId: distinctId,
1219
+ getConsent: () => consent,
1220
+ getConfig: () => config,
1221
+ subscribe: (listener) => {
1222
+ listeners.add(listener);
1223
+ return () => listeners.delete(listener);
1224
+ },
1225
+ getVersion: () => version,
1226
+ close: async () => {
1227
+ if (closed) return;
1228
+ stopTimer();
1229
+ appStateSub?.remove();
1230
+ pushOff?.();
1231
+ pushOff = null;
1232
+ if (handlerInstalled && deps.errorUtils && previousHandler) deps.errorUtils.setGlobalHandler(previousHandler);
1233
+ await flush();
1234
+ await sendErrors();
1235
+ await persistQueue();
1236
+ closed = true;
1237
+ },
1238
+ queued: () => queue
1239
+ };
1240
+ return client;
1241
+ };
1242
+ var activeRouteName = (state) => {
1243
+ let s = state;
1244
+ let name;
1245
+ for (let depth = 0; s && Array.isArray(s.routes) && s.routes.length && depth < 20; depth++) {
1246
+ const route = s.routes[typeof s.index === "number" ? s.index : s.routes.length - 1];
1247
+ if (!route) break;
1248
+ name = route.name ?? name;
1249
+ s = route.state;
1250
+ }
1251
+ return name;
1252
+ };
1253
+
1254
+ // src/native/flows-view.ts
1255
+ var import_react = require("react");
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");
1500
+ var asyncStorageModule = null;
1501
+ try {
1502
+ asyncStorageModule = require("@react-native-async-storage/async-storage");
1503
+ } catch {
1504
+ asyncStorageModule = null;
1505
+ }
1506
+ var expoDevice = null;
1507
+ try {
1508
+ expoDevice = require("expo-device");
1509
+ } catch {
1510
+ expoDevice = null;
1511
+ }
1512
+ var expoApplication = null;
1513
+ try {
1514
+ expoApplication = require("expo-application");
1515
+ } catch {
1516
+ expoApplication = null;
1517
+ }
1518
+ var expoLocalization = null;
1519
+ try {
1520
+ expoLocalization = require("expo-localization");
1521
+ } catch {
1522
+ expoLocalization = null;
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
+ };
1563
+ var asyncStorage = () => {
1564
+ const m = asyncStorageModule;
1565
+ const s = m?.default ?? m;
1566
+ return s && typeof s.getItem === "function" && typeof s.setItem === "function" && typeof s.removeItem === "function" ? s : null;
1567
+ };
1568
+ var DEVICE_TYPES = { 1: "phone", 2: "tablet", 3: "desktop", 4: "tv" };
1569
+ var text = (v, max = 80) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : typeof v === "number" ? String(v) : void 0;
1570
+ var readContext = () => {
1571
+ const p = RN2.Platform;
1572
+ let locale;
1573
+ try {
1574
+ locale = text(expoLocalization?.getLocales?.()[0]?.languageTag, 35);
1575
+ } catch {
1576
+ locale = void 0;
1577
+ }
1578
+ if (!locale) {
1579
+ try {
1580
+ locale = text(Intl.DateTimeFormat().resolvedOptions().locale, 35);
1581
+ } catch {
1582
+ locale = void 0;
1583
+ }
1584
+ }
1585
+ const ctx = {
1586
+ platform: p.OS,
1587
+ appVersion: text(expoApplication?.nativeApplicationVersion),
1588
+ build: text(expoApplication?.nativeBuildVersion),
1589
+ osVersion: text(expoDevice?.osVersion) ?? text(p.Version),
1590
+ deviceModel: text(expoDevice?.modelName) ?? text(p.constants?.Model),
1591
+ deviceType: (expoDevice?.deviceType ? DEVICE_TYPES[expoDevice.deviceType] : void 0) ?? (p.isPad ? "tablet" : void 0),
1592
+ locale
1593
+ };
1594
+ for (const k of Object.keys(ctx)) if (ctx[k] === void 0) delete ctx[k];
1595
+ return ctx;
1596
+ };
1597
+ var nativeDeps = (overrides = {}) => ({
1598
+ storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
1599
+ fetch: ((url, init) => fetch(url, init)),
1600
+ appState: RN2.AppState,
1601
+ errorUtils: globalThis.ErrorUtils ?? null,
1602
+ context: readContext,
1603
+ appId: text(expoApplication?.applicationId, 150),
1604
+ screen: () => {
1605
+ const s = RN2.Dimensions.get("screen");
1606
+ return s ? { w: s.width, h: s.height } : void 0;
1607
+ },
1608
+ initialUrl: () => RN2.Linking.getInitialURL(),
1609
+ openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
1610
+ push: expoPush()
1611
+ });
1612
+ var optionalModules = () => ({
1613
+ asyncStorage: !!asyncStorage(),
1614
+ expoDevice: !!expoDevice,
1615
+ expoApplication: !!expoApplication,
1616
+ expoLocalization: !!expoLocalization,
1617
+ expoNotifications: !!expoNotifications
1618
+ });
1619
+
1620
+ // src/native/index.tsx
1621
+ var shared = null;
1622
+ var sharedKey = "";
1623
+ var createMRRocket = (options) => {
1624
+ if (shared) {
1625
+ if (sharedKey !== options.apiKey) console.warn("[MRRocket] The SDK is already running with another key; keeping the first one.");
1626
+ return shared;
1627
+ }
1628
+ const { storage, ...rest } = options;
1629
+ const deps = nativeDeps({ storage });
1630
+ shared = createClient(rest, deps);
1631
+ sharedKey = options.apiKey;
1632
+ if (options.debug) console.log("[MRRocket] optional modules", JSON.stringify(optionalModules()));
1633
+ return shared;
1634
+ };
1635
+ var getMRRocket = () => shared;
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)(() => {
1640
+ if (consent) client.setConsent(consent);
1641
+ }, [client, consent]);
1642
+ return (0, import_react2.createElement)(Ctx.Provider, { value: client }, children, flows ? (0, import_react2.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null);
1643
+ };
1644
+ var useClient = () => {
1645
+ const c = (0, import_react2.useContext)(Ctx);
1646
+ if (!c) throw new Error("MRRocket: wrap your app in <MRRocketProvider apiKey=\u2026>.");
1647
+ return c;
1648
+ };
1649
+ var useVersion = (client) => (0, import_react2.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
1650
+ var useMRRocket = () => {
1651
+ const client = useClient();
1652
+ useVersion(client);
1653
+ return {
1654
+ track: client.track,
1655
+ screen: client.screen,
1656
+ identify: client.identify,
1657
+ reset: client.reset,
1658
+ setConsent: client.setConsent,
1659
+ captureError: client.captureError,
1660
+ flush: client.flush,
1661
+ distinctId: client.getDistinctId(),
1662
+ consent: client.getConsent(),
1663
+ ready: client.isReady()
1664
+ };
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
+ };
1678
+ var useFlag = (key) => {
1679
+ const client = useClient();
1680
+ useVersion(client);
1681
+ return client.getFlag(key);
1682
+ };
1683
+ var useFlagVariant = (key) => {
1684
+ const client = useClient();
1685
+ useVersion(client);
1686
+ return client.getFlagVariant(key);
1687
+ };
1688
+ var useVariant = (key) => {
1689
+ const client = useClient();
1690
+ useVersion(client);
1691
+ const config = client.getConfig();
1692
+ const variant = client.peekVariant(key);
1693
+ (0, import_react2.useEffect)(() => {
1694
+ if (config) client.expose(key, variant);
1695
+ }, [client, config, key, variant]);
1696
+ return variant;
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
+ };
1709
+ var expoRouter;
1710
+ var loadExpoRouter = () => {
1711
+ if (expoRouter !== void 0) return expoRouter;
1712
+ try {
1713
+ expoRouter = require("expo-router");
1714
+ } catch {
1715
+ expoRouter = null;
1716
+ }
1717
+ return expoRouter;
1718
+ };
1719
+ var noPathname = () => void 0;
1720
+ var routerWarned = false;
1721
+ var useExpoRouterTracking = () => {
1722
+ const client = useClient();
1723
+ const router = loadExpoRouter();
1724
+ if (router === null && !routerWarned) {
1725
+ routerWarned = true;
1726
+ console.warn("[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.");
1727
+ }
1728
+ const pathname = (router?.usePathname ?? noPathname)();
1729
+ (0, import_react2.useEffect)(() => {
1730
+ if (pathname) client.screen(pathname);
1731
+ }, [client, pathname]);
1732
+ };
1733
+ var useNavigationTracking = () => {
1734
+ const client = useClient();
1735
+ return (0, import_react2.useCallback)(
1736
+ (state) => {
1737
+ const name = activeRouteName(state);
1738
+ if (name) client.screen(name);
1739
+ },
1740
+ [client]
1741
+ );
1742
+ };
1743
+ //# sourceMappingURL=index.js.map