@mrrocket/sdk 0.17.0 → 0.18.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 (43) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/dist/browser/mrrocket-flows.js +9 -2
  3. package/dist/browser/mrrocket-messenger.js +3 -3
  4. package/dist/browser/mrrocket.js +1 -1
  5. package/dist/browser.d.ts +1 -1
  6. package/dist/browser.js +2 -1
  7. package/dist/browser.js.map +1 -1
  8. package/dist/client.d.ts +23 -3
  9. package/dist/client.js +15 -1
  10. package/dist/client.js.map +1 -1
  11. package/dist/context.d.ts +3 -1
  12. package/dist/context.js.map +1 -1
  13. package/dist/core.d.ts +3 -1
  14. package/dist/core.js +1 -1
  15. package/dist/core.js.map +1 -1
  16. package/dist/finish-web.d.ts +18 -0
  17. package/dist/finish-web.js +139 -0
  18. package/dist/finish-web.js.map +1 -0
  19. package/dist/flows-ui.d.ts +1 -10
  20. package/dist/flows-ui.js +105 -6
  21. package/dist/flows-ui.js.map +1 -1
  22. package/dist/flows-web.d.ts +8 -1
  23. package/dist/flows-web.js +8 -0
  24. package/dist/flows-web.js.map +1 -1
  25. package/dist/flows.d.ts +159 -7
  26. package/dist/flows.js +240 -28
  27. package/dist/flows.js.map +1 -1
  28. package/dist/icons.d.ts +24 -0
  29. package/dist/icons.js +61 -0
  30. package/dist/icons.js.map +1 -0
  31. package/dist/index.d.ts +4 -2
  32. package/dist/index.js +5 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/messenger-ui.js +78 -53
  35. package/dist/messenger-ui.js.map +1 -1
  36. package/dist/onboarding-state.d.ts +23 -0
  37. package/dist/onboarding-state.js +43 -0
  38. package/dist/onboarding-state.js.map +1 -0
  39. package/dist/react-native/index.d.cts +177 -15
  40. package/dist/react-native/index.d.ts +177 -15
  41. package/dist/react-native/index.js +776 -270
  42. package/dist/react-native/index.js.map +1 -1
  43. package/package.json +1 -1
package/dist/flows.js CHANGED
@@ -24,7 +24,22 @@ var matchesTrigger = (trigger, event) => {
24
24
  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());
25
25
  var FROM_CHECKLIST = "From a checklist";
26
26
  var isPageTrigger = (trigger) => trigger.trim().startsWith("/");
27
- var manual = (trigger) => eventSlug(trigger) === "from_a_checklist";
27
+ var FROM_CODE = "From your code";
28
+ var NEEDS_NEWER = "Needs tracking code 0.18.0";
29
+ var momentOf = (f) => f.when ?? f.trigger;
30
+ var manual = (trigger) => eventSlug(trigger) === "from_a_checklist" || eventSlug(trigger) === "from_your_code" || eventSlug(trigger) === eventSlug(NEEDS_NEWER);
31
+ var AFTER_PREFIX = "after:";
32
+ var afterOf = (trigger) => {
33
+ const t = (trigger ?? "").trim();
34
+ return t.toLowerCase().startsWith(AFTER_PREFIX) ? t.slice(AFTER_PREFIX.length).trim() || null : null;
35
+ };
36
+ var opensOf = (step) => step.opens ?? (step.startsTour ? { kind: "tour", id: step.startsTour } : void 0);
37
+ var answerOk = (q, value) => {
38
+ if (!q || value == null) return false;
39
+ if (q.type === "choice") return (q.options ?? []).some((o) => o.id === value);
40
+ return value.trim().length > 0 && value.length <= 500;
41
+ };
42
+ var answerLabel = (q, value) => q?.type === "choice" ? q.options?.find((o) => o.id === value)?.label ?? value : value.trim();
28
43
  var videoEmbed = (url) => {
29
44
  const u = (url ?? "").trim();
30
45
  if (!/^https?:\/\//i.test(u)) return null;
@@ -90,7 +105,12 @@ var createFlows = (env) => {
90
105
  const now = () => env.now?.() ?? Date.now();
91
106
  let timer = null;
92
107
  let returnTo = null;
108
+ const answers = /* @__PURE__ */ new Map();
109
+ const drafts = /* @__PURE__ */ new Map();
110
+ const celebrated = /* @__PURE__ */ new Set();
111
+ let finaleNow = null;
93
112
  const flows = () => config?.onboarding ?? [];
113
+ const drawn = () => flows().filter((f) => f.shownBy !== "code");
94
114
  const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
95
115
  const prog = (id) => {
96
116
  let p = progress.get(id);
@@ -164,6 +184,14 @@ var createFlows = (env) => {
164
184
  };
165
185
  const keyOf = (pid) => `${PROGRESS_KEY}:${pid ?? "visitor"}`;
166
186
  const eventsKeyOf = (pid) => `${PROGRESS_KEY}:events:${pid ?? "visitor"}`;
187
+ const answersKeyOf = (pid) => `${PROGRESS_KEY}:answers:${pid ?? "visitor"}`;
188
+ const celebrate = (f) => {
189
+ if (!f.finish || f.finish === "none" || f.shownBy === "code" || celebrated.has(f.id)) return;
190
+ const p = progress.get(f.id);
191
+ if (!p || p.dismissed || walkLine(f, nodesOf(f), p, false).at !== "end") return;
192
+ celebrated.add(f.id);
193
+ finaleNow = { kind: f.finish, key: `${f.id}:${now()}` };
194
+ };
167
195
  const save = () => {
168
196
  dirty = false;
169
197
  const store = env.storage?.();
@@ -179,6 +207,7 @@ var createFlows = (env) => {
179
207
  };
180
208
  try {
181
209
  store.set(keyOf(env.personId()), JSON.stringify(out));
210
+ if (answers.size) store.set(answersKeyOf(env.personId()), JSON.stringify(Object.fromEntries(answers)));
182
211
  if (seenEvents.size) store.set(eventsKeyOf(env.personId()), JSON.stringify([...seenEvents].slice(-200)));
183
212
  } catch {
184
213
  }
@@ -231,8 +260,17 @@ var createFlows = (env) => {
231
260
  type: "checklist",
232
261
  flow,
233
262
  items: items.map((s) => {
234
- const tour = s.startsTour && s.startsTour !== flow.id ? flowById(s.startsTour) : void 0;
235
- return { step: s, done: p.done.has(s.id), ...tour && nodesOf(tour).length ? { tour } : {} };
263
+ const o = opensOf(s);
264
+ const t = o && (o.kind === "tour" || o.kind === "modal") && o.id !== flow.id ? flowById(o.id) : void 0;
265
+ const target = t && t.shownBy !== "code" && nodesOf(t).length ? t : void 0;
266
+ const opens = o && (o.kind === "page" ? !!o.target.trim() : o.kind === "code" || !!target) ? o : void 0;
267
+ return {
268
+ step: s,
269
+ done: p.done.has(s.id),
270
+ ...target && o?.kind === "tour" ? { tour: target } : {},
271
+ ...opens ? { opens } : {},
272
+ ...target ? { target } : {}
273
+ };
236
274
  })
237
275
  };
238
276
  }
@@ -243,7 +281,16 @@ var createFlows = (env) => {
243
281
  const rest = line.slice(index + 1);
244
282
  const fork = rest.findIndex((n) => n.kind === "condition");
245
283
  const ahead = (fork < 0 ? rest : rest.slice(0, fork)).filter(isStep).length;
246
- return { type: "step", flow, step, index: w.before, total: w.before + 1 + ahead, last: !ahead && fork < 0 };
284
+ return {
285
+ type: "step",
286
+ flow,
287
+ step,
288
+ index: w.before,
289
+ total: w.before + 1 + ahead,
290
+ last: !ahead && fork < 0,
291
+ // 0.18.0: a question's answer so far, and whether there is one before it to go back to.
292
+ ...step.kind === "question" ? { answer: drafts.get(step.id) ?? (step.question ? answers.get(step.question.key) : void 0), back: w.before > 0 } : {}
293
+ };
247
294
  };
248
295
  const view = () => {
249
296
  if (cancel) {
@@ -270,23 +317,25 @@ var createFlows = (env) => {
270
317
  if (ob) return ob;
271
318
  if (active && !holding) {
272
319
  const back = returnTo;
273
- const ended = active;
320
+ const ended2 = active;
274
321
  returnTo = null;
275
322
  active = null;
276
323
  const list = back ? flowById(back.flow) : void 0;
277
324
  if (back && list) {
278
325
  const item = stepsIn(nodesOf(list)).find((s) => s.id === back.step);
279
326
  const p = prog(list.id);
280
- if (item && !item.event && !progress.get(ended)?.dismissed && !p.done.has(item.id)) {
327
+ if (item && !item.event && !progress.get(ended2)?.dismissed && !p.done.has(item.id)) {
281
328
  markDone(p, item.id);
282
329
  record({ flow: list.id, step: item.id, kind: "checklist", action: "completed" });
330
+ celebrate(list);
283
331
  }
284
332
  active = list.id;
285
333
  const again = onboardingView();
286
334
  if (again) return again;
287
335
  if (!holding) active = null;
288
336
  } else if (known) {
289
- const after = flowById(ended)?.trigger;
337
+ const was = flowById(ended2);
338
+ const after = was ? momentOf(was) : void 0;
290
339
  Promise.resolve().then(() => autoStart(after));
291
340
  }
292
341
  }
@@ -314,22 +363,64 @@ var createFlows = (env) => {
314
363
  emit();
315
364
  return true;
316
365
  };
317
- const allowed = (f) => f.audience !== "signed_in" || !!env.personId();
366
+ const allowed = (f) => f.audience === "answered" ? !!f.answered && answers.get(f.answered.question) === f.answered.option : f.audience !== "signed_in" || !!env.personId();
367
+ const predecessorEnded = (id) => {
368
+ const g = flowById(id);
369
+ return !g || finished(g);
370
+ };
371
+ const due = (f, trigger = momentOf(f)) => {
372
+ const path = env.path?.();
373
+ const pred = afterOf(trigger);
374
+ if (pred) return predecessorEnded(pred);
375
+ if (manual(trigger)) return false;
376
+ if (isPageTrigger(trigger)) return path !== void 0 && pageMatches(trigger, path);
377
+ if (startsOnLoad(trigger)) return true;
378
+ for (const e of seenEvents) if (matchesTrigger(trigger, e)) return true;
379
+ return false;
380
+ };
318
381
  const autoStart = (after) => {
319
382
  if (!auto || active || !known) return;
320
383
  if (!env.personId() && !env.storage?.()) return;
321
384
  const path = env.path?.();
322
385
  const same = after && !manual(after) ? eventSlug(after) : "";
323
- const next = flows().find(
324
- (f) => (isPageTrigger(f.trigger) ? path !== void 0 && pageMatches(f.trigger, path) : startsOnLoad(f.trigger) || !!same && eventSlug(f.trigger) === same) && allowed(f) && nodesOf(f).length && !finished(f)
386
+ const next = drawn().find(
387
+ (f) => (afterOf(momentOf(f)) ? predecessorEnded(afterOf(momentOf(f))) : isPageTrigger(momentOf(f)) ? path !== void 0 && pageMatches(momentOf(f), path) : startsOnLoad(momentOf(f)) || !!same && eventSlug(momentOf(f)) === same) && allowed(f) && nodesOf(f).length && !finished(f)
325
388
  );
326
389
  if (next) start(next);
327
390
  };
391
+ const openItem = (list, stepId, target) => {
392
+ const p = prog(target.id);
393
+ p.done.clear();
394
+ p.at.clear();
395
+ p.dismissed = false;
396
+ returnTo = { flow: list.id, step: stepId };
397
+ active = target.id;
398
+ env.track(ONBOARDING_EVENT, { flow: target.id, action: "started", from: list.id });
399
+ save();
400
+ emit();
401
+ };
402
+ const answerStep = (flow, step, raw) => {
403
+ const q = step.question;
404
+ const value = q.type === "text" ? raw.trim().slice(0, 500) : raw;
405
+ answers.set(q.key, value);
406
+ drafts.delete(step.id);
407
+ markDone(prog(flow.id), step.id);
408
+ env.track(ONBOARDING_EVENT, { flow: flow.id, step: step.id, kind: "question", action: "completed", question: q.key, answer: value, label: answerLabel(q, value) });
409
+ celebrate(flow);
410
+ save();
411
+ };
412
+ const ended = (f) => {
413
+ save();
414
+ if (finished(f)) Promise.resolve().then(() => autoStart());
415
+ emit();
416
+ };
328
417
  const refresh = () => {
329
418
  const pid = env.personId();
330
419
  if (progressFor !== null && progressFor !== pid) {
331
420
  progress.clear();
332
421
  seenEvents.clear();
422
+ answers.clear();
423
+ drafts.clear();
333
424
  handled.clear();
334
425
  active = null;
335
426
  returnTo = null;
@@ -346,6 +437,10 @@ var createFlows = (env) => {
346
437
  if (seen) {
347
438
  for (const e of JSON.parse(seen)) if (typeof e === "string") seenEvents.add(e);
348
439
  }
440
+ const said = await store.get(answersKeyOf(pid));
441
+ if (said) {
442
+ for (const [k, v] of Object.entries(JSON.parse(said))) if (typeof v === "string") answers.set(k, v);
443
+ }
349
444
  } catch {
350
445
  }
351
446
  }
@@ -356,6 +451,7 @@ var createFlows = (env) => {
356
451
  if (state && env.personId() === pid) {
357
452
  merge(state.onboarding);
358
453
  for (const e of state.events ?? []) if (typeof e === "string") seenEvents.add(eventSlug(e));
454
+ for (const [k, v] of Object.entries(state.answers ?? {})) if (typeof v === "string" && !answers.has(k)) answers.set(k, v);
359
455
  messages = (state.messages ?? []).filter((m) => m && m.id && m.title);
360
456
  }
361
457
  }
@@ -449,27 +545,33 @@ var createFlows = (env) => {
449
545
  if (fresh) seenEvents.add(slug);
450
546
  const flow = flowById(active);
451
547
  let ticked = false;
452
- if (flow) {
453
- const p = prog(flow.id);
454
- for (const s of stepsIn(nodesOf(flow)))
548
+ const lists = [...flow ? [flow] : [], ...flows().filter((f) => f.shownBy === "code" && f.id !== flow?.id)];
549
+ for (const l of lists) {
550
+ const p = prog(l.id);
551
+ let here = false;
552
+ for (const s of stepsIn(nodesOf(l)))
455
553
  if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
456
554
  markDone(p, s.id);
457
- ticked = true;
458
- env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: "completed" });
555
+ here = true;
556
+ env.track(ONBOARDING_EVENT, { flow: l.id, step: s.id, kind: s.kind, action: "completed" });
459
557
  }
558
+ if (here) {
559
+ ticked = true;
560
+ celebrate(l);
561
+ }
460
562
  }
461
- if (ticked || fresh && flow) {
563
+ if (ticked || fresh && (flow || lists.length)) {
462
564
  save();
463
565
  emit();
464
566
  } else if (fresh) save();
465
567
  if (!auto || !env.personId() && !env.storage?.()) return;
466
- const next = flows().find((f) => nodesOf(f).length && !manual(f.trigger) && matchesTrigger(f.trigger, name) && allowed(f) && !finished(f));
568
+ const next = drawn().find((f) => nodesOf(f).length && !manual(momentOf(f)) && matchesTrigger(momentOf(f), name) && allowed(f) && !finished(f));
467
569
  if (next) start(next);
468
570
  },
469
571
  openCancelFlow,
470
572
  startOnboarding: (id) => {
471
- const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !manual(x.trigger) && !finished(x));
472
- if (!f || !nodesOf(f).length) return false;
573
+ const f = id ? flowById(id) : drawn().find((x) => nodesOf(x).length && !manual(momentOf(x)) && !afterOf(momentOf(x)) && !finished(x));
574
+ if (!f || !nodesOf(f).length || f.shownBy === "code") return false;
473
575
  const p = prog(f.id);
474
576
  p.dismissed = false;
475
577
  return start(f);
@@ -560,8 +662,17 @@ var createFlows = (env) => {
560
662
  next: () => {
561
663
  const v = onboardingView();
562
664
  if (v?.type !== "step") return;
665
+ if (v.step.kind === "question") {
666
+ const q = v.step.question;
667
+ const a = drafts.get(v.step.id) ?? (q ? answers.get(q.key) : void 0);
668
+ if (!q || !answerOk(q, a)) return;
669
+ answerStep(v.flow, v.step, a);
670
+ emit();
671
+ return;
672
+ }
563
673
  markDone(prog(v.flow.id), v.step.id);
564
674
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
675
+ celebrate(v.flow);
565
676
  save();
566
677
  emit();
567
678
  const url = v.step.kind === "modal" ? safeUrl(v.step.url) : void 0;
@@ -574,6 +685,7 @@ var createFlows = (env) => {
574
685
  if (!item || item.done) return;
575
686
  markDone(prog(v.flow.id), stepId);
576
687
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
688
+ celebrate(v.flow);
577
689
  save();
578
690
  emit();
579
691
  },
@@ -590,20 +702,112 @@ var createFlows = (env) => {
590
702
  startTour: (stepId) => {
591
703
  const v = onboardingView();
592
704
  if (v?.type !== "checklist") return false;
593
- const tour = v.items.find((i) => i.step.id === stepId)?.tour;
594
- if (!tour) return false;
595
- const p = prog(tour.id);
596
- p.done.clear();
597
- p.at.clear();
598
- p.dismissed = false;
599
- returnTo = { flow: v.flow.id, step: stepId };
600
- active = tour.id;
601
- env.track(ONBOARDING_EVENT, { flow: tour.id, action: "started", from: v.flow.id });
705
+ const target = v.items.find((i) => i.step.id === stepId)?.target;
706
+ if (!target) return false;
707
+ openItem(v.flow, stepId, target);
708
+ return true;
709
+ },
710
+ open: (stepId) => {
711
+ const v = onboardingView();
712
+ if (v?.type !== "checklist") return false;
713
+ const it = v.items.find((i) => i.step.id === stepId);
714
+ const o = it?.opens;
715
+ if (!it || !o) return false;
716
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "opened", opens: o.kind });
717
+ if ((o.kind === "tour" || o.kind === "modal") && it.target) {
718
+ openItem(v.flow, stepId, it.target);
719
+ return true;
720
+ }
721
+ if (!it.done && !it.step.event) {
722
+ markDone(prog(v.flow.id), stepId);
723
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
724
+ celebrate(v.flow);
725
+ }
602
726
  save();
603
727
  emit();
728
+ if (o.kind === "page") env.navigate?.(o.target.trim());
729
+ else env.task?.(stepId, v.flow);
604
730
  return true;
731
+ },
732
+ answer: (value) => {
733
+ const v = onboardingView();
734
+ if (v?.type !== "step" || v.step.kind !== "question") return;
735
+ drafts.set(v.step.id, String(value).slice(0, 500));
736
+ emit();
737
+ },
738
+ back: () => {
739
+ const v = onboardingView();
740
+ if (v?.type !== "step" || v.step.kind !== "question" || !v.back) return;
741
+ const line = stepsIn(nodesOf(v.flow));
742
+ const at = line.findIndex((s) => s.id === v.step.id);
743
+ const p = prog(v.flow.id);
744
+ for (let i = at - 1; i >= 0; i--)
745
+ if (p.done.has(line[i].id)) {
746
+ p.done.delete(line[i].id);
747
+ p.at.delete(line[i].id);
748
+ break;
749
+ }
750
+ save();
751
+ emit();
752
+ },
753
+ item: (id) => {
754
+ const f = flowById(id);
755
+ if (!f) return null;
756
+ const p = progress.get(f.id);
757
+ const steps = stepsIn(nodesOf(f)).map((s) => {
758
+ const a = s.question ? answers.get(s.question.key) : void 0;
759
+ return { ...s, done: !!p?.done.has(s.id), ...a !== void 0 ? { answer: a } : {} };
760
+ });
761
+ const done = steps.length > 0 && steps.every((s) => s.done);
762
+ const dismissed = !!p?.dismissed;
763
+ return { item: f, steps, done, dismissed, eligible: !done && !dismissed && allowed(f) && due(f) };
764
+ },
765
+ seen: (id) => {
766
+ const f = flowById(id);
767
+ if (!f || f.shownBy !== "code" || !known) return;
768
+ if (!shown.has(`f:${f.id}`)) {
769
+ shown.add(`f:${f.id}`);
770
+ env.track(ONBOARDING_EVENT, { flow: f.id, action: "started" });
771
+ }
772
+ const p = progress.get(f.id);
773
+ for (const s of stepsIn(nodesOf(f)))
774
+ if (!p?.done.has(s.id) && !shown.has(`s:${s.id}`)) {
775
+ shown.add(`s:${s.id}`);
776
+ env.track(ONBOARDING_EVENT, { flow: f.id, step: s.id, kind: s.kind, action: "shown" });
777
+ if (s.kind !== "checklist") break;
778
+ }
779
+ },
780
+ complete: (id, stepId) => {
781
+ const f = flowById(id);
782
+ const s = f && stepsIn(nodesOf(f)).find((x) => x.id === stepId);
783
+ if (!f || !s || s.kind === "question") return;
784
+ const p = prog(f.id);
785
+ if (p.done.has(s.id)) return;
786
+ markDone(p, s.id);
787
+ env.track(ONBOARDING_EVENT, { flow: f.id, step: s.id, kind: s.kind, action: "completed" });
788
+ ended(f);
789
+ },
790
+ dismissItem: (id) => {
791
+ const f = flowById(id);
792
+ if (!f) return;
793
+ const p = prog(f.id);
794
+ if (p.dismissed) return;
795
+ p.dismissed = true;
796
+ env.track(ONBOARDING_EVENT, { flow: f.id, action: "dismissed" });
797
+ if (active === f.id) active = null;
798
+ ended(f);
799
+ },
800
+ answerItem: (id, question, value) => {
801
+ const f = flowById(id);
802
+ const s = f && stepsIn(nodesOf(f)).find((x) => x.kind === "question" && (x.id === question || x.question?.key === question));
803
+ if (!f || !s?.question || !answerOk(s.question, value)) return;
804
+ answerStep(f, s, value);
805
+ ended(f);
605
806
  }
606
807
  },
808
+ finale: () => finaleNow,
809
+ accent: () => config?.onboardingAccent,
810
+ answers: () => Object.fromEntries(answers),
607
811
  message: {
608
812
  click: () => {
609
813
  const v = view();
@@ -626,22 +830,30 @@ var createFlows = (env) => {
626
830
  return controller;
627
831
  };
628
832
  export {
833
+ AFTER_PREFIX,
629
834
  CANCEL_EVENT,
630
835
  FROM_CHECKLIST,
836
+ FROM_CODE,
631
837
  INAPP_EVENT,
838
+ NEEDS_NEWER,
632
839
  ONBOARDING_EVENT,
633
840
  PLAIN_CANCEL,
634
841
  REVIEW_EVENT,
842
+ afterOf,
843
+ answerLabel,
844
+ answerOk,
635
845
  createFlows,
636
846
  discounted,
637
847
  eventSlug,
638
848
  isPageTrigger,
639
849
  matchesTrigger,
640
850
  mediaOf,
851
+ momentOf,
641
852
  money,
642
853
  monthsSince,
643
854
  nodesOf,
644
855
  offerButton,
856
+ opensOf,
645
857
  pageMatches,
646
858
  ruleMatches,
647
859
  runId,