@mrrocket/sdk 0.4.0 → 0.9.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 (72) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +137 -8
  3. package/dist/api-client.d.ts +213 -0
  4. package/dist/api-client.js +67 -0
  5. package/dist/api-client.js.map +1 -0
  6. package/dist/api.d.ts +9 -109
  7. package/dist/api.js +7 -62
  8. package/dist/api.js.map +1 -1
  9. package/dist/browser/mrrocket-flows.js +64 -0
  10. package/dist/browser/mrrocket-messenger.js +313 -0
  11. package/dist/browser/mrrocket-push.js +1 -0
  12. package/dist/browser/mrrocket-replay.js +3 -3
  13. package/dist/browser/mrrocket-sw.js +1 -0
  14. package/dist/browser/mrrocket.js +2 -1
  15. package/dist/browser.d.ts +3 -2
  16. package/dist/browser.js +6 -1
  17. package/dist/browser.js.map +1 -1
  18. package/dist/capture.d.ts +1 -13
  19. package/dist/capture.js +1 -22
  20. package/dist/capture.js.map +1 -1
  21. package/dist/client.d.ts +36 -1
  22. package/dist/client.js +161 -6
  23. package/dist/client.js.map +1 -1
  24. package/dist/context.d.ts +10 -0
  25. package/dist/context.js.map +1 -1
  26. package/dist/core.d.ts +64 -3
  27. package/dist/core.js +54 -2
  28. package/dist/core.js.map +1 -1
  29. package/dist/feedback.d.ts +19 -4
  30. package/dist/feedback.js +655 -190
  31. package/dist/feedback.js.map +1 -1
  32. package/dist/flows-ui.d.ts +23 -0
  33. package/dist/flows-ui.js +384 -0
  34. package/dist/flows-ui.js.map +1 -0
  35. package/dist/flows-web.d.ts +19 -0
  36. package/dist/flows-web.js +42 -0
  37. package/dist/flows-web.js.map +1 -0
  38. package/dist/flows.d.ts +222 -0
  39. package/dist/flows.js +409 -0
  40. package/dist/flows.js.map +1 -0
  41. package/dist/index.d.ts +5 -2
  42. package/dist/index.js +13 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/messenger-ui.d.ts +47 -0
  45. package/dist/messenger-ui.js +583 -0
  46. package/dist/messenger-ui.js.map +1 -0
  47. package/dist/messenger.d.ts +37 -0
  48. package/dist/messenger.js +69 -0
  49. package/dist/messenger.js.map +1 -0
  50. package/dist/proxy.d.ts +34 -1
  51. package/dist/proxy.js +14 -1
  52. package/dist/proxy.js.map +1 -1
  53. package/dist/push-web.d.ts +43 -0
  54. package/dist/push-web.js +81 -0
  55. package/dist/push-web.js.map +1 -0
  56. package/dist/rage.d.ts +14 -0
  57. package/dist/rage.js +27 -0
  58. package/dist/rage.js.map +1 -0
  59. package/dist/react-native/index.d.cts +640 -3
  60. package/dist/react-native/index.d.ts +640 -3
  61. package/dist/react-native/index.js +2015 -34
  62. package/dist/react-native/index.js.map +1 -1
  63. package/dist/styles-css.d.ts +6 -0
  64. package/dist/styles-css.js +327 -0
  65. package/dist/styles-css.js.map +1 -0
  66. package/dist/styles.d.ts +1 -2
  67. package/dist/styles.js +3 -134
  68. package/dist/styles.js.map +1 -1
  69. package/dist/ui.d.ts +3 -0
  70. package/dist/ui.js +12 -0
  71. package/dist/ui.js.map +1 -1
  72. package/package.json +20 -4
package/dist/browser.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  // src/browser.ts
4
- import { CockpitProvider, Experiment, Text, useCockpit, useFlag, useFlagVariant, useVariant } from "./client.js";
4
+ import { CockpitProvider, Experiment, Text, useCancelFlow, useCockpit, useFlag, useFlagVariant, usePush, useVariant } from "./client.js";
5
5
  import { FeedbackBoard, FeedbackWidget, useFeedback } from "./feedback.js";
6
+ import { Messenger, useMessenger } from "./messenger.js";
6
7
  import { Survey } from "./survey.js";
7
8
  import {
8
9
  createApi
@@ -13,14 +14,18 @@ export {
13
14
  Experiment,
14
15
  FeedbackBoard,
15
16
  FeedbackWidget,
17
+ Messenger,
16
18
  Survey,
17
19
  Text,
18
20
  CSS as WIDGET_CSS,
19
21
  createApi,
22
+ useCancelFlow,
20
23
  useCockpit,
21
24
  useFeedback,
22
25
  useFlag,
23
26
  useFlagVariant,
27
+ useMessenger,
28
+ usePush,
24
29
  useStyles,
25
30
  useVariant
26
31
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Everything of the SDK that needs the browser or React state. It is its own file with the \"use client\" directive so\n * that the build keeps one client chunk: server components import these as client references, while the plain\n * helpers (`core`, `server`, `proxy`) stay free of the directive.\n */\nexport { CockpitProvider, Experiment, Text, useCockpit, useFlag, useFlagVariant, useVariant, type CockpitProviderProps } from \"./client\";\nexport { FeedbackBoard, FeedbackWidget, useFeedback, type FeedbackBoardProps, type FeedbackWidgetProps } from \"./feedback\";\nexport { Survey } from \"./survey\";\nexport type { CockpitUser } from \"./context\";\nexport {\n type ChangelogEntry,\n type FeedbackBoardInfo,\n type FeedbackComment,\n type FeedbackList,\n type FeedbackPost,\n type FeedbackRoadmap,\n type PostStatus,\n type SurveyAnswer,\n type SurveyDefinition,\n type SurveyQuestion,\n type SurveyQuestionKind,\n createApi,\n} from \"./api\";\nexport { CSS as WIDGET_CSS, useStyles } from \"./styles\";\n"],"mappings":";;;AAOA,SAAS,iBAAiB,YAAY,MAAM,YAAY,SAAS,gBAAgB,kBAA6C;AAC9H,SAAS,eAAe,gBAAgB,mBAAsE;AAC9G,SAAS,cAAc;AAEvB;AAAA,EAYI;AAAA,OACG;AACP,SAAgB,KAAY,iBAAiB;","names":[]}
1
+ {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Everything of the SDK that needs the browser or React state. It is its own file with the \"use client\" directive so\n * that the build keeps one client chunk: server components import these as client references, while the plain\n * helpers (`core`, `server`, `proxy`) stay free of the directive.\n */\nexport { CockpitProvider, Experiment, Text, useCancelFlow, useCockpit, useFlag, useFlagVariant, usePush, useVariant, type CockpitProviderProps } from \"./client\";\nexport { FeedbackBoard, FeedbackWidget, useFeedback, type FeedbackBoardProps, type FeedbackWidgetProps } from \"./feedback\";\nexport { Messenger, useMessenger, type MessengerProps } from \"./messenger\";\nexport { Survey } from \"./survey\";\nexport type { CockpitUser } from \"./context\";\nexport {\n type ChangelogEntry,\n type ChatMessage,\n type ChatThread,\n type FeedbackBoardInfo,\n type FeedbackComment,\n type FeedbackList,\n type FeedbackPost,\n type FeedbackRoadmap,\n type PostStatus,\n type SurveyAnswer,\n type SurveyDefinition,\n type SurveyQuestion,\n type SurveyQuestionKind,\n createApi,\n} from \"./api\";\nexport { CSS as WIDGET_CSS, useStyles } from \"./styles\";\n"],"mappings":";;;AAOA,SAAS,iBAAiB,YAAY,MAAM,eAAe,YAAY,SAAS,gBAAgB,SAAS,kBAA6C;AACtJ,SAAS,eAAe,gBAAgB,mBAAsE;AAC9G,SAAS,WAAW,oBAAyC;AAC7D,SAAS,cAAc;AAEvB;AAAA,EAcI;AAAA,OACG;AACP,SAAgB,KAAY,iBAAiB;","names":[]}
package/dist/capture.d.ts CHANGED
@@ -33,18 +33,6 @@ export declare const selectorOf: (el: Element) => string;
33
33
  export declare const labelOf: (el: Element) => string;
34
34
  /** The click's description, or null when it did not land on an element. */
35
35
  export declare const clickInfo: (e: MouseEvent) => ClickInfo | null;
36
- /**
37
- * Three or more clicks within one second on the same spot (30 pixels) are a rage click. `push` answers true once per
38
- * burst, on the third click.
39
- */
40
- export declare class RageDetector {
41
- private readonly windowMs;
42
- private readonly radius;
43
- private readonly count;
44
- private clicks;
45
- private fired;
46
- constructor(windowMs?: number, radius?: number, count?: number);
47
- push(x: number, y: number, t: number): boolean;
48
- }
36
+ export { RageDetector } from "./rage.js";
49
37
  /** How far down the page the visitor has seen, 0 to 100. */
50
38
  export declare const seenDepth: () => number;
package/dist/capture.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/capture.ts
2
+ import { RageDetector } from "./rage.js";
2
3
  var INTERACTIVE = 'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="checkbox"],[role="switch"],[onclick],[data-ck-goal]';
3
4
  var MASK_SELECTOR = "[data-ck-mask]";
4
5
  var BLOCK_ATTR_SELECTOR = "[data-ck-block]";
@@ -50,28 +51,6 @@ var clickInfo = (e) => {
50
51
  interactive: !!hit
51
52
  };
52
53
  };
53
- var RageDetector = class {
54
- constructor(windowMs = 1e3, radius = 30, count = 3) {
55
- this.windowMs = windowMs;
56
- this.radius = radius;
57
- this.count = count;
58
- }
59
- windowMs;
60
- radius;
61
- count;
62
- clicks = [];
63
- fired = false;
64
- push(x, y, t) {
65
- const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
66
- if (near.length !== this.clicks.length) this.fired = false;
67
- this.clicks = [...near, { x, y, t }];
68
- if (this.clicks.length >= this.count && !this.fired) {
69
- this.fired = true;
70
- return true;
71
- }
72
- return false;
73
- }
74
- };
75
54
  var seenDepth = () => {
76
55
  const doc = document.documentElement;
77
56
  const h = Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/capture.ts"],"sourcesContent":["/**\n * Clicks and scroll depth for heatmaps, and the rage-click detector session recordings share. Small on purpose: it is\n * part of every page that uses the provider. Runs under the normal analytics consent; it never reads what anyone typed.\n *\n * A click is described by where it landed (x in CSS pixels with the viewport width, y in page pixels with the page\n * height), a short CSS selector and, for links and buttons only, a label of at most 40 characters. Nothing inside\n * `data-ck-mask` or `data-ck-block` gets a label, form fields never do.\n */\n\n/** Elements a click is meant for. A click on anything else is a dead click. */\nexport const INTERACTIVE =\n 'a[href],button,input,select,textarea,label,summary,[role=\"button\"],[role=\"link\"],[role=\"tab\"],[role=\"menuitem\"],[role=\"checkbox\"],[role=\"switch\"],[onclick],[data-ck-goal]';\n/** Text inside is masked in recordings and never becomes a click label. */\nexport const MASK_SELECTOR = \"[data-ck-mask]\";\n/** Left out of recordings entirely (a placeholder of the same size) and never labelled. */\nexport const BLOCK_ATTR_SELECTOR = \"[data-ck-block]\";\n\nexport interface ClickInfo {\n /** Pointer position: x in CSS pixels from the left of the viewport, y in page pixels from the top of the document. */\n x: number;\n y: number;\n /** Viewport width and height, and the page height, in CSS pixels. */\n vw: number;\n vh: number;\n docH: number;\n /** A short CSS path, like `main > section.pricing > button`. Ids and classes only, never text. */\n selector: string;\n /** For links and buttons: what they say (aria-label, title or text), at most 40 characters; else \"\". */\n label: string;\n /** On a link, button or field; false is a dead click. */\n interactive: boolean;\n}\n\n/** Ids and classes that look generated (hashes, long numbers) say nothing across builds. */\nconst generated = /\\d{4,}|^[a-z]{1,3}-[a-z0-9]{5,}$|^_|__|css-|^sc-|^jsx-/i;\n\nconst part = (el: Element) => {\n const tag = el.tagName.toLowerCase();\n if (el.id && !generated.test(el.id) && el.id.length <= 40) return `${tag}#${el.id}`;\n const cls = typeof el.className === \"string\" ? el.className.split(/\\s+/).filter((c) => c && !generated.test(c) && c.length <= 30 && !c.includes(\":\")) : [];\n const base = cls.length ? `${tag}.${cls.slice(0, 2).join(\".\")}` : tag;\n // Siblings of the same kind (two plain buttons side by side) are told apart by their place.\n const same = el.parentElement ? [...el.parentElement.children].filter((c) => c.tagName === el.tagName) : [];\n return same.length > 1 ? `${base}:nth-of-type(${same.indexOf(el) + 1})` : base;\n};\n\n/** A selector for the element, three levels at most; stops early at an id. */\nexport const selectorOf = (el: Element) => {\n const out: string[] = [];\n let cur: Element | null = el;\n while (cur && out.length < 3 && cur.tagName && cur.tagName !== \"BODY\" && cur.tagName !== \"HTML\") {\n const p = part(cur);\n out.unshift(p);\n if (p.includes(\"#\")) break;\n cur = cur.parentElement;\n }\n return out.join(\" > \").slice(0, 160);\n};\n\nconst clean = (s: string | null | undefined) => (s ?? \"\").replace(/\\s+/g, \" \").trim().slice(0, 40);\n\n/** What a link or button says, never a field's value; \"\" inside masked or blocked parts. */\nexport const labelOf = (el: Element) => {\n if (el.closest(`${MASK_SELECTOR},${BLOCK_ATTR_SELECTOR}`)) return \"\";\n const tag = el.tagName.toLowerCase();\n if (tag === \"input\") {\n const type = (el.getAttribute(\"type\") ?? \"text\").toLowerCase();\n return type === \"submit\" || type === \"button\" ? clean(el.getAttribute(\"aria-label\") ?? (el as HTMLInputElement).value) : clean(el.getAttribute(\"aria-label\"));\n }\n if (tag === \"select\" || tag === \"textarea\") return clean(el.getAttribute(\"aria-label\"));\n return clean(el.getAttribute(\"aria-label\") ?? el.getAttribute(\"title\") ?? (el as HTMLElement).innerText ?? el.textContent);\n};\n\n/** The click's description, or null when it did not land on an element. */\nexport const clickInfo = (e: MouseEvent): ClickInfo | null => {\n const target = e.target instanceof Element ? e.target : null;\n if (!target) return null;\n const hit = target.closest(INTERACTIVE);\n const el = hit ?? target;\n const doc = document.documentElement;\n return {\n x: Math.round(e.clientX),\n y: Math.round(e.clientY + window.scrollY),\n vw: window.innerWidth,\n vh: window.innerHeight,\n docH: Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0),\n selector: selectorOf(el),\n label: hit ? labelOf(hit) : \"\",\n interactive: !!hit,\n };\n};\n\n/**\n * Three or more clicks within one second on the same spot (30 pixels) are a rage click. `push` answers true once per\n * burst, on the third click.\n */\nexport class RageDetector {\n private clicks: { x: number; y: number; t: number }[] = [];\n private fired = false;\n constructor(\n private readonly windowMs = 1000,\n private readonly radius = 30,\n private readonly count = 3,\n ) {}\n push(x: number, y: number, t: number) {\n const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);\n if (near.length !== this.clicks.length) this.fired = false;\n this.clicks = [...near, { x, y, t }];\n if (this.clicks.length >= this.count && !this.fired) {\n this.fired = true;\n return true;\n }\n return false;\n }\n}\n\n/** How far down the page the visitor has seen, 0 to 100. */\nexport const seenDepth = () => {\n const doc = document.documentElement;\n const h = Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0);\n if (!h) return 0;\n return Math.min(100, Math.round(((window.scrollY + window.innerHeight) / h) * 100));\n};\n"],"mappings":";AAUO,IAAM,cACT;AAEG,IAAM,gBAAgB;AAEtB,IAAM,sBAAsB;AAmBnC,IAAM,YAAY;AAElB,IAAM,OAAO,CAAC,OAAgB;AAC1B,QAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,MAAI,GAAG,MAAM,CAAC,UAAU,KAAK,GAAG,EAAE,KAAK,GAAG,GAAG,UAAU,GAAI,QAAO,GAAG,GAAG,IAAI,GAAG,EAAE;AACjF,QAAM,MAAM,OAAO,GAAG,cAAc,WAAW,GAAG,UAAU,MAAM,KAAK,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,UAAU,KAAK,CAAC,KAAK,EAAE,UAAU,MAAM,CAAC,EAAE,SAAS,GAAG,CAAC,IAAI,CAAC;AACzJ,QAAM,OAAO,IAAI,SAAS,GAAG,GAAG,IAAI,IAAI,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,KAAK;AAElE,QAAM,OAAO,GAAG,gBAAgB,CAAC,GAAG,GAAG,cAAc,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,IAAI,CAAC;AAC1G,SAAO,KAAK,SAAS,IAAI,GAAG,IAAI,gBAAgB,KAAK,QAAQ,EAAE,IAAI,CAAC,MAAM;AAC9E;AAGO,IAAM,aAAa,CAAC,OAAgB;AACvC,QAAM,MAAgB,CAAC;AACvB,MAAI,MAAsB;AAC1B,SAAO,OAAO,IAAI,SAAS,KAAK,IAAI,WAAW,IAAI,YAAY,UAAU,IAAI,YAAY,QAAQ;AAC7F,UAAM,IAAI,KAAK,GAAG;AAClB,QAAI,QAAQ,CAAC;AACb,QAAI,EAAE,SAAS,GAAG,EAAG;AACrB,UAAM,IAAI;AAAA,EACd;AACA,SAAO,IAAI,KAAK,KAAK,EAAE,MAAM,GAAG,GAAG;AACvC;AAEA,IAAM,QAAQ,CAAC,OAAkC,KAAK,IAAI,QAAQ,QAAQ,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,EAAE;AAG1F,IAAM,UAAU,CAAC,OAAgB;AACpC,MAAI,GAAG,QAAQ,GAAG,aAAa,IAAI,mBAAmB,EAAE,EAAG,QAAO;AAClE,QAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,MAAI,QAAQ,SAAS;AACjB,UAAM,QAAQ,GAAG,aAAa,MAAM,KAAK,QAAQ,YAAY;AAC7D,WAAO,SAAS,YAAY,SAAS,WAAW,MAAM,GAAG,aAAa,YAAY,KAAM,GAAwB,KAAK,IAAI,MAAM,GAAG,aAAa,YAAY,CAAC;AAAA,EAChK;AACA,MAAI,QAAQ,YAAY,QAAQ,WAAY,QAAO,MAAM,GAAG,aAAa,YAAY,CAAC;AACtF,SAAO,MAAM,GAAG,aAAa,YAAY,KAAK,GAAG,aAAa,OAAO,KAAM,GAAmB,aAAa,GAAG,WAAW;AAC7H;AAGO,IAAM,YAAY,CAAC,MAAoC;AAC1D,QAAM,SAAS,EAAE,kBAAkB,UAAU,EAAE,SAAS;AACxD,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,MAAM,OAAO,QAAQ,WAAW;AACtC,QAAM,KAAK,OAAO;AAClB,QAAM,MAAM,SAAS;AACrB,SAAO;AAAA,IACH,GAAG,KAAK,MAAM,EAAE,OAAO;AAAA,IACvB,GAAG,KAAK,MAAM,EAAE,UAAU,OAAO,OAAO;AAAA,IACxC,IAAI,OAAO;AAAA,IACX,IAAI,OAAO;AAAA,IACX,MAAM,KAAK,IAAI,IAAI,cAAc,SAAS,MAAM,gBAAgB,CAAC;AAAA,IACjE,UAAU,WAAW,EAAE;AAAA,IACvB,OAAO,MAAM,QAAQ,GAAG,IAAI;AAAA,IAC5B,aAAa,CAAC,CAAC;AAAA,EACnB;AACJ;AAMO,IAAM,eAAN,MAAmB;AAAA,EAGtB,YACqB,WAAW,KACX,SAAS,IACT,QAAQ,GAC3B;AAHmB;AACA;AACA;AAAA,EAClB;AAAA,EAHkB;AAAA,EACA;AAAA,EACA;AAAA,EALb,SAAgD,CAAC;AAAA,EACjD,QAAQ;AAAA,EAMhB,KAAK,GAAW,GAAW,GAAW;AAClC,UAAM,OAAO,KAAK,OAAO,OAAO,CAAC,MAAM,IAAI,EAAE,KAAK,KAAK,YAAY,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,IAAI,CAAC,KAAK,KAAK,MAAM;AAC9G,QAAI,KAAK,WAAW,KAAK,OAAO,OAAQ,MAAK,QAAQ;AACrD,SAAK,SAAS,CAAC,GAAG,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC;AACnC,QAAI,KAAK,OAAO,UAAU,KAAK,SAAS,CAAC,KAAK,OAAO;AACjD,WAAK,QAAQ;AACb,aAAO;AAAA,IACX;AACA,WAAO;AAAA,EACX;AACJ;AAGO,IAAM,YAAY,MAAM;AAC3B,QAAM,MAAM,SAAS;AACrB,QAAM,IAAI,KAAK,IAAI,IAAI,cAAc,SAAS,MAAM,gBAAgB,CAAC;AACrE,MAAI,CAAC,EAAG,QAAO;AACf,SAAO,KAAK,IAAI,KAAK,KAAK,OAAQ,OAAO,UAAU,OAAO,eAAe,IAAK,GAAG,CAAC;AACtF;","names":[]}
1
+ {"version":3,"sources":["../src/capture.ts"],"sourcesContent":["/**\n * Clicks and scroll depth for heatmaps, and the rage-click detector session recordings share. Small on purpose: it is\n * part of every page that uses the provider. Runs under the normal analytics consent; it never reads what anyone typed.\n *\n * A click is described by where it landed (x in CSS pixels with the viewport width, y in page pixels with the page\n * height), a short CSS selector and, for links and buttons only, a label of at most 40 characters. Nothing inside\n * `data-ck-mask` or `data-ck-block` gets a label, form fields never do.\n */\n\n/** Elements a click is meant for. A click on anything else is a dead click. */\nexport const INTERACTIVE =\n 'a[href],button,input,select,textarea,label,summary,[role=\"button\"],[role=\"link\"],[role=\"tab\"],[role=\"menuitem\"],[role=\"checkbox\"],[role=\"switch\"],[onclick],[data-ck-goal]';\n/** Text inside is masked in recordings and never becomes a click label. */\nexport const MASK_SELECTOR = \"[data-ck-mask]\";\n/** Left out of recordings entirely (a placeholder of the same size) and never labelled. */\nexport const BLOCK_ATTR_SELECTOR = \"[data-ck-block]\";\n\nexport interface ClickInfo {\n /** Pointer position: x in CSS pixels from the left of the viewport, y in page pixels from the top of the document. */\n x: number;\n y: number;\n /** Viewport width and height, and the page height, in CSS pixels. */\n vw: number;\n vh: number;\n docH: number;\n /** A short CSS path, like `main > section.pricing > button`. Ids and classes only, never text. */\n selector: string;\n /** For links and buttons: what they say (aria-label, title or text), at most 40 characters; else \"\". */\n label: string;\n /** On a link, button or field; false is a dead click. */\n interactive: boolean;\n}\n\n/** Ids and classes that look generated (hashes, long numbers) say nothing across builds. */\nconst generated = /\\d{4,}|^[a-z]{1,3}-[a-z0-9]{5,}$|^_|__|css-|^sc-|^jsx-/i;\n\nconst part = (el: Element) => {\n const tag = el.tagName.toLowerCase();\n if (el.id && !generated.test(el.id) && el.id.length <= 40) return `${tag}#${el.id}`;\n const cls = typeof el.className === \"string\" ? el.className.split(/\\s+/).filter((c) => c && !generated.test(c) && c.length <= 30 && !c.includes(\":\")) : [];\n const base = cls.length ? `${tag}.${cls.slice(0, 2).join(\".\")}` : tag;\n // Siblings of the same kind (two plain buttons side by side) are told apart by their place.\n const same = el.parentElement ? [...el.parentElement.children].filter((c) => c.tagName === el.tagName) : [];\n return same.length > 1 ? `${base}:nth-of-type(${same.indexOf(el) + 1})` : base;\n};\n\n/** A selector for the element, three levels at most; stops early at an id. */\nexport const selectorOf = (el: Element) => {\n const out: string[] = [];\n let cur: Element | null = el;\n while (cur && out.length < 3 && cur.tagName && cur.tagName !== \"BODY\" && cur.tagName !== \"HTML\") {\n const p = part(cur);\n out.unshift(p);\n if (p.includes(\"#\")) break;\n cur = cur.parentElement;\n }\n return out.join(\" > \").slice(0, 160);\n};\n\nconst clean = (s: string | null | undefined) => (s ?? \"\").replace(/\\s+/g, \" \").trim().slice(0, 40);\n\n/** What a link or button says, never a field's value; \"\" inside masked or blocked parts. */\nexport const labelOf = (el: Element) => {\n if (el.closest(`${MASK_SELECTOR},${BLOCK_ATTR_SELECTOR}`)) return \"\";\n const tag = el.tagName.toLowerCase();\n if (tag === \"input\") {\n const type = (el.getAttribute(\"type\") ?? \"text\").toLowerCase();\n return type === \"submit\" || type === \"button\" ? clean(el.getAttribute(\"aria-label\") ?? (el as HTMLInputElement).value) : clean(el.getAttribute(\"aria-label\"));\n }\n if (tag === \"select\" || tag === \"textarea\") return clean(el.getAttribute(\"aria-label\"));\n return clean(el.getAttribute(\"aria-label\") ?? el.getAttribute(\"title\") ?? (el as HTMLElement).innerText ?? el.textContent);\n};\n\n/** The click's description, or null when it did not land on an element. */\nexport const clickInfo = (e: MouseEvent): ClickInfo | null => {\n const target = e.target instanceof Element ? e.target : null;\n if (!target) return null;\n const hit = target.closest(INTERACTIVE);\n const el = hit ?? target;\n const doc = document.documentElement;\n return {\n x: Math.round(e.clientX),\n y: Math.round(e.clientY + window.scrollY),\n vw: window.innerWidth,\n vh: window.innerHeight,\n docH: Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0),\n selector: selectorOf(el),\n label: hit ? labelOf(hit) : \"\",\n interactive: !!hit,\n };\n};\n\nexport { RageDetector } from \"./rage\";\n\n/** How far down the page the visitor has seen, 0 to 100. */\nexport const seenDepth = () => {\n const doc = document.documentElement;\n const h = Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0);\n if (!h) return 0;\n return Math.min(100, Math.round(((window.scrollY + window.innerHeight) / h) * 100));\n};\n"],"mappings":"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","names":[]}
package/dist/client.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { type AttributionMode } from "./referral.js";
3
3
  import { type CockpitConfig, type ConsentChoice, type ConsentMode, type VariantId } from "./core.js";
4
+ import type { PushResult, WebPushOptions } from "./push-web.js";
5
+ import { type CancelFlowOptions, type CancelFlowResult } from "./flows.js";
6
+ import type { FlowsTheme } from "./flows-ui.js";
4
7
  export interface CockpitProviderProps {
5
8
  /** The product's public key from Cockpit › Settings › SDK. Safe in the browser. Default: `NEXT_PUBLIC_MRROCKET_KEY`. */
6
9
  apiKey?: string;
@@ -48,6 +51,13 @@ export interface CockpitProviderProps {
48
51
  attribution?: AttributionMode;
49
52
  /** The query parameter of direct partner links, default `ref` (`https://your.site/?ref=maya`). */
50
53
  referralParam?: string;
54
+ /**
55
+ * Onboarding flows start on their own when the visitor does their trigger (default true). False: only
56
+ * `useCockpit().startOnboarding()` starts one. In-app messages and `openCancelFlow` work either way.
57
+ */
58
+ onboarding?: boolean;
59
+ /** The look of the cancel flow, onboarding and message dialogs: `{ theme: "dark", accent: "#0f766e", className }`. */
60
+ appearance?: FlowsTheme;
51
61
  children: ReactNode;
52
62
  }
53
63
  /**
@@ -57,7 +67,7 @@ export interface CockpitProviderProps {
57
67
  * `preview` forces variants for this render, for example from the `ck_preview` search param on the server,
58
68
  * so a Cockpit preview never flickers. Without it the provider reads the param itself after the first render.
59
69
  */
60
- export declare const CockpitProvider: ({ apiKey: apiKeyProp, host: hostProp, preview: forced, embedded: embeddedProp, config: bootstrapConfig, distinctId: bootstrapId, consent: consentProp, respectDnt, replay: replayProp, heatmaps, surveys, errors, release, attribution, referralParam, children, }: CockpitProviderProps) => import("react").JSX.Element;
70
+ export declare const CockpitProvider: ({ apiKey: apiKeyProp, host: hostProp, preview: forced, embedded: embeddedProp, config: bootstrapConfig, distinctId: bootstrapId, consent: consentProp, respectDnt, replay: replayProp, heatmaps, surveys, errors, release, attribution, referralParam, onboarding, appearance, children, }: CockpitProviderProps) => import("react").JSX.Element;
61
71
  /**
62
72
  * `track`, `identify`, `captureError`, the consent mode, the replay consent and their setter, the visitor id and whether
63
73
  * the experiments and flags have loaded. `setConsent("cookies")` sets the mode, `setConsent({ replay: true })` the
@@ -73,6 +83,21 @@ export declare const useCockpit: () => {
73
83
  consent: ConsentMode;
74
84
  replay: boolean;
75
85
  setConsent: (choice: ConsentChoice) => void;
86
+ openCancelFlow: (options?: CancelFlowOptions) => Promise<CancelFlowResult>;
87
+ startOnboarding: (id?: string) => boolean;
88
+ };
89
+ /**
90
+ * Push notifications from your Lifecycle journeys for the signed-in person (`identify` first). Call `subscribe()` from
91
+ * a click: it asks the browser's permission and signs this browser up (your site hosts `/mrrocket-sw.js`). Call
92
+ * `unsubscribe()` on sign-out. Resolves with `{ status: "subscribed" | "denied" | "not-asked" | "unsupported" |
93
+ * "not-signed-in" | "error" }`.
94
+ *
95
+ * const push = usePush();
96
+ * <button onClick={() => push.subscribe()}>Turn on notifications</button>
97
+ */
98
+ export declare const usePush: () => {
99
+ subscribe: (options?: WebPushOptions) => Promise<PushResult>;
100
+ unsubscribe: () => Promise<boolean>;
76
101
  };
77
102
  /**
78
103
  * A feature flag from MRRocket › Build › Feature flags: true while it is on for this visitor (its audience and rollout
@@ -101,3 +126,13 @@ export declare const Text: ({ id, children }: {
101
126
  id: string;
102
127
  children: string;
103
128
  }) => import("react").JSX.Element;
129
+ /**
130
+ * The cancel flow you built in MRRocket › Churn › Cancel flow. Call `open` from your Cancel button: it shows the flow
131
+ * and resolves with what the customer did. MRRocket never changes the subscription; your `onCancel` and `onSave` do.
132
+ *
133
+ * const cancelFlow = useCancelFlow();
134
+ * <button onClick={() => cancelFlow.open({ customer: "business", onCancel: cancelSubscription, onSave: applyOffer })}>
135
+ */
136
+ export declare const useCancelFlow: () => {
137
+ open: (options?: CancelFlowOptions) => Promise<CancelFlowResult>;
138
+ };
package/dist/client.js CHANGED
@@ -18,19 +18,25 @@ import {
18
18
  } from "./referral.js";
19
19
  import {
20
20
  EMBED_PARAM,
21
+ EXPOSURE_PARAM,
21
22
  ID_COOKIE,
22
23
  PREVIEW_PARAM,
23
24
  SDK_VERSION,
24
25
  SESSION_COOKIE,
25
26
  envKey,
27
+ experimentsOfText,
28
+ exposureFrom,
26
29
  flagFor,
27
30
  parsePreview,
31
+ redirectFor,
28
32
  resolveHost,
29
33
  resolveVariant,
30
34
  textChanges
31
35
  } from "./core.js";
32
36
  import { bodies } from "./batch.js";
33
37
  import { YEAR_S, deleteCookie, enterPage, page, privacySignal, touchSession, visitorId, writeCookie } from "./web-state.js";
38
+ import { createFlows } from "./flows.js";
39
+ import { forgetFlowsStorage, webFlowsEnv } from "./flows-web.js";
34
40
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
35
41
  var warned = /* @__PURE__ */ new Set();
36
42
  var warnOnce = (message) => {
@@ -54,6 +60,8 @@ var CockpitProvider = ({
54
60
  release,
55
61
  attribution = "consent",
56
62
  referralParam = "ref",
63
+ onboarding = true,
64
+ appearance,
57
65
  children
58
66
  }) => {
59
67
  const pathname = usePathname();
@@ -115,6 +123,7 @@ var CockpitProvider = ({
115
123
  deleteCookie(ID_COOKIE);
116
124
  deleteCookie(SESSION_COOKIE);
117
125
  forgetStoredDismissals();
126
+ forgetFlowsStorage();
118
127
  }
119
128
  if (effectiveConsent === "off") {
120
129
  queue.current.length = 0;
@@ -196,12 +205,24 @@ var CockpitProvider = ({
196
205
  }, []);
197
206
  const replayer = useRef(null);
198
207
  const errorer = useRef(null);
208
+ const flowsRef = useRef(null);
209
+ const flows = flowsRef.current ??= createFlows(
210
+ webFlowsEnv({
211
+ host,
212
+ apiKey,
213
+ track: (event, props) => record(event, props),
214
+ personId: () => live.current.tracking && live.current.identified ? live.current.distinctId : void 0,
215
+ canStore: () => live.current.persist,
216
+ auto: onboarding
217
+ })
218
+ );
199
219
  const track = useCallback(
200
220
  (event, props) => {
201
221
  record(event, props);
202
222
  if (live.current.tracking) replayer.current?.event(event);
223
+ flows.onEvent(event);
203
224
  },
204
- [record]
225
+ [record, flows]
205
226
  );
206
227
  const identify = useCallback(
207
228
  (id, props) => {
@@ -268,6 +289,35 @@ var CockpitProvider = ({
268
289
  },
269
290
  [pathname, track, tracking]
270
291
  );
292
+ const decided = useRef(null);
293
+ const arrived = useRef(void 0);
294
+ useEffect(() => {
295
+ if (!ready || !visitor) return;
296
+ if (decided.current?.path !== pathname) {
297
+ const r2 = redirectFor(config, window.location.href, distinctId, preview);
298
+ decided.current = { path: pathname, r: r2 };
299
+ if (r2?.to) {
300
+ flush(true);
301
+ window.location.replace(r2.to);
302
+ return;
303
+ }
304
+ }
305
+ if (arrived.current === void 0) {
306
+ const params = new URLSearchParams(window.location.search);
307
+ arrived.current = params.get(EXPOSURE_PARAM);
308
+ if (arrived.current !== null) {
309
+ params.delete(EXPOSURE_PARAM);
310
+ const q = params.toString();
311
+ window.history.replaceState(window.history.state, "", `${window.location.pathname}${q ? `?${q}` : ""}${window.location.hash}`);
312
+ }
313
+ }
314
+ if (!tracking) return;
315
+ const r = decided.current.r;
316
+ if (r?.experiment && !r.to) expose(r.experiment, r.variant);
317
+ const x = exposureFrom(config, arrived.current);
318
+ if (x) expose(x.experiment, x.variant);
319
+ arrived.current = null;
320
+ }, [ready, visitor, pathname, config, distinctId, preview, tracking, expose, flush]);
271
321
  useEffect(() => {
272
322
  enterPage();
273
323
  replayer.current?.page(pathname);
@@ -378,10 +428,102 @@ var CockpitProvider = ({
378
428
  if (replayer.current === handle) replayer.current = null;
379
429
  };
380
430
  }, [recording, replayConfig, host, apiKey]);
431
+ useEffect(() => {
432
+ if (!ready || !visitor || embedded) return;
433
+ flows.setConfig(config);
434
+ void flows.refresh();
435
+ }, [flows, ready, visitor, embedded, config, identified, tracking, effectiveConsent]);
436
+ const look = useRef(appearance);
437
+ look.current = appearance;
438
+ useEffect(() => {
439
+ if (embedded) return;
440
+ let unmount = null;
441
+ let loading = false;
442
+ let gone = false;
443
+ const show = () => {
444
+ if (unmount || loading || !flows.view()) return;
445
+ loading = true;
446
+ void (typeof window === "undefined" ? Promise.resolve(null) : import("./flows-ui.js")).then((m) => {
447
+ if (!m || gone) return;
448
+ unmount = m.mountFlows(flows, look.current);
449
+ }).catch(() => warnOnce("The cancel flow and onboarding dialogs did not load.")).finally(() => {
450
+ loading = false;
451
+ });
452
+ };
453
+ const off = flows.subscribe(show);
454
+ show();
455
+ return () => {
456
+ gone = true;
457
+ off();
458
+ unmount?.();
459
+ };
460
+ }, [flows, embedded]);
461
+ const openCancelFlow = useCallback((options) => flows.openCancelFlow(options), [flows]);
462
+ const startOnboarding = useCallback((id) => flows.startOnboarding(id), [flows]);
463
+ const subscribeToPush = useCallback(
464
+ (options) => {
465
+ if (typeof window === "undefined") return Promise.resolve({ status: "unsupported" });
466
+ const now = live.current;
467
+ const distinctId2 = now.tracking && now.identified ? now.distinctId : void 0;
468
+ if (!distinctId2) return Promise.resolve({ status: "not-signed-in" });
469
+ const N = window.Notification;
470
+ const permission = Promise.resolve(N && N.permission === "default" && options?.ask !== false ? N.requestPermission() : N?.permission);
471
+ return import("./push-web.js").then((m) => m.subscribeWebPush({ host, apiKey, distinctId: distinctId2, permission }, options));
472
+ },
473
+ [host, apiKey]
474
+ );
475
+ const unsubscribeFromPush = useCallback(
476
+ () => typeof window === "undefined" ? Promise.resolve(false) : import("./push-web.js").then((m) => m.unsubscribeWebPush({ host, apiKey, distinctId: live.current.identified ? live.current.distinctId : void 0 })),
477
+ [host, apiKey]
478
+ );
381
479
  const texts = useMemo(() => textChanges(config, distinctId, preview), [config, distinctId, preview]);
382
480
  const value = useMemo(
383
- () => ({ apiKey, host, user, ready, distinctId, config, preview, embedded, consent, effectiveConsent, replayConsent, setConsent, texts, track, identify, expose, captureError }),
384
- [apiKey, host, user, ready, distinctId, config, preview, embedded, consent, effectiveConsent, replayConsent, setConsent, texts, track, identify, expose, captureError]
481
+ () => ({
482
+ apiKey,
483
+ host,
484
+ user,
485
+ ready,
486
+ distinctId,
487
+ config,
488
+ preview,
489
+ embedded,
490
+ consent,
491
+ effectiveConsent,
492
+ replayConsent,
493
+ setConsent,
494
+ texts,
495
+ track,
496
+ identify,
497
+ expose,
498
+ captureError,
499
+ openCancelFlow,
500
+ startOnboarding,
501
+ subscribeToPush,
502
+ unsubscribeFromPush
503
+ }),
504
+ [
505
+ apiKey,
506
+ host,
507
+ user,
508
+ ready,
509
+ distinctId,
510
+ config,
511
+ preview,
512
+ embedded,
513
+ consent,
514
+ effectiveConsent,
515
+ replayConsent,
516
+ setConsent,
517
+ texts,
518
+ track,
519
+ identify,
520
+ expose,
521
+ captureError,
522
+ openCancelFlow,
523
+ startOnboarding,
524
+ subscribeToPush,
525
+ unsubscribeFromPush
526
+ ]
385
527
  );
386
528
  return /* @__PURE__ */ jsxs(Ctx.Provider, { value, children: [
387
529
  children,
@@ -389,8 +531,12 @@ var CockpitProvider = ({
389
531
  ] });
390
532
  };
391
533
  var useCockpit = () => {
392
- const { track, identify, captureError, distinctId, ready, embedded, consent, replayConsent, setConsent } = useCtx();
393
- return { track, identify, captureError, distinctId, ready, embedded, consent, replay: replayConsent, setConsent };
534
+ const { track, identify, captureError, distinctId, ready, embedded, consent, replayConsent, setConsent, openCancelFlow, startOnboarding } = useCtx();
535
+ return { track, identify, captureError, distinctId, ready, embedded, consent, replay: replayConsent, setConsent, openCancelFlow, startOnboarding };
536
+ };
537
+ var usePush = () => {
538
+ const { subscribeToPush, unsubscribeFromPush } = useCtx();
539
+ return useMemo(() => ({ subscribe: subscribeToPush, unsubscribe: unsubscribeFromPush }), [subscribeToPush, unsubscribeFromPush]);
394
540
  };
395
541
  var useFlag = (key) => useFlagValue(key) !== false;
396
542
  var useFlagVariant = (key) => {
@@ -414,16 +560,25 @@ var Experiment = ({ name, variants }) => {
414
560
  return /* @__PURE__ */ jsx(Fragment, { children: variants[variant] ?? variants.control });
415
561
  };
416
562
  var Text = ({ id, children }) => {
417
- const { texts } = useCtx();
563
+ const { texts, config, distinctId, preview, expose } = useCtx();
564
+ useEffect(() => {
565
+ for (const key of experimentsOfText(config, id)) expose(key, resolveVariant(key, config, distinctId, preview));
566
+ }, [config, id, distinctId, preview, expose]);
418
567
  return /* @__PURE__ */ jsx(Fragment, { children: texts[id] ?? children });
419
568
  };
569
+ var useCancelFlow = () => {
570
+ const { openCancelFlow } = useCtx();
571
+ return useMemo(() => ({ open: openCancelFlow }), [openCancelFlow]);
572
+ };
420
573
  export {
421
574
  CockpitProvider,
422
575
  Experiment,
423
576
  Text,
577
+ useCancelFlow,
424
578
  useCockpit,
425
579
  useFlag,
426
580
  useFlagVariant,
581
+ usePush,
427
582
  useVariant
428
583
  };
429
584
  //# sourceMappingURL=client.js.map