@mrrocket/sdk 0.18.0 → 0.22.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 (63) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +36 -1
  3. package/dist/api-client.d.ts +28 -4
  4. package/dist/api-client.js +30 -13
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/browser/mrrocket-flows.js +8 -3
  7. package/dist/browser/mrrocket-messenger.js +38 -5
  8. package/dist/browser/mrrocket.js +1 -1
  9. package/dist/browser.d.ts +2 -2
  10. package/dist/browser.js +2 -1
  11. package/dist/browser.js.map +1 -1
  12. package/dist/client.d.ts +17 -1
  13. package/dist/client.js +25 -5
  14. package/dist/client.js.map +1 -1
  15. package/dist/core.d.ts +5 -1
  16. package/dist/core.js +1 -1
  17. package/dist/core.js.map +1 -1
  18. package/dist/feedback.js +161 -119
  19. package/dist/feedback.js.map +1 -1
  20. package/dist/flows-ui.d.ts +1 -1
  21. package/dist/flows-ui.js +100 -72
  22. package/dist/flows-ui.js.map +1 -1
  23. package/dist/flows-web.js +2 -1
  24. package/dist/flows-web.js.map +1 -1
  25. package/dist/flows.d.ts +38 -6
  26. package/dist/flows.js +51 -17
  27. package/dist/flows.js.map +1 -1
  28. package/dist/i18n.d.ts +22 -0
  29. package/dist/i18n.js +74 -0
  30. package/dist/i18n.js.map +1 -0
  31. package/dist/index.d.ts +4 -1
  32. package/dist/index.js +5 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/messenger-ui.d.ts +9 -3
  35. package/dist/messenger-ui.js +324 -152
  36. package/dist/messenger-ui.js.map +1 -1
  37. package/dist/messenger.d.ts +3 -1
  38. package/dist/messenger.js +3 -2
  39. package/dist/messenger.js.map +1 -1
  40. package/dist/onboarding-report.d.ts +19 -0
  41. package/dist/onboarding-report.js +23 -0
  42. package/dist/onboarding-report.js.map +1 -0
  43. package/dist/onboarding-state.d.ts +3 -0
  44. package/dist/onboarding-state.js +7 -1
  45. package/dist/onboarding-state.js.map +1 -1
  46. package/dist/paywall.js +2 -1
  47. package/dist/paywall.js.map +1 -1
  48. package/dist/react-native/index.d.cts +93 -8
  49. package/dist/react-native/index.d.ts +93 -8
  50. package/dist/react-native/index.js +819 -511
  51. package/dist/react-native/index.js.map +1 -1
  52. package/dist/server.d.ts +4 -2
  53. package/dist/server.js +2 -2
  54. package/dist/server.js.map +1 -1
  55. package/dist/styles-css.d.ts +1 -1
  56. package/dist/styles-css.js +35 -2
  57. package/dist/styles-css.js.map +1 -1
  58. package/dist/survey.js +18 -14
  59. package/dist/survey.js.map +1 -1
  60. package/dist/ui.d.ts +2 -0
  61. package/dist/ui.js +15 -12
  62. package/dist/ui.js.map +1 -1
  63. package/package.json +6 -3
@@ -24,6 +24,8 @@ export interface MessengerProps {
24
24
  launcher?: boolean;
25
25
  /** The Help tab with your published help articles (default true; hidden while there are none). */
26
26
  help?: boolean;
27
+ /** The Updates tab with your published changelog (default true; hidden while there are none). */
28
+ updates?: boolean;
27
29
  /** A short sound when an answer arrives (default true). */
28
30
  sound?: boolean;
29
31
  /** A preview of a new answer above the closed launcher (default true). */
@@ -42,4 +44,4 @@ export interface MessengerProps {
42
44
  *
43
45
  * <Messenger title="Acme" greeting="Hi! How can we help?" />
44
46
  */
45
- export declare const Messenger: ({ position, title, greeting, askEmail, label, launcher, help, sound, preview, theme, accent, corners, font, header }: MessengerProps) => null;
47
+ export declare const Messenger: ({ position, title, greeting, askEmail, label, launcher, help, updates, sound, preview, theme, accent, corners, font, header }: MessengerProps) => null;
package/dist/messenger.js CHANGED
@@ -14,7 +14,7 @@ var useMessenger = () => ({
14
14
  close: () => handle?.close(),
15
15
  toggle: () => withHandle((h) => h.toggle())
16
16
  });
17
- var Messenger = ({ position, title, greeting, askEmail, label, launcher, help, sound, preview, theme, accent, corners, font, header }) => {
17
+ var Messenger = ({ position, title, greeting, askEmail, label, launcher, help, updates, sound, preview, theme, accent, corners, font, header }) => {
18
18
  const ctx = useCtx();
19
19
  const who = useRef({ distinctId: ctx.distinctId, user: ctx.user, userHash: ctx.userHash });
20
20
  who.current = { distinctId: ctx.distinctId, user: ctx.user, userHash: ctx.userHash };
@@ -38,6 +38,7 @@ var Messenger = ({ position, title, greeting, askEmail, label, launcher, help, s
38
38
  askEmail,
39
39
  label,
40
40
  help,
41
+ updates,
41
42
  sound,
42
43
  preview,
43
44
  theme,
@@ -63,7 +64,7 @@ var Messenger = ({ position, title, greeting, askEmail, label, launcher, help, s
63
64
  mine?.destroy();
64
65
  if (handle === mine) handle = null;
65
66
  };
66
- }, [apiKey, host, embedded, launcher, position, title, greeting, askEmail, label, help, sound, preview, theme, accent, corners, font, header]);
67
+ }, [apiKey, host, embedded, launcher, position, title, greeting, askEmail, label, help, updates, sound, preview, theme, accent, corners, font, header]);
67
68
  return null;
68
69
  };
69
70
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/messenger.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { MessengerCorners, MessengerPosition } from \"./api-client\";\nimport { useCtx } from \"./context\";\nimport type { MessengerHandle, MessengerView } from \"./messenger-ui\";\nimport type { Theme } from \"./ui\";\n\n/**\n * The messenger, after Featurebase's: a launcher and a panel with Home, Messages (a chat with the product's team) and\n * Help (the product's published help articles). A visitor's messages become a Support conversation (channel \"chat\")\n * in MRRocket's inbox, where the owner answers or the Customer experience agent does (Support › Automation › AI\n * answers). A new answer plays a short sound and shows a preview above the closed launcher.\n *\n * The panel is plain DOM (messenger-ui.ts, shared with the script tag), loaded with a dynamic import after the page\n * rendered. The visitor is the provider's id; a person the site identified carries their name and email.\n */\n\nlet handle: MessengerHandle | null = null;\n/** A call made before the panel loaded runs once it has. */\nlet waiting: ((h: MessengerHandle) => void) | null = null;\nconst withHandle = (fn: (h: MessengerHandle) => void) => {\n if (handle) fn(handle);\n else waiting = fn;\n};\n\n/** Open (on Home, a tab or an article), close or toggle the messenger from your own button (or mark any element `data-ck-messenger`). */\nexport const useMessenger = () => ({\n open: (view?: MessengerView | { article: string }) => withHandle((h) => h.open(view)),\n close: () => handle?.close(),\n toggle: () => withHandle((h) => h.toggle()),\n});\n\nexport interface MessengerProps {\n /** A corner, the middle of the top or bottom edge, or the middle of a side (default \"bottom-right\"). */\n position?: MessengerPosition;\n /** The team's name beside its mark on Home (default: the product's name). */\n title?: string;\n /** The big line on Home and the first line of an empty chat. */\n greeting?: string;\n /** Ask anonymous visitors for an email (optional for them). Default true. */\n askEmail?: boolean;\n /** Text on the launcher; empty shows the icon only. */\n label?: string;\n /** Show the round launcher (default true). False: open it with `useMessenger()` or `data-ck-messenger`. */\n launcher?: boolean;\n /** The Help tab with your published help articles (default true; hidden while there are none). */\n help?: boolean;\n /** A short sound when an answer arrives (default true). */\n sound?: boolean;\n /** A preview of a new answer above the closed launcher (default true). */\n preview?: boolean;\n theme?: Theme;\n accent?: string;\n /** \"round\" (default), \"soft\" or \"square\". */\n corners?: MessengerCorners;\n /** Your site's font (default) or \"system\". */\n font?: \"site\" | \"system\";\n /** Home's top: \"plain\" (default) or \"color\" (the accent). */\n header?: \"plain\" | \"color\";\n}\n\n/**\n * A floating launcher that opens the messenger. Put it once inside `<CockpitProvider>`:\n *\n * <Messenger title=\"Acme\" greeting=\"Hi! How can we help?\" />\n */\nexport const Messenger = ({ position, title, greeting, askEmail, label, launcher, help, sound, preview, theme, accent, corners, font, header }: MessengerProps) => {\n const ctx = useCtx();\n // The panel reads the visitor at each request, so a later identify or reset needs no remount.\n const who = useRef({ distinctId: ctx.distinctId, user: ctx.user, userHash: ctx.userHash });\n who.current = { distinctId: ctx.distinctId, user: ctx.user, userHash: ctx.userHash };\n const { apiKey, host, embedded } = ctx;\n\n useEffect(() => {\n if (embedded || !apiKey) return;\n let gone = false;\n let mine: MessengerHandle | null = null;\n void import(\"./messenger-ui\").then(({ mountMessenger }) => {\n if (gone) return;\n mine = mountMessenger({\n host,\n apiKey,\n distinctId: () => who.current.distinctId,\n user: () => who.current.user as { id?: string; name?: string; email?: string } | undefined,\n userHash: () => who.current.userHash,\n launcher,\n position,\n title,\n greeting,\n askEmail,\n label,\n help,\n sound,\n preview,\n theme,\n accent,\n corners,\n font,\n header,\n });\n handle = mine;\n waiting?.(mine);\n waiting = null;\n });\n const click = (e: MouseEvent) => {\n if ((e.target as HTMLElement | null)?.closest(\"[data-ck-messenger]\")) {\n e.preventDefault();\n withHandle((h) => h.open());\n }\n };\n document.addEventListener(\"click\", click);\n return () => {\n gone = true;\n document.removeEventListener(\"click\", click);\n mine?.destroy();\n if (handle === mine) handle = null;\n };\n }, [apiKey, host, embedded, launcher, position, title, greeting, askEmail, label, help, sound, preview, theme, accent, corners, font, header]);\n\n return null;\n};\n"],"mappings":";;;AAEA,SAAS,WAAW,cAAc;AAElC,SAAS,cAAc;AAcvB,IAAI,SAAiC;AAErC,IAAI,UAAiD;AACrD,IAAM,aAAa,CAAC,OAAqC;AACrD,MAAI,OAAQ,IAAG,MAAM;AAAA,MAChB,WAAU;AACnB;AAGO,IAAM,eAAe,OAAO;AAAA,EAC/B,MAAM,CAAC,SAA+C,WAAW,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC;AAAA,EACpF,OAAO,MAAM,QAAQ,MAAM;AAAA,EAC3B,QAAQ,MAAM,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC;AAC9C;AAoCO,IAAM,YAAY,CAAC,EAAE,UAAU,OAAO,UAAU,UAAU,OAAO,UAAU,MAAM,OAAO,SAAS,OAAO,QAAQ,SAAS,MAAM,OAAO,MAAsB;AAC/J,QAAM,MAAM,OAAO;AAEnB,QAAM,MAAM,OAAO,EAAE,YAAY,IAAI,YAAY,MAAM,IAAI,MAAM,UAAU,IAAI,SAAS,CAAC;AACzF,MAAI,UAAU,EAAE,YAAY,IAAI,YAAY,MAAM,IAAI,MAAM,UAAU,IAAI,SAAS;AACnF,QAAM,EAAE,QAAQ,MAAM,SAAS,IAAI;AAEnC,YAAU,MAAM;AACZ,QAAI,YAAY,CAAC,OAAQ;AACzB,QAAI,OAAO;AACX,QAAI,OAA+B;AACnC,SAAK,OAAO,mBAAgB,EAAE,KAAK,CAAC,EAAE,eAAe,MAAM;AACvD,UAAI,KAAM;AACV,aAAO,eAAe;AAAA,QAClB;AAAA,QACA;AAAA,QACA,YAAY,MAAM,IAAI,QAAQ;AAAA,QAC9B,MAAM,MAAM,IAAI,QAAQ;AAAA,QACxB,UAAU,MAAM,IAAI,QAAQ;AAAA,QAC5B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACJ,CAAC;AACD,eAAS;AACT,gBAAU,IAAI;AACd,gBAAU;AAAA,IACd,CAAC;AACD,UAAM,QAAQ,CAAC,MAAkB;AAC7B,UAAK,EAAE,QAA+B,QAAQ,qBAAqB,GAAG;AAClE,UAAE,eAAe;AACjB,mBAAW,CAAC,MAAM,EAAE,KAAK,CAAC;AAAA,MAC9B;AAAA,IACJ;AACA,aAAS,iBAAiB,SAAS,KAAK;AACxC,WAAO,MAAM;AACT,aAAO;AACP,eAAS,oBAAoB,SAAS,KAAK;AAC3C,YAAM,QAAQ;AACd,UAAI,WAAW,KAAM,UAAS;AAAA,IAClC;AAAA,EACJ,GAAG,CAAC,QAAQ,MAAM,UAAU,UAAU,UAAU,OAAO,UAAU,UAAU,OAAO,MAAM,OAAO,SAAS,OAAO,QAAQ,SAAS,MAAM,MAAM,CAAC;AAE7I,SAAO;AACX;","names":[]}
1
+ {"version":3,"sources":["../src/messenger.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { MessengerCorners, MessengerPosition } from \"./api-client\";\nimport { useCtx } from \"./context\";\nimport type { MessengerHandle, MessengerView } from \"./messenger-ui\";\nimport type { Theme } from \"./ui\";\n\n/**\n * The messenger, after Featurebase's: a launcher and a panel with Home, Messages (a chat with the product's team) and\n * Help (the product's published help articles). A visitor's messages become a Support conversation (channel \"chat\")\n * in MRRocket's inbox, where the owner answers or the Customer experience agent does (Support › Automation › AI\n * answers). A new answer plays a short sound and shows a preview above the closed launcher.\n *\n * The panel is plain DOM (messenger-ui.ts, shared with the script tag), loaded with a dynamic import after the page\n * rendered. The visitor is the provider's id; a person the site identified carries their name and email.\n */\n\nlet handle: MessengerHandle | null = null;\n/** A call made before the panel loaded runs once it has. */\nlet waiting: ((h: MessengerHandle) => void) | null = null;\nconst withHandle = (fn: (h: MessengerHandle) => void) => {\n if (handle) fn(handle);\n else waiting = fn;\n};\n\n/** Open (on Home, a tab or an article), close or toggle the messenger from your own button (or mark any element `data-ck-messenger`). */\nexport const useMessenger = () => ({\n open: (view?: MessengerView | { article: string }) => withHandle((h) => h.open(view)),\n close: () => handle?.close(),\n toggle: () => withHandle((h) => h.toggle()),\n});\n\nexport interface MessengerProps {\n /** A corner, the middle of the top or bottom edge, or the middle of a side (default \"bottom-right\"). */\n position?: MessengerPosition;\n /** The team's name beside its mark on Home (default: the product's name). */\n title?: string;\n /** The big line on Home and the first line of an empty chat. */\n greeting?: string;\n /** Ask anonymous visitors for an email (optional for them). Default true. */\n askEmail?: boolean;\n /** Text on the launcher; empty shows the icon only. */\n label?: string;\n /** Show the round launcher (default true). False: open it with `useMessenger()` or `data-ck-messenger`. */\n launcher?: boolean;\n /** The Help tab with your published help articles (default true; hidden while there are none). */\n help?: boolean;\n /** The Updates tab with your published changelog (default true; hidden while there are none). */\n updates?: boolean;\n /** A short sound when an answer arrives (default true). */\n sound?: boolean;\n /** A preview of a new answer above the closed launcher (default true). */\n preview?: boolean;\n theme?: Theme;\n accent?: string;\n /** \"round\" (default), \"soft\" or \"square\". */\n corners?: MessengerCorners;\n /** Your site's font (default) or \"system\". */\n font?: \"site\" | \"system\";\n /** Home's top: \"plain\" (default) or \"color\" (the accent). */\n header?: \"plain\" | \"color\";\n}\n\n/**\n * A floating launcher that opens the messenger. Put it once inside `<CockpitProvider>`:\n *\n * <Messenger title=\"Acme\" greeting=\"Hi! How can we help?\" />\n */\nexport const Messenger = ({ position, title, greeting, askEmail, label, launcher, help, updates, sound, preview, theme, accent, corners, font, header }: MessengerProps) => {\n const ctx = useCtx();\n // The panel reads the visitor at each request, so a later identify or reset needs no remount.\n const who = useRef({ distinctId: ctx.distinctId, user: ctx.user, userHash: ctx.userHash });\n who.current = { distinctId: ctx.distinctId, user: ctx.user, userHash: ctx.userHash };\n const { apiKey, host, embedded } = ctx;\n\n useEffect(() => {\n if (embedded || !apiKey) return;\n let gone = false;\n let mine: MessengerHandle | null = null;\n void import(\"./messenger-ui\").then(({ mountMessenger }) => {\n if (gone) return;\n mine = mountMessenger({\n host,\n apiKey,\n distinctId: () => who.current.distinctId,\n user: () => who.current.user as { id?: string; name?: string; email?: string } | undefined,\n userHash: () => who.current.userHash,\n launcher,\n position,\n title,\n greeting,\n askEmail,\n label,\n help,\n updates,\n sound,\n preview,\n theme,\n accent,\n corners,\n font,\n header,\n });\n handle = mine;\n waiting?.(mine);\n waiting = null;\n });\n const click = (e: MouseEvent) => {\n if ((e.target as HTMLElement | null)?.closest(\"[data-ck-messenger]\")) {\n e.preventDefault();\n withHandle((h) => h.open());\n }\n };\n document.addEventListener(\"click\", click);\n return () => {\n gone = true;\n document.removeEventListener(\"click\", click);\n mine?.destroy();\n if (handle === mine) handle = null;\n };\n }, [apiKey, host, embedded, launcher, position, title, greeting, askEmail, label, help, updates, sound, preview, theme, accent, corners, font, header]);\n\n return null;\n};\n"],"mappings":";;;AAEA,SAAS,WAAW,cAAc;AAElC,SAAS,cAAc;AAcvB,IAAI,SAAiC;AAErC,IAAI,UAAiD;AACrD,IAAM,aAAa,CAAC,OAAqC;AACrD,MAAI,OAAQ,IAAG,MAAM;AAAA,MAChB,WAAU;AACnB;AAGO,IAAM,eAAe,OAAO;AAAA,EAC/B,MAAM,CAAC,SAA+C,WAAW,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC;AAAA,EACpF,OAAO,MAAM,QAAQ,MAAM;AAAA,EAC3B,QAAQ,MAAM,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC;AAC9C;AAsCO,IAAM,YAAY,CAAC,EAAE,UAAU,OAAO,UAAU,UAAU,OAAO,UAAU,MAAM,SAAS,OAAO,SAAS,OAAO,QAAQ,SAAS,MAAM,OAAO,MAAsB;AACxK,QAAM,MAAM,OAAO;AAEnB,QAAM,MAAM,OAAO,EAAE,YAAY,IAAI,YAAY,MAAM,IAAI,MAAM,UAAU,IAAI,SAAS,CAAC;AACzF,MAAI,UAAU,EAAE,YAAY,IAAI,YAAY,MAAM,IAAI,MAAM,UAAU,IAAI,SAAS;AACnF,QAAM,EAAE,QAAQ,MAAM,SAAS,IAAI;AAEnC,YAAU,MAAM;AACZ,QAAI,YAAY,CAAC,OAAQ;AACzB,QAAI,OAAO;AACX,QAAI,OAA+B;AACnC,SAAK,OAAO,mBAAgB,EAAE,KAAK,CAAC,EAAE,eAAe,MAAM;AACvD,UAAI,KAAM;AACV,aAAO,eAAe;AAAA,QAClB;AAAA,QACA;AAAA,QACA,YAAY,MAAM,IAAI,QAAQ;AAAA,QAC9B,MAAM,MAAM,IAAI,QAAQ;AAAA,QACxB,UAAU,MAAM,IAAI,QAAQ;AAAA,QAC5B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACJ,CAAC;AACD,eAAS;AACT,gBAAU,IAAI;AACd,gBAAU;AAAA,IACd,CAAC;AACD,UAAM,QAAQ,CAAC,MAAkB;AAC7B,UAAK,EAAE,QAA+B,QAAQ,qBAAqB,GAAG;AAClE,UAAE,eAAe;AACjB,mBAAW,CAAC,MAAM,EAAE,KAAK,CAAC;AAAA,MAC9B;AAAA,IACJ;AACA,aAAS,iBAAiB,SAAS,KAAK;AACxC,WAAO,MAAM;AACT,aAAO;AACP,eAAS,oBAAoB,SAAS,KAAK;AAC3C,YAAM,QAAQ;AACd,UAAI,WAAW,KAAM,UAAS;AAAA,IAClC;AAAA,EACJ,GAAG,CAAC,QAAQ,MAAM,UAAU,UAAU,UAAU,OAAO,UAAU,UAAU,OAAO,MAAM,SAAS,OAAO,SAAS,OAAO,QAAQ,SAAS,MAAM,MAAM,CAAC;AAEtJ,SAAO;AACX;","names":[]}
@@ -0,0 +1,19 @@
1
+ export type OnboardingReportType = "questions" | "checklist" | "tour";
2
+ export interface OnboardingReportOptions {
3
+ /** Every step's id in the order people meet them, so a step only some people see keeps its place. */
4
+ order?: string[];
5
+ /** "questions" (the default), a "checklist" or a "tour". */
6
+ type?: OnboardingReportType;
7
+ }
8
+ export interface OnboardingReport {
9
+ /** The person sees this step; `title` is its name in MRRocket. Sent once per step and page. */
10
+ saw(step: string, title: string): void;
11
+ /** The person answered a question with one of its options: its id, and its words in MRRocket. */
12
+ answered(question: string, answer: string, label?: string): void;
13
+ /** The person did this task or tour step. */
14
+ done(step: string): void;
15
+ /** The person closed it or skipped the rest, at this step if you name one. */
16
+ closed(step?: string): void;
17
+ }
18
+ /** The report behind `useOnboardingReport` and the script tag's `onboarding.report`, over the SDK's `track`. */
19
+ export declare const createOnboardingReport: (track: (event: string, props?: Record<string, unknown>) => void, name: string, options?: OnboardingReportOptions) => OnboardingReport;
@@ -0,0 +1,23 @@
1
+ // src/onboarding-report.ts
2
+ import { ONBOARDING_EVENT, eventSlug } from "./flows.js";
3
+ var KIND = { questions: "question", checklist: "checklist", tour: "tour" };
4
+ var createOnboardingReport = (track, name, options = {}) => {
5
+ const type = options.type ?? "questions";
6
+ const kind = KIND[type] ?? "question";
7
+ const seen = /* @__PURE__ */ new Set();
8
+ const send = (props) => track(ONBOARDING_EVENT, { code: eventSlug(name), name, type, ...options.order?.length ? { order: options.order } : {}, ...props });
9
+ return {
10
+ saw: (step, title) => {
11
+ if (seen.has(step)) return;
12
+ seen.add(step);
13
+ send({ step, title, kind, action: "shown" });
14
+ },
15
+ answered: (question, answer, label) => send({ step: question, kind: "question", action: "completed", question, answer, label: label ?? answer }),
16
+ done: (step) => send({ step, kind, action: "completed" }),
17
+ closed: (step) => send({ ...step ? { step } : {}, kind, action: "dismissed" })
18
+ };
19
+ };
20
+ export {
21
+ createOnboardingReport
22
+ };
23
+ //# sourceMappingURL=onboarding-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/onboarding-report.ts"],"sourcesContent":["/**\n * 0.22.0: an onboarding the app keeps in its own code. There is nothing to set up in MRRocket: the app reports its\n * steps by name, and the first report makes the item (Win customers › Onboarding, \"From your code\"). MRRocket then shows\n * who saw each step, who did it and what they answered, and keeps the answers on the contact. It never draws such an\n * item and has no say in when it shows; that stays in the app.\n *\n * const report = useOnboardingReport(\"Welcome questions\", { order: [\"goal\", \"role\", \"source\"] });\n * report.saw(\"goal\", \"What brings you here?\");\n * report.answered(\"goal\", \"dream\", \"Dream game\");\n * report.closed(\"goal\"); // the person skipped the rest\n *\n * Everything goes through `track`, so it follows the visitor's consent like any other event. Report the words in one\n * language, whatever the visitor reads: the ids count the people, the words only name them in MRRocket.\n */\nimport { ONBOARDING_EVENT, eventSlug } from \"./flows\";\n\nexport type OnboardingReportType = \"questions\" | \"checklist\" | \"tour\";\n\nexport interface OnboardingReportOptions {\n /** Every step's id in the order people meet them, so a step only some people see keeps its place. */\n order?: string[];\n /** \"questions\" (the default), a \"checklist\" or a \"tour\". */\n type?: OnboardingReportType;\n}\n\nexport interface OnboardingReport {\n /** The person sees this step; `title` is its name in MRRocket. Sent once per step and page. */\n saw(step: string, title: string): void;\n /** The person answered a question with one of its options: its id, and its words in MRRocket. */\n answered(question: string, answer: string, label?: string): void;\n /** The person did this task or tour step. */\n done(step: string): void;\n /** The person closed it or skipped the rest, at this step if you name one. */\n closed(step?: string): void;\n}\n\nconst KIND = { questions: \"question\", checklist: \"checklist\", tour: \"tour\" } as const;\n\n/** The report behind `useOnboardingReport` and the script tag's `onboarding.report`, over the SDK's `track`. */\nexport const createOnboardingReport = (track: (event: string, props?: Record<string, unknown>) => void, name: string, options: OnboardingReportOptions = {}): OnboardingReport => {\n const type = options.type ?? \"questions\";\n const kind = KIND[type] ?? \"question\";\n const seen = new Set<string>();\n const send = (props: Record<string, unknown>) =>\n track(ONBOARDING_EVENT, { code: eventSlug(name), name, type, ...(options.order?.length ? { order: options.order } : {}), ...props });\n return {\n saw: (step, title) => {\n if (seen.has(step)) return;\n seen.add(step);\n send({ step, title, kind, action: \"shown\" });\n },\n answered: (question, answer, label) => send({ step: question, kind: \"question\", action: \"completed\", question, answer, label: label ?? answer }),\n done: (step) => send({ step, kind, action: \"completed\" }),\n closed: (step) => send({ ...(step ? { step } : {}), kind, action: \"dismissed\" }),\n };\n};\n"],"mappings":";AAcA,SAAS,kBAAkB,iBAAiB;AAsB5C,IAAM,OAAO,EAAE,WAAW,YAAY,WAAW,aAAa,MAAM,OAAO;AAGpE,IAAM,yBAAyB,CAAC,OAAiE,MAAc,UAAmC,CAAC,MAAwB;AAC9K,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,OAAO,KAAK,IAAI,KAAK;AAC3B,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,OAAO,CAAC,UACV,MAAM,kBAAkB,EAAE,MAAM,UAAU,IAAI,GAAG,MAAM,MAAM,GAAI,QAAQ,OAAO,SAAS,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC,GAAI,GAAG,MAAM,CAAC;AACvI,SAAO;AAAA,IACH,KAAK,CAAC,MAAM,UAAU;AAClB,UAAI,KAAK,IAAI,IAAI,EAAG;AACpB,WAAK,IAAI,IAAI;AACb,WAAK,EAAE,MAAM,OAAO,MAAM,QAAQ,QAAQ,CAAC;AAAA,IAC/C;AAAA,IACA,UAAU,CAAC,UAAU,QAAQ,UAAU,KAAK,EAAE,MAAM,UAAU,MAAM,YAAY,QAAQ,aAAa,UAAU,QAAQ,OAAO,SAAS,OAAO,CAAC;AAAA,IAC/I,MAAM,CAAC,SAAS,KAAK,EAAE,MAAM,MAAM,QAAQ,YAAY,CAAC;AAAA,IACxD,QAAQ,CAAC,SAAS,KAAK,EAAE,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC,GAAI,MAAM,QAAQ,YAAY,CAAC;AAAA,EACnF;AACJ;","names":[]}
@@ -1,4 +1,5 @@
1
1
  import type { FlowsController, OnboardingFlow, OnboardingItemState } from "./flows.js";
2
+ import { type OnboardingReport, type OnboardingReportOptions } from "./onboarding-report.js";
2
3
  export interface UseOnboarding {
3
4
  /** The item as MRRocket published it, or null while the config is loading, or when it is not live. */
4
5
  item: OnboardingFlow | null;
@@ -21,3 +22,5 @@ export interface UseOnboarding {
21
22
  }
22
23
  /** The hook behind `useOnboarding` on both platforms. */
23
24
  export declare const useOnboardingItem: (flows: FlowsController | null | undefined, id: string | undefined, start: (id?: string) => boolean) => UseOnboarding;
25
+ /** 0.22.0: the hook behind `useOnboardingReport` on both platforms: one report per name and options. */
26
+ export declare const useReportWith: (track: (event: string, props?: Record<string, unknown>) => void, name: string, options?: OnboardingReportOptions) => OnboardingReport;
@@ -1,5 +1,6 @@
1
1
  // src/onboarding-state.ts
2
2
  import { useEffect, useMemo, useState } from "react";
3
+ import { createOnboardingReport } from "./onboarding-report.js";
3
4
  var none = [];
4
5
  var useOnboardingItem = (flows, id, start) => {
5
6
  const [state, setState] = useState(() => flows && id ? flows.onboarding.item(id) : null);
@@ -37,7 +38,12 @@ var useOnboardingItem = (flows, id, start) => {
37
38
  [state, flows, id, start]
38
39
  );
39
40
  };
41
+ var useReportWith = (track, name, options) => {
42
+ const key = JSON.stringify(options ?? null);
43
+ return useMemo(() => createOnboardingReport(track, name, options), [track, name, key]);
44
+ };
40
45
  export {
41
- useOnboardingItem
46
+ useOnboardingItem,
47
+ useReportWith
42
48
  };
43
49
  //# sourceMappingURL=onboarding-state.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/onboarding-state.ts"],"sourcesContent":["/**\n * 0.18.0: `useOnboarding(id)` for an onboarding item the app draws itself (\"Shown by: your code\" in MRRocket): its\n * content and the person's progress, and the calls that report what they did. Shared by the Next.js provider and React\n * Native (only React here). The SDK draws nothing for such an item and never starts it; what the app reports still\n * reaches MRRocket (who saw it, who finished it, the answers on the contact), and the item after it follows as usual.\n *\n * const { item, steps, done, complete, dismiss, answer } = useOnboarding(\"flow_123\");\n * if (!item || done) return null;\n * return steps.map((s) => <Task key={s.id} title={s.title} done={s.done} onPress={() => complete(s.id)} />);\n */\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { FlowsController, OnboardingFlow, OnboardingItemState } from \"./flows\";\n\nexport interface UseOnboarding {\n /** The item as MRRocket published it, or null while the config is loading, or when it is not live. */\n item: OnboardingFlow | null;\n /** Its steps (a tour's steps, a checklist's tasks, the questions), each with `done` (and a question's `answer`). */\n steps: OnboardingItemState[\"steps\"];\n /** Every step is done. */\n done: boolean;\n /** The person closed it. */\n dismissed: boolean;\n /** Its moment has come for this person (the trigger MRRocket set, and who it is for). */\n eligible: boolean;\n /** Marks a step done (a tour step, a task). */\n complete(stepId: string): void;\n /** The person closed it; the item after it follows. */\n dismiss(): void;\n /** Answers a question (its step id or its key) with an option's id or a text; it is kept on the contact. */\n answer(question: string, value: string): void;\n /** Starts an item the SDK draws (the first live one without an id). */\n start(id?: string): boolean;\n}\n\nconst none: OnboardingItemState[\"steps\"] = [];\n\n/** The hook behind `useOnboarding` on both platforms. */\nexport const useOnboardingItem = (flows: FlowsController | null | undefined, id: string | undefined, start: (id?: string) => boolean): UseOnboarding => {\n const [state, setState] = useState<OnboardingItemState | null>(() => (flows && id ? flows.onboarding.item(id) : null));\n useEffect(() => {\n if (!flows || !id) {\n setState(null);\n return;\n }\n const up = () => {\n const s = flows.onboarding.item(id);\n setState(s);\n // Asked for by the app: counted as seen (started, and its first open steps), once per page.\n if (s && !s.done && !s.dismissed) flows.onboarding.seen(id);\n };\n up();\n return flows.subscribe(up);\n }, [flows, id]);\n return useMemo(\n () => ({\n item: state?.item ?? null,\n steps: state?.steps ?? none,\n done: !!state?.done,\n dismissed: !!state?.dismissed,\n eligible: !!state?.eligible,\n complete: (stepId: string) => {\n if (flows && id) flows.onboarding.complete(id, stepId);\n },\n dismiss: () => {\n if (flows && id) flows.onboarding.dismissItem(id);\n },\n answer: (question: string, value: string) => {\n if (flows && id) flows.onboarding.answerItem(id, question, value);\n },\n start,\n }),\n [state, flows, id, start],\n );\n};\n"],"mappings":";AAUA,SAAS,WAAW,SAAS,gBAAgB;AAwB7C,IAAM,OAAqC,CAAC;AAGrC,IAAM,oBAAoB,CAAC,OAA2C,IAAwB,UAAmD;AACpJ,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAqC,MAAO,SAAS,KAAK,MAAM,WAAW,KAAK,EAAE,IAAI,IAAK;AACrH,YAAU,MAAM;AACZ,QAAI,CAAC,SAAS,CAAC,IAAI;AACf,eAAS,IAAI;AACb;AAAA,IACJ;AACA,UAAM,KAAK,MAAM;AACb,YAAM,IAAI,MAAM,WAAW,KAAK,EAAE;AAClC,eAAS,CAAC;AAEV,UAAI,KAAK,CAAC,EAAE,QAAQ,CAAC,EAAE,UAAW,OAAM,WAAW,KAAK,EAAE;AAAA,IAC9D;AACA,OAAG;AACH,WAAO,MAAM,UAAU,EAAE;AAAA,EAC7B,GAAG,CAAC,OAAO,EAAE,CAAC;AACd,SAAO;AAAA,IACH,OAAO;AAAA,MACH,MAAM,OAAO,QAAQ;AAAA,MACrB,OAAO,OAAO,SAAS;AAAA,MACvB,MAAM,CAAC,CAAC,OAAO;AAAA,MACf,WAAW,CAAC,CAAC,OAAO;AAAA,MACpB,UAAU,CAAC,CAAC,OAAO;AAAA,MACnB,UAAU,CAAC,WAAmB;AAC1B,YAAI,SAAS,GAAI,OAAM,WAAW,SAAS,IAAI,MAAM;AAAA,MACzD;AAAA,MACA,SAAS,MAAM;AACX,YAAI,SAAS,GAAI,OAAM,WAAW,YAAY,EAAE;AAAA,MACpD;AAAA,MACA,QAAQ,CAAC,UAAkB,UAAkB;AACzC,YAAI,SAAS,GAAI,OAAM,WAAW,WAAW,IAAI,UAAU,KAAK;AAAA,MACpE;AAAA,MACA;AAAA,IACJ;AAAA,IACA,CAAC,OAAO,OAAO,IAAI,KAAK;AAAA,EAC5B;AACJ;","names":[]}
1
+ {"version":3,"sources":["../src/onboarding-state.ts"],"sourcesContent":["/**\n * 0.18.0: `useOnboarding(id)` for an onboarding item the app draws itself (\"Shown by: your code\" in MRRocket): its\n * content and the person's progress, and the calls that report what they did. Shared by the Next.js provider and React\n * Native (only React here). The SDK draws nothing for such an item and never starts it; what the app reports still\n * reaches MRRocket (who saw it, who finished it, the answers on the contact), and the item after it follows as usual.\n *\n * const { item, steps, done, complete, dismiss, answer } = useOnboarding(\"flow_123\");\n * if (!item || done) return null;\n * return steps.map((s) => <Task key={s.id} title={s.title} done={s.done} onPress={() => complete(s.id)} />);\n */\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { FlowsController, OnboardingFlow, OnboardingItemState } from \"./flows\";\nimport { type OnboardingReport, type OnboardingReportOptions, createOnboardingReport } from \"./onboarding-report\";\n\nexport interface UseOnboarding {\n /** The item as MRRocket published it, or null while the config is loading, or when it is not live. */\n item: OnboardingFlow | null;\n /** Its steps (a tour's steps, a checklist's tasks, the questions), each with `done` (and a question's `answer`). */\n steps: OnboardingItemState[\"steps\"];\n /** Every step is done. */\n done: boolean;\n /** The person closed it. */\n dismissed: boolean;\n /** Its moment has come for this person (the trigger MRRocket set, and who it is for). */\n eligible: boolean;\n /** Marks a step done (a tour step, a task). */\n complete(stepId: string): void;\n /** The person closed it; the item after it follows. */\n dismiss(): void;\n /** Answers a question (its step id or its key) with an option's id or a text; it is kept on the contact. */\n answer(question: string, value: string): void;\n /** Starts an item the SDK draws (the first live one without an id). */\n start(id?: string): boolean;\n}\n\nconst none: OnboardingItemState[\"steps\"] = [];\n\n/** The hook behind `useOnboarding` on both platforms. */\nexport const useOnboardingItem = (flows: FlowsController | null | undefined, id: string | undefined, start: (id?: string) => boolean): UseOnboarding => {\n const [state, setState] = useState<OnboardingItemState | null>(() => (flows && id ? flows.onboarding.item(id) : null));\n useEffect(() => {\n if (!flows || !id) {\n setState(null);\n return;\n }\n const up = () => {\n const s = flows.onboarding.item(id);\n setState(s);\n // Asked for by the app: counted as seen (started, and its first open steps), once per page.\n if (s && !s.done && !s.dismissed) flows.onboarding.seen(id);\n };\n up();\n return flows.subscribe(up);\n }, [flows, id]);\n return useMemo(\n () => ({\n item: state?.item ?? null,\n steps: state?.steps ?? none,\n done: !!state?.done,\n dismissed: !!state?.dismissed,\n eligible: !!state?.eligible,\n complete: (stepId: string) => {\n if (flows && id) flows.onboarding.complete(id, stepId);\n },\n dismiss: () => {\n if (flows && id) flows.onboarding.dismissItem(id);\n },\n answer: (question: string, value: string) => {\n if (flows && id) flows.onboarding.answerItem(id, question, value);\n },\n start,\n }),\n [state, flows, id, start],\n );\n};\n\n/** 0.22.0: the hook behind `useOnboardingReport` on both platforms: one report per name and options. */\nexport const useReportWith = (track: (event: string, props?: Record<string, unknown>) => void, name: string, options?: OnboardingReportOptions): OnboardingReport => {\n const key = JSON.stringify(options ?? null);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n return useMemo(() => createOnboardingReport(track, name, options), [track, name, key]);\n};\n"],"mappings":";AAUA,SAAS,WAAW,SAAS,gBAAgB;AAE7C,SAA8D,8BAA8B;AAuB5F,IAAM,OAAqC,CAAC;AAGrC,IAAM,oBAAoB,CAAC,OAA2C,IAAwB,UAAmD;AACpJ,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAqC,MAAO,SAAS,KAAK,MAAM,WAAW,KAAK,EAAE,IAAI,IAAK;AACrH,YAAU,MAAM;AACZ,QAAI,CAAC,SAAS,CAAC,IAAI;AACf,eAAS,IAAI;AACb;AAAA,IACJ;AACA,UAAM,KAAK,MAAM;AACb,YAAM,IAAI,MAAM,WAAW,KAAK,EAAE;AAClC,eAAS,CAAC;AAEV,UAAI,KAAK,CAAC,EAAE,QAAQ,CAAC,EAAE,UAAW,OAAM,WAAW,KAAK,EAAE;AAAA,IAC9D;AACA,OAAG;AACH,WAAO,MAAM,UAAU,EAAE;AAAA,EAC7B,GAAG,CAAC,OAAO,EAAE,CAAC;AACd,SAAO;AAAA,IACH,OAAO;AAAA,MACH,MAAM,OAAO,QAAQ;AAAA,MACrB,OAAO,OAAO,SAAS;AAAA,MACvB,MAAM,CAAC,CAAC,OAAO;AAAA,MACf,WAAW,CAAC,CAAC,OAAO;AAAA,MACpB,UAAU,CAAC,CAAC,OAAO;AAAA,MACnB,UAAU,CAAC,WAAmB;AAC1B,YAAI,SAAS,GAAI,OAAM,WAAW,SAAS,IAAI,MAAM;AAAA,MACzD;AAAA,MACA,SAAS,MAAM;AACX,YAAI,SAAS,GAAI,OAAM,WAAW,YAAY,EAAE;AAAA,MACpD;AAAA,MACA,QAAQ,CAAC,UAAkB,UAAkB;AACzC,YAAI,SAAS,GAAI,OAAM,WAAW,WAAW,IAAI,UAAU,KAAK;AAAA,MACpE;AAAA,MACA;AAAA,IACJ;AAAA,IACA,CAAC,OAAO,OAAO,IAAI,KAAK;AAAA,EAC5B;AACJ;AAGO,IAAM,gBAAgB,CAAC,OAAiE,MAAc,YAAwD;AACjK,QAAM,MAAM,KAAK,UAAU,WAAW,IAAI;AAE1C,SAAO,QAAQ,MAAM,uBAAuB,OAAO,MAAM,OAAO,GAAG,CAAC,OAAO,MAAM,GAAG,CAAC;AACzF;","names":[]}
package/dist/paywall.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/paywall.ts
2
2
  import { assign } from "./core.js";
3
+ import { t } from "./i18n.js";
3
4
  var PAYWALL_EVENT = "$paywall";
4
5
  var paywallPages = (config, w, distinctId) => {
5
6
  const e = w.experiment ? config?.experiments.find((x) => x.key === w.experiment) : void 0;
@@ -58,7 +59,7 @@ var createPaywallHost = (env) => {
58
59
  },
59
60
  () => {
60
61
  c.busy = false;
61
- c.error = "The purchase did not go through. Please try again.";
62
+ c.error = t("The purchase did not go through. Please try again.");
62
63
  emit();
63
64
  }
64
65
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/paywall.ts"],"sourcesContent":["/**\n * Paywalls with several pages (SDK 0.14.0): what MRRocket's config serves under `paywalls` (one per placement, built\n * in MRRocket › Conversion › Paywalls) and a small controller that walks the person through its pages, page after page,\n * until the last one hands the chosen plan to the app. No DOM and no React: the web renderer (`mountPaywall` in\n * flows-ui.ts, loaded only when a paywall opens) and the React Native one (native/flows-view.ts) draw `view()` and\n * call the actions.\n *\n * MRRocket never takes a payment. The last page's button calls the app's `onPurchase(plan)`, which buys through the\n * app's own store code (StoreKit or Play through RevenueCat or Adapty, or Stripe Checkout on the web); only when it\n * resolves is the purchase recorded. It throwing keeps the paywall open with an error, resolving `false` (the person\n * closed the store sheet) keeps it open without one.\n *\n * A running test on the paywall (`experiment`) gives each person their variant's pages, by the same stable hash as\n * every other test (`assign`), and records `$exposure` when the paywall opens. What it records, as normal events under\n * the SDK's analytics consent:\n * - `$paywall` `{paywall, placement, variant, page, index, action: \"page\"}` the first time each page shows in a run,\n * so MRRocket shows how many reach page 2 and 3;\n * - `{action: \"purchased\", plan}` after `onPurchase` went through (a test's purchase goal counts it), and\n * `{action: \"closed\", index}` when the person closed it.\n */\nimport { type CockpitConfig, type VariantId, assign } from \"./core\";\n\nexport const PAYWALL_EVENT = \"$paywall\";\n\n/** One page of a paywall. Plans pages list the plans and buy; the others say why and go on. */\nexport interface PaywallPage {\n id: string;\n /** \"benefits\": a list of what the person gets; \"proof\": quotes of happy customers; \"plans\": the plans and the buy button. */\n kind: \"benefits\" | \"proof\" | \"plans\";\n title: string;\n body?: string;\n /** Benefits: one line each. */\n items?: string[];\n /** Proof: what customers said, and who. */\n quotes?: { text: string; by?: string }[];\n /** The button: \"Continue\" on the way, the buy button on the plans page (\"Start free trial\"). */\n button?: string;\n}\n\n/** A plan the paywall offers. `id` is what the app buys: the store product id or the Stripe price, as set in MRRocket. */\nexport interface PaywallPlan {\n id: string;\n name: string;\n /** As shown (\"€59.99 / year\"). The app's own store prices win when it passes `plans`. */\n price: string;\n /** A line under it (\"€5 a month\", \"7 days free\"). */\n note?: string;\n}\n\n/** A published paywall as the config serves it. */\nexport interface PaywallConfig {\n id: string;\n /** The placement's key (`onboarding_end`), what the app opens: `paywall.open(\"onboarding_end\")`. */\n placement: string;\n /** The running test on it, a key of `experiments`; each of its variants has pages under `variants`. */\n experiment?: string;\n /** Control (the live paywall) first. */\n variants: { id: VariantId; pages: PaywallPage[] }[];\n plans: PaywallPlan[];\n /** The plan picked before the person picks one. */\n highlight?: string;\n}\n\nexport interface PaywallResult {\n /**\n * \"purchased\": `onPurchase` went through; \"chosen\": the person picked a plan and there is no `onPurchase`, so the app\n * buys it now (the store's purchase still counts for a test through RevenueCat or Adapty); \"closed\": the person\n * closed it (or there was no paywall).\n */\n outcome: \"purchased\" | \"chosen\" | \"closed\";\n plan?: PaywallPlan;\n paywall?: string;\n variant?: VariantId;\n /** The page the person was on, from 0. */\n index: number;\n}\n\nexport interface PaywallOptions {\n /**\n * Buy the plan in your code (RevenueCat `purchasePackage`, Adapty `makePurchase`, Stripe Checkout). Throwing keeps\n * the paywall open with an error; `false` keeps it open without one (the person cancelled the store sheet).\n */\n onPurchase?: (plan: PaywallPlan) => unknown;\n /** Your store's plans with their local prices, by the same ids; they replace what MRRocket has for them. */\n plans?: PaywallPlan[];\n onClose?: (result: PaywallResult) => void;\n}\n\nexport interface PaywallView {\n paywall: PaywallConfig;\n variant: VariantId;\n page: PaywallPage;\n index: number;\n total: number;\n last: boolean;\n plans: PaywallPlan[];\n /** The picked plan's id. */\n plan?: string;\n busy: boolean;\n error?: string;\n}\n\nexport interface PaywallEnv {\n config: () => CockpitConfig | null;\n distinctId: () => string;\n /** Records an event under the SDK's consent. */\n track: (event: string, props?: Record<string, unknown>) => void;\n}\n\nexport interface PaywallHost {\n /** Opens the paywall of a placement; resolves when it is bought or closed (at once with \"closed\" when there is none). */\n open(placement: string, options?: PaywallOptions): Promise<PaywallResult>;\n view(): PaywallView | null;\n subscribe(listener: () => void): () => void;\n next(): void;\n back(): void;\n pick(planId: string): void;\n close(): void;\n}\n\n/** The pages of a person's variant: the test's when it runs (or shipped), else control's. */\nexport const paywallPages = (config: CockpitConfig | null, w: PaywallConfig, distinctId: string) => {\n const e = w.experiment ? config?.experiments.find((x) => x.key === w.experiment) : undefined;\n const id = e ? assign(e, distinctId) : w.variants[0]?.id;\n const v = w.variants.find((x) => x.id === id) ?? w.variants[0];\n return v ? { variant: v.id, pages: v.pages, test: e?.status === \"running\" && !e.winner ? e.key : undefined } : null;\n};\n\nexport const createPaywallHost = (env: PaywallEnv): PaywallHost => {\n let run: {\n w: PaywallConfig;\n variant: VariantId;\n pages: PaywallPage[];\n plans: PaywallPlan[];\n index: number;\n plan?: string;\n busy: boolean;\n error?: string;\n seen: Set<number>;\n options: PaywallOptions;\n resolve: (r: PaywallResult) => void;\n promise: Promise<PaywallResult>;\n } | null = null;\n const listeners = new Set<() => void>();\n const emit = () => {\n for (const l of [...listeners]) {\n try {\n l();\n } catch {\n // One renderer must not stop another.\n }\n }\n };\n const base = () => (run ? { paywall: run.w.id, placement: run.w.placement, variant: run.variant } : {});\n const show = () => {\n if (run && !run.seen.has(run.index)) {\n run.seen.add(run.index);\n env.track(PAYWALL_EVENT, { ...base(), page: run.pages[run.index].id, index: run.index, action: \"page\" });\n }\n emit();\n };\n const end = (r: PaywallResult) => {\n const c = run;\n if (!c) return;\n run = null;\n if (r.outcome === \"closed\") c.options.onClose?.(r);\n c.resolve(r);\n emit();\n };\n const result = (outcome: PaywallResult[\"outcome\"]): PaywallResult => ({\n outcome,\n index: run?.index ?? 0,\n ...(run ? { paywall: run.w.id, variant: run.variant } : {}),\n ...(outcome !== \"closed\" && run ? { plan: run.plans.find((p) => p.id === run!.plan) } : {}),\n });\n\n const buy = () => {\n const c = run;\n const plan = c?.plans.find((p) => p.id === c.plan);\n if (!c || !plan || c.busy) return;\n if (!c.options.onPurchase) return end(result(\"chosen\"));\n c.busy = true;\n c.error = undefined;\n emit();\n Promise.resolve()\n .then(() => c.options.onPurchase?.(plan))\n .then(\n (ok) => {\n c.busy = false;\n if (run !== c) return;\n if (ok === false) return emit();\n env.track(PAYWALL_EVENT, { ...base(), page: c.pages[c.index].id, index: c.index, plan: plan.id, action: \"purchased\" });\n end(result(\"purchased\"));\n },\n () => {\n c.busy = false;\n c.error = \"The purchase did not go through. Please try again.\";\n emit();\n },\n );\n };\n\n return {\n open: (placement, options = {}) => {\n if (run) return run.promise;\n const config = env.config();\n const w = config?.paywalls?.find((x) => x.placement === placement);\n const picked = w ? paywallPages(config, w, env.distinctId()) : null;\n if (!w || !picked?.pages.length) return Promise.resolve({ outcome: \"closed\", index: 0 });\n // The app's store prices replace MRRocket's for the same plans.\n const own = new Map((options.plans ?? []).map((p) => [p.id, p]));\n const plans = w.plans.map((p) => own.get(p.id) ?? p);\n let resolve: (r: PaywallResult) => void = () => {};\n const promise = new Promise<PaywallResult>((r) => (resolve = r));\n const plan = plans.some((p) => p.id === w.highlight) ? w.highlight : plans[0]?.id;\n run = { w, variant: picked.variant, pages: picked.pages, plans, index: 0, plan, busy: false, seen: new Set(), options, resolve, promise };\n if (picked.test) env.track(\"$exposure\", { experiment: picked.test, variant: picked.variant });\n show();\n return promise;\n },\n view: () =>\n run\n ? {\n paywall: run.w,\n variant: run.variant,\n page: run.pages[run.index],\n index: run.index,\n total: run.pages.length,\n last: run.index === run.pages.length - 1,\n plans: run.plans,\n plan: run.plan,\n busy: run.busy,\n error: run.error,\n }\n : null,\n subscribe: (l) => {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n next: () => {\n if (!run || run.busy) return;\n // A plans page or the last page buys; any other page goes on.\n if (run.pages[run.index].kind === \"plans\" || run.index === run.pages.length - 1) return buy();\n run.index++;\n run.error = undefined;\n show();\n },\n back: () => {\n if (!run || run.busy || !run.index) return;\n run.index--;\n run.error = undefined;\n emit();\n },\n pick: (id) => {\n if (!run || run.busy || !run.plans.some((p) => p.id === id)) return;\n run.plan = id;\n run.error = undefined;\n emit();\n },\n close: () => {\n if (!run || run.busy) return;\n env.track(PAYWALL_EVENT, { ...base(), index: run.index, action: \"closed\" });\n end(result(\"closed\"));\n },\n };\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/paywall.ts"],"sourcesContent":["/**\n * Paywalls with several pages (SDK 0.14.0): what MRRocket's config serves under `paywalls` (one per placement, built\n * in MRRocket › Conversion › Paywalls) and a small controller that walks the person through its pages, page after page,\n * until the last one hands the chosen plan to the app. No DOM and no React: the web renderer (`mountPaywall` in\n * flows-ui.ts, loaded only when a paywall opens) and the React Native one (native/flows-view.ts) draw `view()` and\n * call the actions.\n *\n * MRRocket never takes a payment. The last page's button calls the app's `onPurchase(plan)`, which buys through the\n * app's own store code (StoreKit or Play through RevenueCat or Adapty, or Stripe Checkout on the web); only when it\n * resolves is the purchase recorded. It throwing keeps the paywall open with an error, resolving `false` (the person\n * closed the store sheet) keeps it open without one.\n *\n * A running test on the paywall (`experiment`) gives each person their variant's pages, by the same stable hash as\n * every other test (`assign`), and records `$exposure` when the paywall opens. What it records, as normal events under\n * the SDK's analytics consent:\n * - `$paywall` `{paywall, placement, variant, page, index, action: \"page\"}` the first time each page shows in a run,\n * so MRRocket shows how many reach page 2 and 3;\n * - `{action: \"purchased\", plan}` after `onPurchase` went through (a test's purchase goal counts it), and\n * `{action: \"closed\", index}` when the person closed it.\n */\nimport { type CockpitConfig, type VariantId, assign } from \"./core\";\nimport { t } from \"./i18n\";\n\nexport const PAYWALL_EVENT = \"$paywall\";\n\n/** One page of a paywall. Plans pages list the plans and buy; the others say why and go on. */\nexport interface PaywallPage {\n id: string;\n /** \"benefits\": a list of what the person gets; \"proof\": quotes of happy customers; \"plans\": the plans and the buy button. */\n kind: \"benefits\" | \"proof\" | \"plans\";\n title: string;\n body?: string;\n /** Benefits: one line each. */\n items?: string[];\n /** Proof: what customers said, and who. */\n quotes?: { text: string; by?: string }[];\n /** The button: \"Continue\" on the way, the buy button on the plans page (\"Start free trial\"). */\n button?: string;\n}\n\n/** A plan the paywall offers. `id` is what the app buys: the store product id or the Stripe price, as set in MRRocket. */\nexport interface PaywallPlan {\n id: string;\n name: string;\n /** As shown (\"€59.99 / year\"). The app's own store prices win when it passes `plans`. */\n price: string;\n /** A line under it (\"€5 a month\", \"7 days free\"). */\n note?: string;\n}\n\n/** A published paywall as the config serves it. */\nexport interface PaywallConfig {\n id: string;\n /** The placement's key (`onboarding_end`), what the app opens: `paywall.open(\"onboarding_end\")`. */\n placement: string;\n /** The running test on it, a key of `experiments`; each of its variants has pages under `variants`. */\n experiment?: string;\n /** Control (the live paywall) first. */\n variants: { id: VariantId; pages: PaywallPage[] }[];\n plans: PaywallPlan[];\n /** The plan picked before the person picks one. */\n highlight?: string;\n}\n\nexport interface PaywallResult {\n /**\n * \"purchased\": `onPurchase` went through; \"chosen\": the person picked a plan and there is no `onPurchase`, so the app\n * buys it now (the store's purchase still counts for a test through RevenueCat or Adapty); \"closed\": the person\n * closed it (or there was no paywall).\n */\n outcome: \"purchased\" | \"chosen\" | \"closed\";\n plan?: PaywallPlan;\n paywall?: string;\n variant?: VariantId;\n /** The page the person was on, from 0. */\n index: number;\n}\n\nexport interface PaywallOptions {\n /**\n * Buy the plan in your code (RevenueCat `purchasePackage`, Adapty `makePurchase`, Stripe Checkout). Throwing keeps\n * the paywall open with an error; `false` keeps it open without one (the person cancelled the store sheet).\n */\n onPurchase?: (plan: PaywallPlan) => unknown;\n /** Your store's plans with their local prices, by the same ids; they replace what MRRocket has for them. */\n plans?: PaywallPlan[];\n onClose?: (result: PaywallResult) => void;\n}\n\nexport interface PaywallView {\n paywall: PaywallConfig;\n variant: VariantId;\n page: PaywallPage;\n index: number;\n total: number;\n last: boolean;\n plans: PaywallPlan[];\n /** The picked plan's id. */\n plan?: string;\n busy: boolean;\n error?: string;\n}\n\nexport interface PaywallEnv {\n config: () => CockpitConfig | null;\n distinctId: () => string;\n /** Records an event under the SDK's consent. */\n track: (event: string, props?: Record<string, unknown>) => void;\n}\n\nexport interface PaywallHost {\n /** Opens the paywall of a placement; resolves when it is bought or closed (at once with \"closed\" when there is none). */\n open(placement: string, options?: PaywallOptions): Promise<PaywallResult>;\n view(): PaywallView | null;\n subscribe(listener: () => void): () => void;\n next(): void;\n back(): void;\n pick(planId: string): void;\n close(): void;\n}\n\n/** The pages of a person's variant: the test's when it runs (or shipped), else control's. */\nexport const paywallPages = (config: CockpitConfig | null, w: PaywallConfig, distinctId: string) => {\n const e = w.experiment ? config?.experiments.find((x) => x.key === w.experiment) : undefined;\n const id = e ? assign(e, distinctId) : w.variants[0]?.id;\n const v = w.variants.find((x) => x.id === id) ?? w.variants[0];\n return v ? { variant: v.id, pages: v.pages, test: e?.status === \"running\" && !e.winner ? e.key : undefined } : null;\n};\n\nexport const createPaywallHost = (env: PaywallEnv): PaywallHost => {\n let run: {\n w: PaywallConfig;\n variant: VariantId;\n pages: PaywallPage[];\n plans: PaywallPlan[];\n index: number;\n plan?: string;\n busy: boolean;\n error?: string;\n seen: Set<number>;\n options: PaywallOptions;\n resolve: (r: PaywallResult) => void;\n promise: Promise<PaywallResult>;\n } | null = null;\n const listeners = new Set<() => void>();\n const emit = () => {\n for (const l of [...listeners]) {\n try {\n l();\n } catch {\n // One renderer must not stop another.\n }\n }\n };\n const base = () => (run ? { paywall: run.w.id, placement: run.w.placement, variant: run.variant } : {});\n const show = () => {\n if (run && !run.seen.has(run.index)) {\n run.seen.add(run.index);\n env.track(PAYWALL_EVENT, { ...base(), page: run.pages[run.index].id, index: run.index, action: \"page\" });\n }\n emit();\n };\n const end = (r: PaywallResult) => {\n const c = run;\n if (!c) return;\n run = null;\n if (r.outcome === \"closed\") c.options.onClose?.(r);\n c.resolve(r);\n emit();\n };\n const result = (outcome: PaywallResult[\"outcome\"]): PaywallResult => ({\n outcome,\n index: run?.index ?? 0,\n ...(run ? { paywall: run.w.id, variant: run.variant } : {}),\n ...(outcome !== \"closed\" && run ? { plan: run.plans.find((p) => p.id === run!.plan) } : {}),\n });\n\n const buy = () => {\n const c = run;\n const plan = c?.plans.find((p) => p.id === c.plan);\n if (!c || !plan || c.busy) return;\n if (!c.options.onPurchase) return end(result(\"chosen\"));\n c.busy = true;\n c.error = undefined;\n emit();\n Promise.resolve()\n .then(() => c.options.onPurchase?.(plan))\n .then(\n (ok) => {\n c.busy = false;\n if (run !== c) return;\n if (ok === false) return emit();\n env.track(PAYWALL_EVENT, { ...base(), page: c.pages[c.index].id, index: c.index, plan: plan.id, action: \"purchased\" });\n end(result(\"purchased\"));\n },\n () => {\n c.busy = false;\n c.error = t(\"The purchase did not go through. Please try again.\");\n emit();\n },\n );\n };\n\n return {\n open: (placement, options = {}) => {\n if (run) return run.promise;\n const config = env.config();\n const w = config?.paywalls?.find((x) => x.placement === placement);\n const picked = w ? paywallPages(config, w, env.distinctId()) : null;\n if (!w || !picked?.pages.length) return Promise.resolve({ outcome: \"closed\", index: 0 });\n // The app's store prices replace MRRocket's for the same plans.\n const own = new Map((options.plans ?? []).map((p) => [p.id, p]));\n const plans = w.plans.map((p) => own.get(p.id) ?? p);\n let resolve: (r: PaywallResult) => void = () => {};\n const promise = new Promise<PaywallResult>((r) => (resolve = r));\n const plan = plans.some((p) => p.id === w.highlight) ? w.highlight : plans[0]?.id;\n run = { w, variant: picked.variant, pages: picked.pages, plans, index: 0, plan, busy: false, seen: new Set(), options, resolve, promise };\n if (picked.test) env.track(\"$exposure\", { experiment: picked.test, variant: picked.variant });\n show();\n return promise;\n },\n view: () =>\n run\n ? {\n paywall: run.w,\n variant: run.variant,\n page: run.pages[run.index],\n index: run.index,\n total: run.pages.length,\n last: run.index === run.pages.length - 1,\n plans: run.plans,\n plan: run.plan,\n busy: run.busy,\n error: run.error,\n }\n : null,\n subscribe: (l) => {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n next: () => {\n if (!run || run.busy) return;\n // A plans page or the last page buys; any other page goes on.\n if (run.pages[run.index].kind === \"plans\" || run.index === run.pages.length - 1) return buy();\n run.index++;\n run.error = undefined;\n show();\n },\n back: () => {\n if (!run || run.busy || !run.index) return;\n run.index--;\n run.error = undefined;\n emit();\n },\n pick: (id) => {\n if (!run || run.busy || !run.plans.some((p) => p.id === id)) return;\n run.plan = id;\n run.error = undefined;\n emit();\n },\n close: () => {\n if (!run || run.busy) return;\n env.track(PAYWALL_EVENT, { ...base(), index: run.index, action: \"closed\" });\n end(result(\"closed\"));\n },\n };\n};\n"],"mappings":"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","names":[]}
@@ -61,6 +61,11 @@ interface SurveyQuestion {
61
61
  description?: string;
62
62
  required: boolean;
63
63
  options?: string[];
64
+ /**
65
+ * 0.21.0: what each option says in the visitor's language, parallel to `options`. An answer is still recorded as the
66
+ * `options` value, so rules and numbers do not split by language.
67
+ */
68
+ labels?: string[];
64
69
  scale?: number;
65
70
  low?: string;
66
71
  high?: string;
@@ -165,6 +170,8 @@ interface MessengerLook {
165
170
  /** A round chat button in the app (React Native) too. */
166
171
  appLauncher?: boolean;
167
172
  help: boolean;
173
+ /** The Updates tab with the published changelog (MRRocket from 0.22; older servers leave it out). */
174
+ updates?: boolean;
168
175
  sound: boolean;
169
176
  preview: boolean;
170
177
  askEmail: boolean;
@@ -185,13 +192,24 @@ interface MessengerLook {
185
192
  /** The AI agent's picture beside its answers (from 0.17). */
186
193
  agentPhoto?: string;
187
194
  }
188
- /** The visitor's newest chat (open or closed) and its messages. */
195
+ /** One of the visitor's conversations in their list (0.19). */
196
+ interface ChatSummary {
197
+ id: string;
198
+ status: "open" | "solved";
199
+ /** The visitor's first words. */
200
+ subject: string;
201
+ /** The newest message, its body cut to a preview. */
202
+ last: ChatMessage | null;
203
+ }
204
+ /** One chat (the one asked for, else the visitor's newest, open or closed) and its messages. */
189
205
  interface ChatThread {
190
206
  conversation: {
191
207
  id: string;
192
208
  status: "open" | "solved";
193
209
  } | null;
194
210
  messages: ChatMessage[];
211
+ /** All the visitor's conversations, the latest activity first (0.19). */
212
+ conversations?: ChatSummary[];
195
213
  /** The visitor gave an email already: the messenger stops asking. */
196
214
  hasEmail: boolean;
197
215
  }
@@ -243,10 +261,15 @@ declare const createApi: (c: Conn) => {
243
261
  completed: boolean;
244
262
  seconds: number;
245
263
  }) => Promise<unknown>;
246
- chat: (signal?: AbortSignal) => Promise<ChatThread>;
247
- /** A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). */
264
+ /** One chat by id, else the visitor's newest; `conversations` lists them all. */
265
+ chat: (signal?: AbortSignal, conversation?: string) => Promise<ChatThread>;
266
+ /**
267
+ * A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). `conversation` is
268
+ * the chat it answers, or "new" to start one; without it, the running chat or else a new one.
269
+ */
248
270
  sendChat: (m: {
249
271
  body: string;
272
+ conversation?: string;
250
273
  email?: string;
251
274
  url?: string;
252
275
  attachments?: string[];
@@ -303,8 +326,10 @@ interface CancelOffer {
303
326
  /** 0.15.0: a second question after an answer ("What would be a fair price?"); no answers means a free text. */
304
327
  interface CancelFollowUp {
305
328
  question: string;
306
- /** The choices, in order; empty for a free text. */
329
+ /** The choices, in order; empty for a free text. What is recorded and what rules compare, whatever the language. */
307
330
  answers: string[];
331
+ /** 0.21.0: what each choice says in the visitor's language, parallel to `answers`; shown instead of them. */
332
+ answerLabels?: string[];
308
333
  }
309
334
  /**
310
335
  * 0.15.0: a different offer for some customers of one answer. Every condition that is set must hold; the first rule
@@ -387,11 +412,15 @@ type FlowIcon = {
387
412
  };
388
413
  /** 0.18.0: what plays when someone finishes a checklist, a tour or the questions. */
389
414
  type FinishAnimation = "none" | "confetti" | "fireworks" | "stars";
390
- /** 0.18.0: one question of a Questions item: one choice of 2 to 6 options, or a short text. */
415
+ /**
416
+ * 0.18.0: one question of a Questions item: one choice of 2 to 6 options, or a short text. 0.21.0: a number scale, an
417
+ * "Other" with the person's own words, and a Skip button. An older app shows a scale as a text field, leaves "Other"
418
+ * out, and offers no Skip (its close button still ends the window).
419
+ */
391
420
  interface OnboardingQuestion {
392
421
  /** The answer's name on the contact (`onboarding.<key>`) and in "People who answered …". */
393
422
  key: string;
394
- type: "choice" | "text";
423
+ type: "choice" | "text" | "scale";
395
424
  /** Choice: the options, in order; `id` is what is stored, `label` what people read. */
396
425
  options?: {
397
426
  id: string;
@@ -399,6 +428,19 @@ interface OnboardingQuestion {
399
428
  }[];
400
429
  /** Text: the field's hint. */
401
430
  placeholder?: string;
431
+ /** 0.21.0, choice: one more option where people type their own words, stored as `other:<words>`. */
432
+ other?: boolean;
433
+ /** 0.21.0: the words of that option; "Other" when unset. */
434
+ otherLabel?: string;
435
+ /** 0.21.0, scale: the lowest and highest number, and the words under both ends. */
436
+ min?: number;
437
+ max?: number;
438
+ lowLabel?: string;
439
+ highLabel?: string;
440
+ /** 0.21.0: people may go on without answering. */
441
+ optional?: boolean;
442
+ /** 0.21.0: the words of that button; "Skip" when unset. */
443
+ skipLabel?: string;
402
444
  }
403
445
  /** 0.17.0: where a tour bubble sits next to its element; "auto" (and no value) puts it below, else above, as before. */
404
446
  type TourPlacement = "auto" | "top" | "bottom" | "left" | "right";
@@ -628,6 +670,8 @@ interface FlowsController {
628
670
  startTour(stepId: string): boolean;
629
671
  open(stepId: string): boolean;
630
672
  answer(value: string): void;
673
+ /** 0.21.0: goes on from an optional question without an answer. */
674
+ skip(): void;
631
675
  back(): void;
632
676
  item(id: string): OnboardingItemState | null;
633
677
  seen(id: string): void;
@@ -782,7 +826,7 @@ interface PaywallHost {
782
826
 
783
827
  type VariantId = string;
784
828
  /** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
785
- declare const SDK_VERSION = "0.18.0";
829
+ declare const SDK_VERSION = "0.22.0";
786
830
  /** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
787
831
  interface VariantConfig {
788
832
  id: VariantId;
@@ -816,6 +860,10 @@ interface RedirectConfig {
816
860
  interface CockpitConfig {
817
861
  /** The product the public key belongs to. */
818
862
  project: string;
863
+ /** The language the server chose for the words below, like "de" (`lang` on the request). SDK 0.21.0+. */
864
+ lang?: string;
865
+ /** The SDK's own words: English source word to translation (see words.ts). Absent for English. SDK 0.21.0+. */
866
+ words?: Record<string, string>;
819
867
  experiments: ExperimentConfig[];
820
868
  /** The product's affiliate program: how many days a partner's referral is kept. Missing without a program. */
821
869
  referral?: {
@@ -983,6 +1031,11 @@ interface MRRocketOptions {
983
1031
  maxQueue?: number;
984
1032
  /** Log what the SDK does to the console. */
985
1033
  debug?: boolean;
1034
+ /**
1035
+ * 0.21.0: the language of the SDK's words and of what the server answers, like "de-AT", for an app with its own
1036
+ * language switch. Default: the phone's. `setLocale` does the same from anywhere.
1037
+ */
1038
+ locale?: string;
986
1039
  /** Onboarding flows start on their own when the person does their trigger (default true). False: only `startOnboarding()`. */
987
1040
  onboarding?: boolean;
988
1041
  /**
@@ -1137,6 +1190,24 @@ interface MRRocketClient {
1137
1190
  /** The name of the screen a React Navigation state shows: the focused route, followed into nested navigators. */
1138
1191
  declare const activeRouteName: (state: unknown) => string | undefined;
1139
1192
 
1193
+ type OnboardingReportType = "questions" | "checklist" | "tour";
1194
+ interface OnboardingReportOptions {
1195
+ /** Every step's id in the order people meet them, so a step only some people see keeps its place. */
1196
+ order?: string[];
1197
+ /** "questions" (the default), a "checklist" or a "tour". */
1198
+ type?: OnboardingReportType;
1199
+ }
1200
+ interface OnboardingReport {
1201
+ /** The person sees this step; `title` is its name in MRRocket. Sent once per step and page. */
1202
+ saw(step: string, title: string): void;
1203
+ /** The person answered a question with one of its options: its id, and its words in MRRocket. */
1204
+ answered(question: string, answer: string, label?: string): void;
1205
+ /** The person did this task or tour step. */
1206
+ done(step: string): void;
1207
+ /** The person closed it or skipped the rest, at this step if you name one. */
1208
+ closed(step?: string): void;
1209
+ }
1210
+
1140
1211
  interface UseOnboarding {
1141
1212
  /** The item as MRRocket published it, or null while the config is loading, or when it is not live. */
1142
1213
  item: OnboardingFlow | null;
@@ -1251,6 +1322,11 @@ declare const MRRocketMessenger: ({ client, appearance, options: given }: {
1251
1322
  options?: NativeMessengerOptions;
1252
1323
  }) => react.FunctionComponentElement<Record<string, unknown>> | null;
1253
1324
 
1325
+ /** An app with its own language switch says the language here; undefined goes back to the device's. Re-reads the config. */
1326
+ declare const setLocale: (locale?: string) => void;
1327
+ /** The app's choice, else the phone's (React Native), else the browser's. */
1328
+ declare const currentLocale: () => string | undefined;
1329
+
1254
1330
  /**
1255
1331
  * Hides what it wraps in session recordings: its area is painted black on every frame before the frame is stored. Use
1256
1332
  * it for anything private on screen (balances, addresses, messages). Text fields need no wrapper: nothing is recorded
@@ -1375,6 +1451,15 @@ declare const useReview: () => {
1375
1451
  * if (!item || done) return null;
1376
1452
  */
1377
1453
  declare const useOnboarding: (id?: string) => UseOnboarding;
1454
+ /**
1455
+ * 0.22.0: for an onboarding that lives in your own code. Nothing to set up in MRRocket: the first report makes the item
1456
+ * under Onboarding, and MRRocket shows who saw each step and what they answered. It follows the person's consent.
1457
+ *
1458
+ * const report = useOnboardingReport("Welcome questions", { order: ["goal", "role"] });
1459
+ * report.saw("goal", "What brings you here?");
1460
+ * report.answered("goal", "dream", "Dream game");
1461
+ */
1462
+ declare const useOnboardingReport: (name: string, options?: OnboardingReportOptions) => OnboardingReport;
1378
1463
  /** A feature flag from MRRocket › Build › Feature flags: true while it is on for this person; false while off, unknown or loading. */
1379
1464
  declare const useFlag: (key: string) => boolean;
1380
1465
  /** The variant of a multivariate flag ("control", "test" …), or null while it is off. */
@@ -1409,4 +1494,4 @@ declare const useExpoRouterTracking: () => void;
1409
1494
  */
1410
1495
  declare const useNavigationTracking: () => (state: unknown) => void;
1411
1496
 
1412
- export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FinishAnimation, type FlagConfig, type FlowIcon, type FlowMedia, type FlowView, type IdentifyOptions, type InAppMessage, type KeyValueStorage, type MRRocketClient, MRRocketFeedback, MRRocketFlows, MRRocketMask, MRRocketMessenger, type MRRocketOptions, MRRocketProvider, type MRRocketProviderProps, MRRocketSurveys, type NativeAppearance, type NativeConsent, type NativeFeedbackOptions, type NativeMessengerOptions, type NativePushResult, type NativePushStatus, type NativeReviewResult, type OnboardingFlow, type OnboardingItemState, type OnboardingQuestion, type OnboardingStep, type PaywallConfig, type PaywallOptions, type PaywallPage, type PaywallPlan, type PaywallResult, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type TaskOpens, type TourPlacement, type UseOnboarding, type VariantId, activeRouteName, createMRRocket, getMRRocket, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, usePaywall, usePush, useReview, useText, useVariant };
1497
+ export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FinishAnimation, type FlagConfig, type FlowIcon, type FlowMedia, type FlowView, type IdentifyOptions, type InAppMessage, type KeyValueStorage, type MRRocketClient, MRRocketFeedback, MRRocketFlows, MRRocketMask, MRRocketMessenger, type MRRocketOptions, MRRocketProvider, type MRRocketProviderProps, MRRocketSurveys, type NativeAppearance, type NativeConsent, type NativeFeedbackOptions, type NativeMessengerOptions, type NativePushResult, type NativePushStatus, type NativeReviewResult, type OnboardingFlow, type OnboardingItemState, type OnboardingQuestion, type OnboardingReport, type OnboardingReportOptions, type OnboardingReportType, type OnboardingStep, type PaywallConfig, type PaywallOptions, type PaywallPage, type PaywallPlan, type PaywallResult, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type TaskOpens, type TourPlacement, type UseOnboarding, type VariantId, activeRouteName, createMRRocket, currentLocale, getMRRocket, setLocale, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, useOnboardingReport, usePaywall, usePush, useReview, useText, useVariant };