@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.
- package/CHANGELOG.md +30 -0
- package/README.md +102 -7
- package/dist/api.d.ts +23 -3
- package/dist/api.js +7 -3
- package/dist/api.js.map +1 -1
- package/dist/browser/mrrocket-flows.js +64 -0
- package/dist/browser/mrrocket-push.js +1 -0
- package/dist/browser/mrrocket-replay.js +3 -3
- package/dist/browser/mrrocket-sw.js +1 -0
- package/dist/browser/mrrocket.js +2 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +3 -1
- package/dist/browser.js.map +1 -1
- package/dist/client.d.ts +36 -1
- package/dist/client.js +161 -6
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +10 -0
- package/dist/context.js.map +1 -1
- package/dist/core.d.ts +64 -3
- package/dist/core.js +54 -2
- package/dist/core.js.map +1 -1
- package/dist/feedback.d.ts +19 -4
- package/dist/feedback.js +652 -190
- package/dist/feedback.js.map +1 -1
- package/dist/flows-ui.d.ts +23 -0
- package/dist/flows-ui.js +384 -0
- package/dist/flows-ui.js.map +1 -0
- package/dist/flows-web.d.ts +19 -0
- package/dist/flows-web.js +42 -0
- package/dist/flows-web.js.map +1 -0
- package/dist/flows.d.ts +222 -0
- package/dist/flows.js +409 -0
- package/dist/flows.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +34 -1
- package/dist/proxy.js +14 -1
- package/dist/proxy.js.map +1 -1
- package/dist/push-web.d.ts +43 -0
- package/dist/push-web.js +81 -0
- package/dist/push-web.js.map +1 -0
- package/dist/react-native/index.d.cts +305 -3
- package/dist/react-native/index.d.ts +305 -3
- package/dist/react-native/index.js +852 -31
- package/dist/react-native/index.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +127 -24
- package/dist/styles.js.map +1 -1
- package/dist/ui.d.ts +3 -0
- package/dist/ui.js +12 -0
- package/dist/ui.js.map +1 -1
- 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
|
|
41
|
+
var import_react2 = require("react");
|
|
37
42
|
|
|
38
43
|
// src/core.ts
|
|
39
|
-
var SDK_VERSION = "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
|
|
117
|
+
let h2 = 2166136261;
|
|
113
118
|
for (let i = 0; i < input.length; i++) {
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
h2 ^= input.charCodeAt(i);
|
|
120
|
+
h2 = Math.imul(h2, 16777619);
|
|
116
121
|
}
|
|
117
|
-
return (
|
|
122
|
+
return (h2 >>> 0) / 4294967296;
|
|
118
123
|
};
|
|
119
124
|
var flagUnit = (input) => {
|
|
120
|
-
let
|
|
125
|
+
let h2 = 2166136261;
|
|
121
126
|
for (let i = 0; i < input.length; i++) {
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
h2 ^= input.charCodeAt(i);
|
|
128
|
+
h2 = Math.imul(h2, 16777619);
|
|
124
129
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return (
|
|
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/
|
|
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 =
|
|
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:
|
|
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 =
|
|
1605
|
+
const s = RN2.Dimensions.get("screen");
|
|
811
1606
|
return s ? { w: s.width, h: s.height } : void 0;
|
|
812
1607
|
},
|
|
813
|
-
initialUrl: () =>
|
|
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,
|
|
839
|
-
var MRRocketProvider = ({ client: given, children, consent, ...options }) => {
|
|
840
|
-
const [client] = (0,
|
|
841
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1735
|
+
return (0, import_react2.useCallback)(
|
|
915
1736
|
(state) => {
|
|
916
1737
|
const name = activeRouteName(state);
|
|
917
1738
|
if (name) client.screen(name);
|