@mrrocket/sdk 0.15.0 → 0.16.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 (64) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +36 -1
  3. package/dist/api-client.d.ts +5 -4
  4. package/dist/api-client.js +12 -8
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/api.d.ts +4 -1
  7. package/dist/api.js +2 -1
  8. package/dist/api.js.map +1 -1
  9. package/dist/batch.d.ts +6 -4
  10. package/dist/batch.js +3 -3
  11. package/dist/batch.js.map +1 -1
  12. package/dist/browser/mrrocket-flows.js +2 -2
  13. package/dist/browser/mrrocket-forms.js +1 -1
  14. package/dist/browser/mrrocket-messenger.js +2 -2
  15. package/dist/browser/mrrocket-push.js +1 -1
  16. package/dist/browser/mrrocket-replay.js +3 -3
  17. package/dist/browser/mrrocket.js +1 -1
  18. package/dist/client.d.ts +10 -2
  19. package/dist/client.js +36 -15
  20. package/dist/client.js.map +1 -1
  21. package/dist/context.d.ts +5 -1
  22. package/dist/context.js.map +1 -1
  23. package/dist/core.d.ts +1 -1
  24. package/dist/core.js +1 -1
  25. package/dist/core.js.map +1 -1
  26. package/dist/errors.d.ts +5 -1
  27. package/dist/errors.js +8 -2
  28. package/dist/errors.js.map +1 -1
  29. package/dist/flows-web.d.ts +2 -0
  30. package/dist/flows-web.js +4 -1
  31. package/dist/flows-web.js.map +1 -1
  32. package/dist/flows.d.ts +10 -0
  33. package/dist/flows.js +2 -0
  34. package/dist/flows.js.map +1 -1
  35. package/dist/forms.d.ts +4 -0
  36. package/dist/forms.js +4 -1
  37. package/dist/forms.js.map +1 -1
  38. package/dist/identity.d.ts +42 -0
  39. package/dist/identity.js +24 -0
  40. package/dist/identity.js.map +1 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/messenger-ui.d.ts +2 -0
  44. package/dist/messenger-ui.js +2 -2
  45. package/dist/messenger-ui.js.map +1 -1
  46. package/dist/messenger.js +3 -2
  47. package/dist/messenger.js.map +1 -1
  48. package/dist/push-web.d.ts +2 -13
  49. package/dist/push-web.js +4 -2
  50. package/dist/push-web.js.map +1 -1
  51. package/dist/react-native/index.d.cts +61 -9
  52. package/dist/react-native/index.d.ts +61 -9
  53. package/dist/react-native/index.js +107 -28
  54. package/dist/react-native/index.js.map +1 -1
  55. package/dist/replay.d.ts +4 -1
  56. package/dist/replay.js +8 -2
  57. package/dist/replay.js.map +1 -1
  58. package/dist/server.d.ts +2 -0
  59. package/dist/server.js +2 -0
  60. package/dist/server.js.map +1 -1
  61. package/dist/user-hash.d.ts +27 -0
  62. package/dist/user-hash.js +24 -0
  63. package/dist/user-hash.js.map +1 -0
  64. package/package.json +5 -1
package/dist/messenger.js CHANGED
@@ -16,8 +16,8 @@ var useMessenger = () => ({
16
16
  });
17
17
  var Messenger = ({ position, title, greeting, askEmail, label, launcher, help, sound, preview, theme, accent, corners, font, header }) => {
18
18
  const ctx = useCtx();
19
- const who = useRef({ distinctId: ctx.distinctId, user: ctx.user });
20
- who.current = { distinctId: ctx.distinctId, user: ctx.user };
19
+ const who = useRef({ distinctId: ctx.distinctId, user: ctx.user, userHash: ctx.userHash });
20
+ who.current = { distinctId: ctx.distinctId, user: ctx.user, userHash: ctx.userHash };
21
21
  const { apiKey, host, embedded } = ctx;
22
22
  useEffect(() => {
23
23
  if (embedded || !apiKey) return;
@@ -30,6 +30,7 @@ var Messenger = ({ position, title, greeting, askEmail, label, launcher, help, s
30
30
  apiKey,
31
31
  distinctId: () => who.current.distinctId,
32
32
  user: () => who.current.user,
33
+ userHash: () => who.current.userHash,
33
34
  launcher,
34
35
  position,
35
36
  title,
@@ -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 });\n who.current = { distinctId: ctx.distinctId, user: ctx.user };\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 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,KAAK,CAAC;AACjE,MAAI,UAAU,EAAE,YAAY,IAAI,YAAY,MAAM,IAAI,KAAK;AAC3D,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;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 /** 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,16 +1,3 @@
1
- /**
2
- * Web push in the browser (SDK 0.7.0): signs the signed-in person up for push notifications from the product's Lifecycle
3
- * journeys. Loaded only when the site calls `push.subscribe()` (a dynamic import in Next.js, `sdk-push.js` for the script
4
- * tag), so pages that never ask carry none of it.
5
- *
6
- * 1. The browser's permission (asked only from a click, and only when the site says so; `ask: false` only re-sends an
7
- * existing subscription).
8
- * 2. The service worker the site hosts (`/mrrocket-sw.js`, a copy of the file MRRocket serves), registered with the
9
- * public key and MRRocket's address in its URL, so it can report opened notifications. A site with its own service
10
- * worker imports ours there and passes `serviceWorker: false`.
11
- * 3. The workspace's VAPID key from `GET /api/sdk/push`, the browser's subscription with it, and the subscription sent to
12
- * `POST /api/sdk/push` for this person. A subscription made with another key is replaced.
13
- */
14
1
  export type PushStatus = "subscribed" | "denied" | "not-asked" | "unsupported" | "not-signed-in" | "error";
15
2
  export interface PushResult {
16
3
  status: PushStatus;
@@ -31,6 +18,8 @@ export interface WebPushContext {
31
18
  apiKey: string;
32
19
  /** The identify() id; push needs a signed-in person. */
33
20
  distinctId: string | undefined;
21
+ /** The identity hash of that id (0.16.0), when the site gave one to identify. */
22
+ userHash?: string;
34
23
  /** The permission, asked synchronously by the caller inside the click (Safari needs that), or the current one. */
35
24
  permission?: Promise<NotificationPermission>;
36
25
  }
package/dist/push-web.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/push-web.ts
2
+ import { warnUnsigned } from "./identity.js";
2
3
  var b64urlBytes = (s) => {
3
4
  const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "="));
4
5
  const out = new Uint8Array(bin.length);
@@ -49,8 +50,9 @@ var subscribeWebPush = async (ctx, options = {}) => {
49
50
  const saved = await fetch(`${ctx.host}/api/sdk/push`, {
50
51
  method: "POST",
51
52
  headers: { "content-type": "application/json" },
52
- body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, platform: "web", subscription: sub.toJSON(), host: ctx.host })
53
+ body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, userHash: ctx.userHash, platform: "web", subscription: sub.toJSON(), host: ctx.host })
53
54
  });
55
+ warnUnsigned(saved.status);
54
56
  return saved.ok ? { status: "subscribed" } : { status: "error", reason: `MRRocket answered ${saved.status}.` };
55
57
  } catch (e) {
56
58
  return { status: "error", reason: e instanceof Error ? e.message : String(e) };
@@ -65,7 +67,7 @@ var unsubscribeWebPush = async (ctx) => {
65
67
  await fetch(`${ctx.host}/api/sdk/push`, {
66
68
  method: "POST",
67
69
  headers: { "content-type": "application/json" },
68
- body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, platform: "web", remove: true, endpoint: sub.endpoint })
70
+ body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, userHash: ctx.userHash, platform: "web", remove: true, endpoint: sub.endpoint })
69
71
  }).catch(() => null);
70
72
  return await sub.unsubscribe();
71
73
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/push-web.ts"],"sourcesContent":["/**\n * Web push in the browser (SDK 0.7.0): signs the signed-in person up for push notifications from the product's Lifecycle\n * journeys. Loaded only when the site calls `push.subscribe()` (a dynamic import in Next.js, `sdk-push.js` for the script\n * tag), so pages that never ask carry none of it.\n *\n * 1. The browser's permission (asked only from a click, and only when the site says so; `ask: false` only re-sends an\n * existing subscription).\n * 2. The service worker the site hosts (`/mrrocket-sw.js`, a copy of the file MRRocket serves), registered with the\n * public key and MRRocket's address in its URL, so it can report opened notifications. A site with its own service\n * worker imports ours there and passes `serviceWorker: false`.\n * 3. The workspace's VAPID key from `GET /api/sdk/push`, the browser's subscription with it, and the subscription sent to\n * `POST /api/sdk/push` for this person. A subscription made with another key is replaced.\n */\nexport type PushStatus = \"subscribed\" | \"denied\" | \"not-asked\" | \"unsupported\" | \"not-signed-in\" | \"error\";\n\nexport interface PushResult {\n status: PushStatus;\n /** What went wrong, for \"error\" and \"unsupported\". */\n reason?: string;\n}\n\nexport interface WebPushOptions {\n /**\n * The service worker's path on your site (default \"/mrrocket-sw.js\"), or false when your site already has one that\n * imports ours (`importScripts(\"/mrrocket-sw.js\")`): the SDK then uses the one that is active.\n */\n serviceWorker?: string | false;\n /** Ask for the permission when it was not given yet (default true). False: only re-send an existing subscription. */\n ask?: boolean;\n}\n\nexport interface WebPushContext {\n host: string;\n apiKey: string;\n /** The identify() id; push needs a signed-in person. */\n distinctId: string | undefined;\n /** The permission, asked synchronously by the caller inside the click (Safari needs that), or the current one. */\n permission?: Promise<NotificationPermission>;\n}\n\nconst b64urlBytes = (s: string) => {\n const bin = atob(s.replace(/-/g, \"+\").replace(/_/g, \"/\").padEnd(Math.ceil(s.length / 4) * 4, \"=\"));\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n};\n\nconst sameKey = (a: ArrayBuffer | null | undefined, b: Uint8Array) => {\n if (!a) return false;\n const x = new Uint8Array(a);\n return x.length === b.length && x.every((v, i) => v === b[i]);\n};\n\n/** True where the browser can receive web push (Safari 16.4+ only for sites added to the Home Screen on iOS). */\nexport const webPushSupported = () =>\n typeof window !== \"undefined\" && \"serviceWorker\" in navigator && \"PushManager\" in window && \"Notification\" in window;\n\n/** The permission, asked right away when `ask` and not given yet. Call it synchronously in the click handler. */\nexport const askPermission = (ask = true): Promise<NotificationPermission> => {\n if (!webPushSupported()) return Promise.resolve(\"denied\");\n if (Notification.permission !== \"default\" || !ask) return Promise.resolve(Notification.permission);\n try {\n return Promise.resolve(Notification.requestPermission());\n } catch {\n return Promise.resolve(\"denied\");\n }\n};\n\nconst registration = async (host: string, apiKey: string, sw: string | false) => {\n if (sw !== false) {\n const url = `${sw}?key=${encodeURIComponent(apiKey)}&host=${encodeURIComponent(host)}`;\n await navigator.serviceWorker.register(url);\n }\n return navigator.serviceWorker.ready;\n};\n\nexport const subscribeWebPush = async (ctx: WebPushContext, options: WebPushOptions = {}): Promise<PushResult> => {\n if (!webPushSupported()) return { status: \"unsupported\", reason: \"This browser has no web push.\" };\n if (!ctx.distinctId) return { status: \"not-signed-in\" };\n const ask = options.ask !== false;\n const permission = await (ctx.permission ?? askPermission(ask));\n if (permission === \"denied\") return { status: \"denied\" };\n if (permission !== \"granted\") return { status: ask ? \"denied\" : \"not-asked\" };\n try {\n const r = await fetch(`${ctx.host}/api/sdk/push?key=${encodeURIComponent(ctx.apiKey)}`);\n const vapid = r.ok ? ((await r.json()) as { web?: { vapidKey?: string } }).web?.vapidKey : undefined;\n if (!vapid) return { status: \"error\", reason: `MRRocket answered ${r.status} for the push key.` };\n const key = b64urlBytes(vapid);\n const reg = await registration(ctx.host, ctx.apiKey, options.serviceWorker ?? \"/mrrocket-sw.js\");\n let sub = await reg.pushManager.getSubscription();\n if (sub && !sameKey(sub.options?.applicationServerKey, key)) {\n await sub.unsubscribe().catch(() => false);\n sub = null;\n }\n sub ??= await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });\n const saved = await fetch(`${ctx.host}/api/sdk/push`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, platform: \"web\", subscription: sub.toJSON(), host: ctx.host }),\n });\n return saved.ok ? { status: \"subscribed\" } : { status: \"error\", reason: `MRRocket answered ${saved.status}.` };\n } catch (e) {\n return { status: \"error\", reason: e instanceof Error ? e.message : String(e) };\n }\n};\n\n/** Ends this browser's subscription and tells MRRocket (sign-out, or the person turned push off). */\nexport const unsubscribeWebPush = async (ctx: WebPushContext): Promise<boolean> => {\n if (!webPushSupported()) return false;\n try {\n const reg = await navigator.serviceWorker.getRegistration();\n const sub = await reg?.pushManager.getSubscription();\n if (!sub) return false;\n await fetch(`${ctx.host}/api/sdk/push`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, platform: \"web\", remove: true, endpoint: sub.endpoint }),\n }).catch(() => null);\n return await sub.unsubscribe();\n } catch {\n return false;\n }\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/push-web.ts"],"sourcesContent":["/**\n * Web push in the browser (SDK 0.7.0): signs the signed-in person up for push notifications from the product's Lifecycle\n * journeys. Loaded only when the site calls `push.subscribe()` (a dynamic import in Next.js, `sdk-push.js` for the script\n * tag), so pages that never ask carry none of it.\n *\n * 1. The browser's permission (asked only from a click, and only when the site says so; `ask: false` only re-sends an\n * existing subscription).\n * 2. The service worker the site hosts (`/mrrocket-sw.js`, a copy of the file MRRocket serves), registered with the\n * public key and MRRocket's address in its URL, so it can report opened notifications. A site with its own service\n * worker imports ours there and passes `serviceWorker: false`.\n * 3. The workspace's VAPID key from `GET /api/sdk/push`, the browser's subscription with it, and the subscription sent to\n * `POST /api/sdk/push` for this person. A subscription made with another key is replaced.\n */\nimport { warnUnsigned } from \"./identity\";\n\nexport type PushStatus = \"subscribed\" | \"denied\" | \"not-asked\" | \"unsupported\" | \"not-signed-in\" | \"error\";\n\nexport interface PushResult {\n status: PushStatus;\n /** What went wrong, for \"error\" and \"unsupported\". */\n reason?: string;\n}\n\nexport interface WebPushOptions {\n /**\n * The service worker's path on your site (default \"/mrrocket-sw.js\"), or false when your site already has one that\n * imports ours (`importScripts(\"/mrrocket-sw.js\")`): the SDK then uses the one that is active.\n */\n serviceWorker?: string | false;\n /** Ask for the permission when it was not given yet (default true). False: only re-send an existing subscription. */\n ask?: boolean;\n}\n\nexport interface WebPushContext {\n host: string;\n apiKey: string;\n /** The identify() id; push needs a signed-in person. */\n distinctId: string | undefined;\n /** The identity hash of that id (0.16.0), when the site gave one to identify. */\n userHash?: string;\n /** The permission, asked synchronously by the caller inside the click (Safari needs that), or the current one. */\n permission?: Promise<NotificationPermission>;\n}\n\nconst b64urlBytes = (s: string) => {\n const bin = atob(s.replace(/-/g, \"+\").replace(/_/g, \"/\").padEnd(Math.ceil(s.length / 4) * 4, \"=\"));\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n};\n\nconst sameKey = (a: ArrayBuffer | null | undefined, b: Uint8Array) => {\n if (!a) return false;\n const x = new Uint8Array(a);\n return x.length === b.length && x.every((v, i) => v === b[i]);\n};\n\n/** True where the browser can receive web push (Safari 16.4+ only for sites added to the Home Screen on iOS). */\nexport const webPushSupported = () =>\n typeof window !== \"undefined\" && \"serviceWorker\" in navigator && \"PushManager\" in window && \"Notification\" in window;\n\n/** The permission, asked right away when `ask` and not given yet. Call it synchronously in the click handler. */\nexport const askPermission = (ask = true): Promise<NotificationPermission> => {\n if (!webPushSupported()) return Promise.resolve(\"denied\");\n if (Notification.permission !== \"default\" || !ask) return Promise.resolve(Notification.permission);\n try {\n return Promise.resolve(Notification.requestPermission());\n } catch {\n return Promise.resolve(\"denied\");\n }\n};\n\nconst registration = async (host: string, apiKey: string, sw: string | false) => {\n if (sw !== false) {\n const url = `${sw}?key=${encodeURIComponent(apiKey)}&host=${encodeURIComponent(host)}`;\n await navigator.serviceWorker.register(url);\n }\n return navigator.serviceWorker.ready;\n};\n\nexport const subscribeWebPush = async (ctx: WebPushContext, options: WebPushOptions = {}): Promise<PushResult> => {\n if (!webPushSupported()) return { status: \"unsupported\", reason: \"This browser has no web push.\" };\n if (!ctx.distinctId) return { status: \"not-signed-in\" };\n const ask = options.ask !== false;\n const permission = await (ctx.permission ?? askPermission(ask));\n if (permission === \"denied\") return { status: \"denied\" };\n if (permission !== \"granted\") return { status: ask ? \"denied\" : \"not-asked\" };\n try {\n const r = await fetch(`${ctx.host}/api/sdk/push?key=${encodeURIComponent(ctx.apiKey)}`);\n const vapid = r.ok ? ((await r.json()) as { web?: { vapidKey?: string } }).web?.vapidKey : undefined;\n if (!vapid) return { status: \"error\", reason: `MRRocket answered ${r.status} for the push key.` };\n const key = b64urlBytes(vapid);\n const reg = await registration(ctx.host, ctx.apiKey, options.serviceWorker ?? \"/mrrocket-sw.js\");\n let sub = await reg.pushManager.getSubscription();\n if (sub && !sameKey(sub.options?.applicationServerKey, key)) {\n await sub.unsubscribe().catch(() => false);\n sub = null;\n }\n sub ??= await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });\n const saved = await fetch(`${ctx.host}/api/sdk/push`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, userHash: ctx.userHash, platform: \"web\", subscription: sub.toJSON(), host: ctx.host }),\n });\n warnUnsigned(saved.status);\n return saved.ok ? { status: \"subscribed\" } : { status: \"error\", reason: `MRRocket answered ${saved.status}.` };\n } catch (e) {\n return { status: \"error\", reason: e instanceof Error ? e.message : String(e) };\n }\n};\n\n/** Ends this browser's subscription and tells MRRocket (sign-out, or the person turned push off). */\nexport const unsubscribeWebPush = async (ctx: WebPushContext): Promise<boolean> => {\n if (!webPushSupported()) return false;\n try {\n const reg = await navigator.serviceWorker.getRegistration();\n const sub = await reg?.pushManager.getSubscription();\n if (!sub) return false;\n await fetch(`${ctx.host}/api/sdk/push`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ key: ctx.apiKey, distinctId: ctx.distinctId, userHash: ctx.userHash, platform: \"web\", remove: true, endpoint: sub.endpoint }),\n }).catch(() => null);\n return await sub.unsubscribe();\n } catch {\n return false;\n }\n};\n"],"mappings":"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","names":[]}
@@ -1,10 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
- /**
5
- * The SDK's fetch client and the shapes it reads, without React, so the script tag's widgets (src/script) use it too.
6
- * `./api` re-exports all of it with the `useApi` hook.
7
- */
8
4
  type PostStatus = "review" | "planned" | "progress" | "shipped";
9
5
  interface FeedbackBoardInfo {
10
6
  id: string;
@@ -201,6 +197,11 @@ interface Conn {
201
197
  id?: string;
202
198
  [key: string]: unknown;
203
199
  };
200
+ /**
201
+ * The identity hash of `distinctId` (0.16.0, see identity.ts), sent with every request that names the visitor: a
202
+ * JSON field next to `distinctId`, a query parameter, a form field. Leave it out for any other id.
203
+ */
204
+ userHash?: string;
204
205
  }
205
206
  /** The small fetch client behind the widgets. Every read takes an AbortSignal so unmounting cancels it. */
206
207
  declare const createApi: (c: Conn) => {
@@ -620,7 +621,7 @@ interface PaywallHost {
620
621
 
621
622
  type VariantId = string;
622
623
  /** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
623
- declare const SDK_VERSION = "0.15.0";
624
+ declare const SDK_VERSION = "0.16.0";
624
625
  /** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
625
626
  interface VariantConfig {
626
627
  id: VariantId;
@@ -761,6 +762,27 @@ interface AppContext {
761
762
  locale?: string;
762
763
  }
763
764
 
765
+ /**
766
+ * Identity verification (0.16.0), the browser and app side. After `identify(id)` the SDK's id is your app's own user
767
+ * id, and that id is no secret (a Clerk id shows in URLs and logs), so on its own it proves nothing: anyone who knows it
768
+ * could read that person's support chat or post as them. Your server signs the id with the product's server key
769
+ * (`userHash(user.id)` from `@mrrocket/sdk/next/server`) and you hand the result to `identify(id, props, { hash })`.
770
+ * From then on every request for that id carries it as `userHash` (a JSON field, a query parameter or a form field), and
771
+ * MRRocket checks it.
772
+ *
773
+ * The hash belongs to one id: it is forgotten on `reset()` (sign-out) and when `identify` is called for another id
774
+ * without a hash, so it never goes out with someone else's id (MRRocket refuses a hash that does not match with 401).
775
+ * Shared by the Next.js provider, the script tag and React Native: no React, no browser APIs.
776
+ */
777
+ /** The third argument of `identify`. */
778
+ interface IdentifyOptions {
779
+ /**
780
+ * The id signed by your server: `await userHash(user.id)` from `@mrrocket/sdk/next/server` (or the same HMAC in any
781
+ * language). Needed when MRRocket › Settings › Products › Sign-in › "Only signed users" is on.
782
+ */
783
+ hash?: string;
784
+ }
785
+
764
786
  /**
765
787
  * The React Native client without React: the event queue (batched, persisted for offline use, retried with backoff),
766
788
  * the app lifecycle (`$app_open`, `$app_background`, a new session after 30 minutes away), screens, identity and
@@ -822,6 +844,13 @@ interface RegisterForPushOptions {
822
844
  projectId?: string;
823
845
  }
824
846
  type NativePushStatus = "registered" | "denied" | "not-asked" | "unavailable" | "not-signed-in" | "error";
847
+ /**
848
+ * 0.16.0, what `requestReview()` did: "requested" the store's rating dialog was asked for (the store decides whether it
849
+ * shows; Apple shows it at most three times a year); "not-now" no live review request may ask this person now (not a
850
+ * customer, asked in the last 120 days, no request live); "unavailable" the device has no dialog or `expo-store-review`
851
+ * is not installed; "not-signed-in" call `identify` first.
852
+ */
853
+ type NativeReviewResult = "requested" | "not-now" | "unavailable" | "not-signed-in";
825
854
  interface NativePushResult {
826
855
  status: NativePushStatus;
827
856
  /** The Expo push token, once registered. */
@@ -838,8 +867,14 @@ interface MRRocketClient {
838
867
  tap(x: number, y: number): void;
839
868
  /** The person's answer to session recordings (`replay` in the options); false stops and forgets what was not sent. */
840
869
  setReplay(on: boolean): void;
841
- identify(id: string, props?: Record<string, unknown>): void;
842
- /** Forgets the signed-in person (sign-out): a new anonymous id and a new session. */
870
+ /**
871
+ * Who the person is once they sign in. `{ hash }` (0.16.0) is the id signed by your backend (`userHash(id)` from
872
+ * `@mrrocket/sdk/next/server`, or the same HMAC in any language), fetched after sign-in; sent with every request for
873
+ * that id so MRRocket knows it is really them. Needed with "Only signed users" (MRRocket › Settings › Products ›
874
+ * Sign-in). Not stored on the device: call identify with it on every start.
875
+ */
876
+ identify(id: string, props?: Record<string, unknown>, options?: IdentifyOptions): void;
877
+ /** Forgets the signed-in person and their hash (sign-out): a new anonymous id and a new session. */
843
878
  reset(): void;
844
879
  setConsent(consent: NativeConsent): void;
845
880
  captureError(error: unknown): void;
@@ -886,6 +921,12 @@ interface MRRocketClient {
886
921
  registerForPush(options?: RegisterForPushOptions): Promise<NativePushResult>;
887
922
  /** Removes this device's token for the signed-in person (sign-out does it on its own in `reset`). */
888
923
  unregisterForPush(): Promise<boolean>;
924
+ /**
925
+ * 0.16.0: asks for a store review at the moment your app picked (the trigger of a review request in MRRocket ›
926
+ * Reviews › Requests). MRRocket says whether this signed-in person may be asked now; if so the App Store's or Google
927
+ * Play's own rating dialog opens (needs `expo-store-review`) and MRRocket counts the request. Never filtered by mood.
928
+ */
929
+ requestReview(): Promise<NativeReviewResult>;
889
930
  /** The cancel flow, onboarding and messages for a renderer (the provider's, or your own). */
890
931
  flows: FlowsController;
891
932
  /** Sends what is queued now. */
@@ -1075,7 +1116,7 @@ declare const MRRocketProvider: ({ client: given, children, consent, flows, appe
1075
1116
  declare const useMRRocket: () => {
1076
1117
  track: (event: string, props?: Record<string, unknown>) => void;
1077
1118
  screen: (name: string, props?: Record<string, unknown>) => void;
1078
- identify: (id: string, props?: Record<string, unknown>) => void;
1119
+ identify: (id: string, props?: Record<string, unknown>, options?: IdentifyOptions) => void;
1079
1120
  reset: () => void;
1080
1121
  setConsent: (consent: NativeConsent) => void;
1081
1122
  setReplay: (on: boolean) => void;
@@ -1118,6 +1159,17 @@ declare const usePush: () => {
1118
1159
  declare const usePaywall: () => {
1119
1160
  open: MRRocketClient["openPaywall"];
1120
1161
  };
1162
+ /**
1163
+ * 0.16.0: store reviews from a review request in MRRocket › Reviews › Requests (needs `expo-store-review`). Call
1164
+ * `request()` at the moment the request names (after the third finished challenge, a 7-day streak …): MRRocket says
1165
+ * whether this signed-in person may be asked now, and the App Store's or Google Play's own rating dialog opens.
1166
+ *
1167
+ * const review = useReview();
1168
+ * if (challengesDone === 3) void review.request();
1169
+ */
1170
+ declare const useReview: () => {
1171
+ request: MRRocketClient["requestReview"];
1172
+ };
1121
1173
  /** Starts an onboarding flow now (the first live one without an id), whatever its trigger. */
1122
1174
  declare const useOnboarding: () => {
1123
1175
  start: (id?: string) => boolean;
@@ -1156,4 +1208,4 @@ declare const useExpoRouterTracking: () => void;
1156
1208
  */
1157
1209
  declare const useNavigationTracking: () => (state: unknown) => void;
1158
1210
 
1159
- export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FlagConfig, type FlowView, 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 OnboardingFlow, type OnboardingStep, type PaywallConfig, type PaywallOptions, type PaywallPage, type PaywallPlan, type PaywallResult, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type VariantId, activeRouteName, createMRRocket, getMRRocket, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, usePaywall, usePush, useText, useVariant };
1211
+ export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FlagConfig, 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 OnboardingStep, type PaywallConfig, type PaywallOptions, type PaywallPage, type PaywallPlan, type PaywallResult, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type VariantId, activeRouteName, createMRRocket, getMRRocket, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, usePaywall, usePush, useReview, useText, useVariant };
@@ -1,10 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
- /**
5
- * The SDK's fetch client and the shapes it reads, without React, so the script tag's widgets (src/script) use it too.
6
- * `./api` re-exports all of it with the `useApi` hook.
7
- */
8
4
  type PostStatus = "review" | "planned" | "progress" | "shipped";
9
5
  interface FeedbackBoardInfo {
10
6
  id: string;
@@ -201,6 +197,11 @@ interface Conn {
201
197
  id?: string;
202
198
  [key: string]: unknown;
203
199
  };
200
+ /**
201
+ * The identity hash of `distinctId` (0.16.0, see identity.ts), sent with every request that names the visitor: a
202
+ * JSON field next to `distinctId`, a query parameter, a form field. Leave it out for any other id.
203
+ */
204
+ userHash?: string;
204
205
  }
205
206
  /** The small fetch client behind the widgets. Every read takes an AbortSignal so unmounting cancels it. */
206
207
  declare const createApi: (c: Conn) => {
@@ -620,7 +621,7 @@ interface PaywallHost {
620
621
 
621
622
  type VariantId = string;
622
623
  /** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
623
- declare const SDK_VERSION = "0.15.0";
624
+ declare const SDK_VERSION = "0.16.0";
624
625
  /** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
625
626
  interface VariantConfig {
626
627
  id: VariantId;
@@ -761,6 +762,27 @@ interface AppContext {
761
762
  locale?: string;
762
763
  }
763
764
 
765
+ /**
766
+ * Identity verification (0.16.0), the browser and app side. After `identify(id)` the SDK's id is your app's own user
767
+ * id, and that id is no secret (a Clerk id shows in URLs and logs), so on its own it proves nothing: anyone who knows it
768
+ * could read that person's support chat or post as them. Your server signs the id with the product's server key
769
+ * (`userHash(user.id)` from `@mrrocket/sdk/next/server`) and you hand the result to `identify(id, props, { hash })`.
770
+ * From then on every request for that id carries it as `userHash` (a JSON field, a query parameter or a form field), and
771
+ * MRRocket checks it.
772
+ *
773
+ * The hash belongs to one id: it is forgotten on `reset()` (sign-out) and when `identify` is called for another id
774
+ * without a hash, so it never goes out with someone else's id (MRRocket refuses a hash that does not match with 401).
775
+ * Shared by the Next.js provider, the script tag and React Native: no React, no browser APIs.
776
+ */
777
+ /** The third argument of `identify`. */
778
+ interface IdentifyOptions {
779
+ /**
780
+ * The id signed by your server: `await userHash(user.id)` from `@mrrocket/sdk/next/server` (or the same HMAC in any
781
+ * language). Needed when MRRocket › Settings › Products › Sign-in › "Only signed users" is on.
782
+ */
783
+ hash?: string;
784
+ }
785
+
764
786
  /**
765
787
  * The React Native client without React: the event queue (batched, persisted for offline use, retried with backoff),
766
788
  * the app lifecycle (`$app_open`, `$app_background`, a new session after 30 minutes away), screens, identity and
@@ -822,6 +844,13 @@ interface RegisterForPushOptions {
822
844
  projectId?: string;
823
845
  }
824
846
  type NativePushStatus = "registered" | "denied" | "not-asked" | "unavailable" | "not-signed-in" | "error";
847
+ /**
848
+ * 0.16.0, what `requestReview()` did: "requested" the store's rating dialog was asked for (the store decides whether it
849
+ * shows; Apple shows it at most three times a year); "not-now" no live review request may ask this person now (not a
850
+ * customer, asked in the last 120 days, no request live); "unavailable" the device has no dialog or `expo-store-review`
851
+ * is not installed; "not-signed-in" call `identify` first.
852
+ */
853
+ type NativeReviewResult = "requested" | "not-now" | "unavailable" | "not-signed-in";
825
854
  interface NativePushResult {
826
855
  status: NativePushStatus;
827
856
  /** The Expo push token, once registered. */
@@ -838,8 +867,14 @@ interface MRRocketClient {
838
867
  tap(x: number, y: number): void;
839
868
  /** The person's answer to session recordings (`replay` in the options); false stops and forgets what was not sent. */
840
869
  setReplay(on: boolean): void;
841
- identify(id: string, props?: Record<string, unknown>): void;
842
- /** Forgets the signed-in person (sign-out): a new anonymous id and a new session. */
870
+ /**
871
+ * Who the person is once they sign in. `{ hash }` (0.16.0) is the id signed by your backend (`userHash(id)` from
872
+ * `@mrrocket/sdk/next/server`, or the same HMAC in any language), fetched after sign-in; sent with every request for
873
+ * that id so MRRocket knows it is really them. Needed with "Only signed users" (MRRocket › Settings › Products ›
874
+ * Sign-in). Not stored on the device: call identify with it on every start.
875
+ */
876
+ identify(id: string, props?: Record<string, unknown>, options?: IdentifyOptions): void;
877
+ /** Forgets the signed-in person and their hash (sign-out): a new anonymous id and a new session. */
843
878
  reset(): void;
844
879
  setConsent(consent: NativeConsent): void;
845
880
  captureError(error: unknown): void;
@@ -886,6 +921,12 @@ interface MRRocketClient {
886
921
  registerForPush(options?: RegisterForPushOptions): Promise<NativePushResult>;
887
922
  /** Removes this device's token for the signed-in person (sign-out does it on its own in `reset`). */
888
923
  unregisterForPush(): Promise<boolean>;
924
+ /**
925
+ * 0.16.0: asks for a store review at the moment your app picked (the trigger of a review request in MRRocket ›
926
+ * Reviews › Requests). MRRocket says whether this signed-in person may be asked now; if so the App Store's or Google
927
+ * Play's own rating dialog opens (needs `expo-store-review`) and MRRocket counts the request. Never filtered by mood.
928
+ */
929
+ requestReview(): Promise<NativeReviewResult>;
889
930
  /** The cancel flow, onboarding and messages for a renderer (the provider's, or your own). */
890
931
  flows: FlowsController;
891
932
  /** Sends what is queued now. */
@@ -1075,7 +1116,7 @@ declare const MRRocketProvider: ({ client: given, children, consent, flows, appe
1075
1116
  declare const useMRRocket: () => {
1076
1117
  track: (event: string, props?: Record<string, unknown>) => void;
1077
1118
  screen: (name: string, props?: Record<string, unknown>) => void;
1078
- identify: (id: string, props?: Record<string, unknown>) => void;
1119
+ identify: (id: string, props?: Record<string, unknown>, options?: IdentifyOptions) => void;
1079
1120
  reset: () => void;
1080
1121
  setConsent: (consent: NativeConsent) => void;
1081
1122
  setReplay: (on: boolean) => void;
@@ -1118,6 +1159,17 @@ declare const usePush: () => {
1118
1159
  declare const usePaywall: () => {
1119
1160
  open: MRRocketClient["openPaywall"];
1120
1161
  };
1162
+ /**
1163
+ * 0.16.0: store reviews from a review request in MRRocket › Reviews › Requests (needs `expo-store-review`). Call
1164
+ * `request()` at the moment the request names (after the third finished challenge, a 7-day streak …): MRRocket says
1165
+ * whether this signed-in person may be asked now, and the App Store's or Google Play's own rating dialog opens.
1166
+ *
1167
+ * const review = useReview();
1168
+ * if (challengesDone === 3) void review.request();
1169
+ */
1170
+ declare const useReview: () => {
1171
+ request: MRRocketClient["requestReview"];
1172
+ };
1121
1173
  /** Starts an onboarding flow now (the first live one without an id), whatever its trigger. */
1122
1174
  declare const useOnboarding: () => {
1123
1175
  start: (id?: string) => boolean;
@@ -1156,4 +1208,4 @@ declare const useExpoRouterTracking: () => void;
1156
1208
  */
1157
1209
  declare const useNavigationTracking: () => (state: unknown) => void;
1158
1210
 
1159
- export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FlagConfig, type FlowView, 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 OnboardingFlow, type OnboardingStep, type PaywallConfig, type PaywallOptions, type PaywallPage, type PaywallPlan, type PaywallResult, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type VariantId, activeRouteName, createMRRocket, getMRRocket, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, usePaywall, usePush, useText, useVariant };
1211
+ export { type AppContext, type CancelFlowConfig, type CancelFlowOptions, type CancelFlowResult, type CancelOffer, type ChatMessage, type ChatThread, type CockpitConfig, type FeedbackPost, type FlagConfig, 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 OnboardingStep, type PaywallConfig, type PaywallOptions, type PaywallPage, type PaywallPlan, type PaywallResult, type RegisterForPushOptions, SDK_VERSION, type SurveyDefinition, type VariantId, activeRouteName, createMRRocket, getMRRocket, useCancelFlow, useExpoRouterTracking, useFeedback, useFlag, useFlagVariant, useMRRocket, useMessenger, useNavigationTracking, useOnboarding, usePaywall, usePush, useReview, useText, useVariant };