@mrrocket/sdk 0.15.0 → 0.17.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 (71) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +47 -3
  3. package/dist/api-client.d.ts +12 -5
  4. package/dist/api-client.js +12 -8
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/api.d.ts +4 -1
  7. package/dist/api.js +2 -1
  8. package/dist/api.js.map +1 -1
  9. package/dist/batch.d.ts +6 -4
  10. package/dist/batch.js +3 -3
  11. package/dist/batch.js.map +1 -1
  12. package/dist/browser/mrrocket-flows.js +9 -2
  13. package/dist/browser/mrrocket-forms.js +1 -1
  14. package/dist/browser/mrrocket-messenger.js +42 -43
  15. package/dist/browser/mrrocket-push.js +1 -1
  16. package/dist/browser/mrrocket-replay.js +3 -3
  17. package/dist/browser/mrrocket.js +1 -1
  18. package/dist/client.d.ts +10 -2
  19. package/dist/client.js +36 -15
  20. package/dist/client.js.map +1 -1
  21. package/dist/context.d.ts +5 -1
  22. package/dist/context.js.map +1 -1
  23. package/dist/core.d.ts +1 -1
  24. package/dist/core.js +1 -1
  25. package/dist/core.js.map +1 -1
  26. package/dist/errors.d.ts +5 -1
  27. package/dist/errors.js +8 -2
  28. package/dist/errors.js.map +1 -1
  29. package/dist/flows-ui.d.ts +1 -1
  30. package/dist/flows-ui.js +64 -14
  31. package/dist/flows-ui.js.map +1 -1
  32. package/dist/flows-web.d.ts +2 -0
  33. package/dist/flows-web.js +4 -1
  34. package/dist/flows-web.js.map +1 -1
  35. package/dist/flows.d.ts +59 -3
  36. package/dist/flows.js +88 -7
  37. package/dist/flows.js.map +1 -1
  38. package/dist/forms.d.ts +4 -0
  39. package/dist/forms.js +4 -1
  40. package/dist/forms.js.map +1 -1
  41. package/dist/identity.d.ts +42 -0
  42. package/dist/identity.js +24 -0
  43. package/dist/identity.js.map +1 -0
  44. package/dist/index.d.ts +3 -1
  45. package/dist/index.js +5 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/messenger-ui.d.ts +12 -2
  48. package/dist/messenger-ui.js +58 -47
  49. package/dist/messenger-ui.js.map +1 -1
  50. package/dist/messenger.js +3 -2
  51. package/dist/messenger.js.map +1 -1
  52. package/dist/push-web.d.ts +2 -13
  53. package/dist/push-web.js +4 -2
  54. package/dist/push-web.js.map +1 -1
  55. package/dist/react-native/index.d.cts +105 -14
  56. package/dist/react-native/index.d.ts +105 -14
  57. package/dist/react-native/index.js +238 -55
  58. package/dist/react-native/index.js.map +1 -1
  59. package/dist/replay.d.ts +4 -1
  60. package/dist/replay.js +8 -2
  61. package/dist/replay.js.map +1 -1
  62. package/dist/server.d.ts +2 -0
  63. package/dist/server.js +2 -0
  64. package/dist/server.js.map +1 -1
  65. package/dist/styles-css.d.ts +1 -1
  66. package/dist/styles-css.js +40 -41
  67. package/dist/styles-css.js.map +1 -1
  68. package/dist/user-hash.d.ts +27 -0
  69. package/dist/user-hash.js +24 -0
  70. package/dist/user-hash.js.map +1 -0
  71. package/package.json +5 -1
package/dist/flows.d.ts CHANGED
@@ -11,13 +11,16 @@
11
11
  * `onCancel`, and only after they succeed is the outcome recorded.
12
12
  * - `$onboarding` `{flow, step?, action: "started" | "shown" | "completed" | "dismissed"}`; since 0.11.0 a wait records
13
13
  * "shown" when the person reaches it and "completed" when it is over, a condition `{action: "answered", answer: "yes"
14
- * | "no"}` once it is decided.
14
+ * | "no"}` once it is decided; since 0.17.0 a tour a checklist task started records `{action: "started", from}` with
15
+ * the checklist's id.
15
16
  * - `$inapp` `{message, journey, action: "shown" | "clicked" | "dismissed"}`; the journey moves the person on.
16
17
  */
17
18
  import type { CockpitConfig } from "./core.js";
18
19
  export declare const CANCEL_EVENT = "$cancel_flow";
19
20
  export declare const ONBOARDING_EVENT = "$onboarding";
20
21
  export declare const INAPP_EVENT = "$inapp";
22
+ /** 0.16.0: the app asked the store's rating dialog for a review request (or found none on the device). */
23
+ export declare const REVIEW_EVENT = "$review_prompt";
21
24
  export type CancelOfferKind = "discount" | "pause" | "downgrade" | "contact";
22
25
  /** What one answer of the survey offers before the confirmation. An answer without one goes straight to it. */
23
26
  export interface CancelOffer {
@@ -84,6 +87,17 @@ export interface CancelFlowConfig {
84
87
  offersFor: "business";
85
88
  }
86
89
  export type OnboardingStepKind = "tour" | "checklist" | "modal";
90
+ /** 0.17.0: where a tour bubble sits next to its element; "auto" (and no value) puts it below, else above, as before. */
91
+ export type TourPlacement = "auto" | "top" | "bottom" | "left" | "right";
92
+ /**
93
+ * 0.17.0: a picture or a video above a tour step's or a modal's text. A video is a YouTube, Vimeo or Loom link (shown
94
+ * through their privacy-friendly players, see `videoEmbed`) or a video file (mp4, webm).
95
+ */
96
+ export interface FlowMedia {
97
+ kind: "image" | "video";
98
+ url: string;
99
+ alt?: string;
100
+ }
87
101
  export interface OnboardingStep {
88
102
  id: string;
89
103
  kind: OnboardingStepKind;
@@ -94,6 +108,14 @@ export interface OnboardingStep {
94
108
  target?: string;
95
109
  /** Checklist items: the event that ticks them (`track("<event>")`). Without it the person ticks it themselves. */
96
110
  event?: string;
111
+ /** 0.17.0, tour steps: which side of the element the bubble takes (flips when there is no room). */
112
+ placement?: TourPlacement;
113
+ /** 0.17.0, tour steps and modals: a picture or a video above the text. */
114
+ media?: FlowMedia;
115
+ /** 0.17.0, modals: where the button goes (http(s) or a path of the app). */
116
+ url?: string;
117
+ /** 0.17.0, checklist items: the id of a tour (another flow) the item starts; finishing the tour ticks the item. */
118
+ startsTour?: string;
97
119
  }
98
120
  /** A pause in a flow (0.11.0): the next step shows this long after the one before was done. */
99
121
  export interface OnboardingWait {
@@ -119,9 +141,15 @@ export type OnboardingNode = OnboardingStep | OnboardingWait | OnboardingConditi
119
141
  /** A live onboarding flow's in-app steps (MRRocket › Conversion › Onboarding); its emails go out from MRRocket. */
120
142
  export interface OnboardingFlow {
121
143
  id: string;
144
+ /** A checklist's heading; for tours and modals the name MRRocket knows them by. */
122
145
  name: string;
123
- /** When it starts: an event's words ("Signed up", "First export"), or "First launch" / "First visit". */
146
+ /**
147
+ * When it starts: an event's words ("Signed up", "First export"), "First launch" / "First visit", since 0.17.0 a page
148
+ * of the app ("/settings", "/projects/*"), or "From a checklist" (only a checklist task starts it).
149
+ */
124
150
  trigger: string;
151
+ /** 0.17.0: "signed_in" starts it on its own only for someone the app identified. */
152
+ audience?: "signed_in";
125
153
  /** What every version shows: with `nodes`, only the in-app steps before the first wait or condition. */
126
154
  steps: OnboardingStep[];
127
155
  /** 0.11.0+: the whole flow with its waits and conditions. Read instead of `steps` when it is there. */
@@ -155,6 +183,14 @@ export interface InAppState {
155
183
  messages: InAppMessage[];
156
184
  /** 0.11.0: the events the flows' conditions ask about that this person did (as `eventSlug`s). */
157
185
  events?: string[];
186
+ /**
187
+ * 0.16.0: a live review request may ask this person now. The React Native client's `requestReview()` opens the
188
+ * store's own rating dialog (expo-store-review) and reports `$review_prompt`; websites have no store dialog.
189
+ */
190
+ review?: {
191
+ campaign: string;
192
+ store: string;
193
+ };
158
194
  }
159
195
  export interface CancelFlowResult {
160
196
  /** "cancelled": confirmed; "saved": took the offer; "kept": closed the dialog without cancelling. */
@@ -218,12 +254,15 @@ export type FlowView = {
218
254
  index: number;
219
255
  total: number;
220
256
  last: boolean;
221
- } | {
257
+ }
258
+ /** `tour`: the tour the item starts (0.17.0), when the config has it. */
259
+ | {
222
260
  type: "checklist";
223
261
  flow: OnboardingFlow;
224
262
  items: {
225
263
  step: OnboardingStep;
226
264
  done: boolean;
265
+ tour?: OnboardingFlow;
227
266
  }[];
228
267
  } | {
229
268
  type: "message";
@@ -272,10 +311,12 @@ export interface FlowsController {
272
311
  confirm(comment?: string): void;
273
312
  close(): void;
274
313
  };
314
+ /** `startTour(stepId)` (0.17.0): the tour a checklist item starts; the checklist comes back when it ends. */
275
315
  onboarding: {
276
316
  next(): void;
277
317
  check(stepId: string): void;
278
318
  dismiss(): void;
319
+ startTour(stepId: string): boolean;
279
320
  };
280
321
  message: {
281
322
  click(): void;
@@ -290,6 +331,21 @@ export declare const eventSlug: (s: string) => string;
290
331
  export declare const matchesTrigger: (trigger: string, event: string) => boolean;
291
332
  /** Triggers that mean "the first time the person opens the app": the flow starts on load until it is done. */
292
333
  export declare const startsOnLoad: (trigger: string) => boolean;
334
+ /** 0.17.0: the trigger of a tour only a checklist task starts. */
335
+ export declare const FROM_CHECKLIST = "From a checklist";
336
+ /** 0.17.0: a trigger that is a page of the app ("/settings", "/projects/*"): the flow starts when the person opens it. */
337
+ export declare const isPageTrigger: (trigger: string) => boolean;
338
+ /**
339
+ * 0.17.0: how a video link plays. YouTube, Vimeo and Loom through their players without tracking cookies
340
+ * (youtube-nocookie.com, Vimeo's `dnt=1`), a file (mp4, webm, mov, or an upload in MRRocket) in a plain video element;
341
+ * anything else is not shown. Regular expressions only, so React Native can use it too.
342
+ */
343
+ export declare const videoEmbed: (url: string | undefined) => {
344
+ kind: "iframe" | "file";
345
+ src: string;
346
+ } | null;
347
+ /** 0.17.0: a step's picture or video when the dialogs may load it (http(s), a video they can play), else undefined. */
348
+ export declare const mediaOf: (step: Pick<OnboardingStep, "media">) => FlowMedia | undefined;
293
349
  /** A random id for one cancel flow run. */
294
350
  export declare const runId: () => string;
295
351
  /** The money a discount leaves: `29 EUR` at 30 % → `20.30`. */
package/dist/flows.js CHANGED
@@ -2,6 +2,7 @@
2
2
  var CANCEL_EVENT = "$cancel_flow";
3
3
  var ONBOARDING_EVENT = "$onboarding";
4
4
  var INAPP_EVENT = "$inapp";
5
+ var REVIEW_EVENT = "$review_prompt";
5
6
  var PLAIN_CANCEL = {
6
7
  version: 0,
7
8
  question: "",
@@ -21,6 +22,26 @@ var matchesTrigger = (trigger, event) => {
21
22
  return t === e || SIGNUP.has(t) && SIGNUP.has(e);
22
23
  };
23
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
+ var FROM_CHECKLIST = "From a checklist";
26
+ var isPageTrigger = (trigger) => trigger.trim().startsWith("/");
27
+ var manual = (trigger) => eventSlug(trigger) === "from_a_checklist";
28
+ var videoEmbed = (url) => {
29
+ const u = (url ?? "").trim();
30
+ if (!/^https?:\/\//i.test(u)) return null;
31
+ let m = u.match(/^https?:\/\/(?:www\.|m\.)?(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([\w-]{6,20})/i);
32
+ if (m) return { kind: "iframe", src: `https://www.youtube-nocookie.com/embed/${m[1]}?rel=0` };
33
+ m = u.match(/^https?:\/\/(?:www\.|player\.)?vimeo\.com\/(?:video\/)?(\d{4,12})/i);
34
+ if (m) return { kind: "iframe", src: `https://player.vimeo.com/video/${m[1]}?dnt=1` };
35
+ m = u.match(/^https?:\/\/(?:www\.)?loom\.com\/(?:share|embed)\/([\w-]{8,64})/i);
36
+ if (m) return { kind: "iframe", src: `https://www.loom.com/embed/${m[1]}` };
37
+ if (/\.(mp4|webm|mov|m4v)(\?|#|$)/i.test(u) || /\/media\/uploads\//.test(u)) return { kind: "file", src: u };
38
+ return null;
39
+ };
40
+ var mediaOf = (step) => {
41
+ const m = step.media;
42
+ if (!m || m.kind !== "image" && m.kind !== "video" || !/^https?:\/\//i.test(m.url ?? "")) return void 0;
43
+ return m.kind === "video" && !videoEmbed(m.url) ? void 0 : m;
44
+ };
24
45
  var runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
25
46
  var discounted = (amount, percent) => Math.round(amount * (1 - percent / 100) * 100) / 100;
26
47
  var money = (amount, currency) => {
@@ -68,6 +89,7 @@ var createFlows = (env) => {
68
89
  const seenEvents = /* @__PURE__ */ new Set();
69
90
  const now = () => env.now?.() ?? Date.now();
70
91
  let timer = null;
92
+ let returnTo = null;
71
93
  const flows = () => config?.onboarding ?? [];
72
94
  const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
73
95
  const prog = (id) => {
@@ -205,7 +227,14 @@ var createFlows = (env) => {
205
227
  shown.add(`s:${i.id}`);
206
228
  env.track(ONBOARDING_EVENT, { flow: flow.id, step: i.id, kind: "checklist", action: "shown" });
207
229
  }
208
- return { type: "checklist", flow, items: items.map((s) => ({ step: s, done: p.done.has(s.id) })) };
230
+ return {
231
+ type: "checklist",
232
+ flow,
233
+ 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 } : {} };
236
+ })
237
+ };
209
238
  }
210
239
  if (!shown.has(`s:${step.id}`)) {
211
240
  shown.add(`s:${step.id}`);
@@ -239,7 +268,28 @@ var createFlows = (env) => {
239
268
  }
240
269
  const ob = onboardingView();
241
270
  if (ob) return ob;
242
- if (active && !holding) active = null;
271
+ if (active && !holding) {
272
+ const back = returnTo;
273
+ const ended = active;
274
+ returnTo = null;
275
+ active = null;
276
+ const list = back ? flowById(back.flow) : void 0;
277
+ if (back && list) {
278
+ const item = stepsIn(nodesOf(list)).find((s) => s.id === back.step);
279
+ const p = prog(list.id);
280
+ if (item && !item.event && !progress.get(ended)?.dismissed && !p.done.has(item.id)) {
281
+ markDone(p, item.id);
282
+ record({ flow: list.id, step: item.id, kind: "checklist", action: "completed" });
283
+ }
284
+ active = list.id;
285
+ const again = onboardingView();
286
+ if (again) return again;
287
+ if (!holding) active = null;
288
+ } else if (known) {
289
+ const after = flowById(ended)?.trigger;
290
+ Promise.resolve().then(() => autoStart(after));
291
+ }
292
+ }
243
293
  const m = messages.find((x) => !handled.has(x.id));
244
294
  return m ? { type: "message", message: m } : null;
245
295
  };
@@ -264,10 +314,15 @@ var createFlows = (env) => {
264
314
  emit();
265
315
  return true;
266
316
  };
267
- const autoStart = () => {
317
+ const allowed = (f) => f.audience !== "signed_in" || !!env.personId();
318
+ const autoStart = (after) => {
268
319
  if (!auto || active || !known) return;
269
320
  if (!env.personId() && !env.storage?.()) return;
270
- const next = flows().find((f) => startsOnLoad(f.trigger) && nodesOf(f).length && !finished(f));
321
+ const path = env.path?.();
322
+ 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)
325
+ );
271
326
  if (next) start(next);
272
327
  };
273
328
  const refresh = () => {
@@ -277,6 +332,7 @@ var createFlows = (env) => {
277
332
  seenEvents.clear();
278
333
  handled.clear();
279
334
  active = null;
335
+ returnTo = null;
280
336
  loadedFor = void 0;
281
337
  }
282
338
  progressFor = pid;
@@ -386,6 +442,7 @@ var createFlows = (env) => {
386
442
  },
387
443
  refresh,
388
444
  onEvent: (name) => {
445
+ if (name === "$pageview") autoStart();
389
446
  if (!name || name.startsWith("$")) return;
390
447
  const slug = eventSlug(name);
391
448
  const fresh = !!slug && !seenEvents.has(slug);
@@ -406,12 +463,12 @@ var createFlows = (env) => {
406
463
  emit();
407
464
  } else if (fresh) save();
408
465
  if (!auto || !env.personId() && !env.storage?.()) return;
409
- const next = flows().find((f) => nodesOf(f).length && matchesTrigger(f.trigger, name) && !finished(f));
466
+ const next = flows().find((f) => nodesOf(f).length && !manual(f.trigger) && matchesTrigger(f.trigger, name) && allowed(f) && !finished(f));
410
467
  if (next) start(next);
411
468
  },
412
469
  openCancelFlow,
413
470
  startOnboarding: (id) => {
414
- const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !finished(x));
471
+ const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !manual(x.trigger) && !finished(x));
415
472
  if (!f || !nodesOf(f).length) return false;
416
473
  const p = prog(f.id);
417
474
  p.dismissed = false;
@@ -507,6 +564,8 @@ var createFlows = (env) => {
507
564
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
508
565
  save();
509
566
  emit();
567
+ const url = v.step.kind === "modal" ? safeUrl(v.step.url) : void 0;
568
+ if (url) env.openUrl?.(url);
510
569
  },
511
570
  check: (stepId) => {
512
571
  const v = onboardingView();
@@ -523,9 +582,26 @@ var createFlows = (env) => {
523
582
  if (!v || v.type !== "step" && v.type !== "checklist") return;
524
583
  prog(v.flow.id).dismissed = true;
525
584
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, ...v.type === "step" ? { step: v.step.id } : {}, action: "dismissed" });
526
- active = null;
585
+ active = returnTo && returnTo.flow !== v.flow.id && flowById(returnTo.flow) ? returnTo.flow : null;
586
+ returnTo = null;
587
+ save();
588
+ emit();
589
+ },
590
+ startTour: (stepId) => {
591
+ const v = onboardingView();
592
+ 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 });
527
602
  save();
528
603
  emit();
604
+ return true;
529
605
  }
530
606
  },
531
607
  message: {
@@ -551,13 +627,17 @@ var createFlows = (env) => {
551
627
  };
552
628
  export {
553
629
  CANCEL_EVENT,
630
+ FROM_CHECKLIST,
554
631
  INAPP_EVENT,
555
632
  ONBOARDING_EVENT,
556
633
  PLAIN_CANCEL,
634
+ REVIEW_EVENT,
557
635
  createFlows,
558
636
  discounted,
559
637
  eventSlug,
638
+ isPageTrigger,
560
639
  matchesTrigger,
640
+ mediaOf,
561
641
  money,
562
642
  monthsSince,
563
643
  nodesOf,
@@ -567,6 +647,7 @@ export {
567
647
  runId,
568
648
  safeUrl,
569
649
  startsOnLoad,
650
+ videoEmbed,
570
651
  waitMs
571
652
  };
572
653
  //# sourceMappingURL=flows.js.map