@mrrocket/sdk 0.21.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.
@@ -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":[]}
@@ -826,7 +826,7 @@ interface PaywallHost {
826
826
 
827
827
  type VariantId = string;
828
828
  /** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
829
- declare const SDK_VERSION = "0.21.0";
829
+ declare const SDK_VERSION = "0.22.0";
830
830
  /** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
831
831
  interface VariantConfig {
832
832
  id: VariantId;
@@ -1190,6 +1190,24 @@ interface MRRocketClient {
1190
1190
  /** The name of the screen a React Navigation state shows: the focused route, followed into nested navigators. */
1191
1191
  declare const activeRouteName: (state: unknown) => string | undefined;
1192
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
+
1193
1211
  interface UseOnboarding {
1194
1212
  /** The item as MRRocket published it, or null while the config is loading, or when it is not live. */
1195
1213
  item: OnboardingFlow | null;
@@ -1433,6 +1451,15 @@ declare const useReview: () => {
1433
1451
  * if (!item || done) return null;
1434
1452
  */
1435
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;
1436
1463
  /** A feature flag from MRRocket › Build › Feature flags: true while it is on for this person; false while off, unknown or loading. */
1437
1464
  declare const useFlag: (key: string) => boolean;
1438
1465
  /** The variant of a multivariate flag ("control", "test" …), or null while it is off. */
@@ -1467,4 +1494,4 @@ declare const useExpoRouterTracking: () => void;
1467
1494
  */
1468
1495
  declare const useNavigationTracking: () => (state: unknown) => void;
1469
1496
 
1470
- 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, currentLocale, getMRRocket, setLocale, 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 };
@@ -826,7 +826,7 @@ interface PaywallHost {
826
826
 
827
827
  type VariantId = string;
828
828
  /** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
829
- declare const SDK_VERSION = "0.21.0";
829
+ declare const SDK_VERSION = "0.22.0";
830
830
  /** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
831
831
  interface VariantConfig {
832
832
  id: VariantId;
@@ -1190,6 +1190,24 @@ interface MRRocketClient {
1190
1190
  /** The name of the screen a React Navigation state shows: the focused route, followed into nested navigators. */
1191
1191
  declare const activeRouteName: (state: unknown) => string | undefined;
1192
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
+
1193
1211
  interface UseOnboarding {
1194
1212
  /** The item as MRRocket published it, or null while the config is loading, or when it is not live. */
1195
1213
  item: OnboardingFlow | null;
@@ -1433,6 +1451,15 @@ declare const useReview: () => {
1433
1451
  * if (!item || done) return null;
1434
1452
  */
1435
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;
1436
1463
  /** A feature flag from MRRocket › Build › Feature flags: true while it is on for this person; false while off, unknown or loading. */
1437
1464
  declare const useFlag: (key: string) => boolean;
1438
1465
  /** The variant of a multivariate flag ("control", "test" …), or null while it is off. */
@@ -1467,4 +1494,4 @@ declare const useExpoRouterTracking: () => void;
1467
1494
  */
1468
1495
  declare const useNavigationTracking: () => (state: unknown) => void;
1469
1496
 
1470
- 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, currentLocale, getMRRocket, setLocale, 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 };
@@ -43,6 +43,7 @@ __export(native_exports, {
43
43
  useMessenger: () => useMessenger,
44
44
  useNavigationTracking: () => useNavigationTracking,
45
45
  useOnboarding: () => useOnboarding,
46
+ useOnboardingReport: () => useOnboardingReport,
46
47
  usePaywall: () => usePaywall,
47
48
  usePush: () => usePush,
48
49
  useReview: () => useReview,
@@ -113,7 +114,7 @@ var onLocale = (fn) => {
113
114
  };
114
115
 
115
116
  // src/core.ts
116
- var SDK_VERSION = "0.21.0";
117
+ var SDK_VERSION = "0.22.0";
117
118
  var propOf = (person, name) => {
118
119
  const want = name.trim().toLowerCase();
119
120
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -237,212 +238,6 @@ var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypt
237
238
 
238
239
  // src/onboarding-state.ts
239
240
  var import_react = require("react");
240
- var none = [];
241
- var useOnboardingItem = (flows, id, start) => {
242
- const [state, setState] = (0, import_react.useState)(() => flows && id ? flows.onboarding.item(id) : null);
243
- (0, import_react.useEffect)(() => {
244
- if (!flows || !id) {
245
- setState(null);
246
- return;
247
- }
248
- const up = () => {
249
- const s2 = flows.onboarding.item(id);
250
- setState(s2);
251
- if (s2 && !s2.done && !s2.dismissed) flows.onboarding.seen(id);
252
- };
253
- up();
254
- return flows.subscribe(up);
255
- }, [flows, id]);
256
- return (0, import_react.useMemo)(
257
- () => ({
258
- item: state?.item ?? null,
259
- steps: state?.steps ?? none,
260
- done: !!state?.done,
261
- dismissed: !!state?.dismissed,
262
- eligible: !!state?.eligible,
263
- complete: (stepId) => {
264
- if (flows && id) flows.onboarding.complete(id, stepId);
265
- },
266
- dismiss: () => {
267
- if (flows && id) flows.onboarding.dismissItem(id);
268
- },
269
- answer: (question, value) => {
270
- if (flows && id) flows.onboarding.answerItem(id, question, value);
271
- },
272
- start
273
- }),
274
- [state, flows, id, start]
275
- );
276
- };
277
-
278
- // src/identity.ts
279
- var UNSIGNED_HELP = "401: sign the user id on your server with userHash(id) from @mrrocket/sdk/next/server and pass identify(id, props, { hash }).";
280
- var warnUnsigned = (status, log = (m) => console.warn(m)) => {
281
- const g2 = globalThis;
282
- if (status !== 401 || g2.__mrrocketUnsigned) return;
283
- g2.__mrrocketUnsigned = true;
284
- log(`[MRRocket] ${UNSIGNED_HELP}`);
285
- };
286
- var nextSigned = (signed, id, hash, log = (m) => console.warn(m)) => {
287
- const h3 = typeof hash === "string" ? hash.trim().toLowerCase() : "";
288
- if (/^[0-9a-f]{64}$/.test(h3)) return { id, hash: h3 };
289
- if (h3) log("[MRRocket] identify: that hash is no userHash(id); not sent.");
290
- return signed && signed.id === id ? signed : null;
291
- };
292
- var hashFor = (signed, id) => signed && id && signed.id === id ? signed.hash : void 0;
293
- var hashForAll = (signed, ids) => signed && ids.some((id) => id === signed.id) && ids.every((id) => !id || id === signed.id) ? signed.hash : void 0;
294
-
295
- // src/api-client.ts
296
- var CHAT_FILE_MAX_BYTES = 10 * 1024 * 1024;
297
- var CHAT_FILES_PER_MESSAGE = 4;
298
- var CORNERS = {
299
- round: { panel: 22, bubble: 18, tail: 6, card: 16, field: 20 },
300
- soft: { panel: 14, bubble: 12, tail: 4, card: 10, field: 10 },
301
- square: { panel: 6, bubble: 6, tail: 2, card: 4, field: 4 }
302
- };
303
- var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-by";
304
- var inkOn = (hex) => {
305
- const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
306
- if (!m) return "#ffffff";
307
- const [r, g2, b] = [m[1], m[2], m[3]].map((x) => {
308
- const c = parseInt(x, 16) / 255;
309
- return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
310
- });
311
- return 0.2126 * r + 0.7152 * g2 + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
312
- };
313
- var ApiError = class extends Error {
314
- constructor(message, status) {
315
- super(message);
316
- __publicField(this, "status");
317
- this.status = status;
318
- }
319
- };
320
- var qs = (params, lang = currentLocale()) => Object.entries({ ...params, lang }).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
321
- var withLang = (url) => {
322
- const q = qs({});
323
- return q ? `${url}?${q}` : url;
324
- };
325
- var call = async (url, init, signal) => {
326
- let res;
327
- try {
328
- res = await fetch(url, {
329
- ...init,
330
- signal,
331
- // A form (a file upload) sets its own multipart type with the boundary.
332
- headers: typeof init?.body === "string" ? { "content-type": "application/json" } : void 0
333
- });
334
- } catch (e) {
335
- if (e.name === "AbortError") throw e;
336
- throw new ApiError(t("Could not reach the server. Check your connection and try again."), 0);
337
- }
338
- if (!res.ok) {
339
- warnUnsigned(res.status);
340
- let detail = "";
341
- try {
342
- detail = (await res.json()).error ?? "";
343
- } catch {
344
- }
345
- throw new ApiError(detail || (res.status === 429 ? t("Too many requests. Try again in a minute.") : t("Something went wrong. Please try again.")), res.status);
346
- }
347
- return await res.json();
348
- };
349
- var createApi = (c) => {
350
- const base = `${c.host}/api/sdk`;
351
- const who2 = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
352
- const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
353
- const post = (path, body, signal) => call(withLang(`${base}${path}`), { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
354
- return {
355
- list: (o, signal) => call(`${base}/feedback?${qs({ ...me, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
356
- post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
357
- roadmap: (signal) => call(`${base}/feedback/roadmap?${qs(me)}`, void 0, signal),
358
- changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
359
- submit: (p) => post("/feedback", p),
360
- vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
361
- comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
362
- comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
363
- survey: (url, signal) => call(`${base}/surveys?${qs({ ...me, url })}`, void 0, signal),
364
- seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
365
- respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
366
- /** One chat by id, else the visitor's newest; `conversations` lists them all. */
367
- chat: (signal, conversation) => call(`${base}/messages?${qs({ ...me, conversation })}`, void 0, signal),
368
- /**
369
- * A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). `conversation` is
370
- * the chat it answers, or "new" to start one; without it, the running chat or else a new one.
371
- */
372
- sendChat: (m) => post("/messages", m),
373
- /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
374
- uploadChatFile: (file, signal) => {
375
- const form = new FormData();
376
- form.append("key", c.apiKey);
377
- form.append("distinctId", c.distinctId);
378
- if (c.userHash) form.append("userHash", c.userHash);
379
- if ("uri" in file) form.append("file", file);
380
- else form.append("file", file, file.name || "file");
381
- return call(withLang(`${base}/messages/attachments`), { method: "POST", body: form }, signal);
382
- },
383
- /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
384
- attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me, "")}`,
385
- articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
386
- article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
387
- messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
388
- rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
389
- };
390
- };
391
-
392
- // src/batch.ts
393
- var MAX_BATCH = 100;
394
- var MAX_BYTES = 6e4;
395
- var byteLength = (s2) => {
396
- let n = 0;
397
- for (let i = 0; i < s2.length; i++) {
398
- const c = s2.charCodeAt(i);
399
- if (c < 128) n += 1;
400
- else if (c < 2048) n += 2;
401
- else if (c >= 55296 && c <= 56319 && i + 1 < s2.length && (s2.charCodeAt(i + 1) & 64512) === 56320) {
402
- n += 4;
403
- i++;
404
- } else n += 3;
405
- }
406
- return n;
407
- };
408
- var batchRequests = (key, events, sdk, userHash) => {
409
- const head = `{"key":${JSON.stringify(key)},${userHash ? `"userHash":${JSON.stringify(userHash)},` : ""}"sdk":${JSON.stringify(sdk)},"events":[`;
410
- const empty = byteLength(head) + 2;
411
- const out = [];
412
- let chunk = [];
413
- let items = [];
414
- let size = empty;
415
- const close = () => {
416
- if (chunk.length) out.push({ body: `${head}${chunk.join(",")}]}`, events: items });
417
- chunk = [];
418
- items = [];
419
- size = empty;
420
- };
421
- for (const event of events) {
422
- const json = JSON.stringify(event);
423
- const n = byteLength(json) + 1;
424
- if (empty + n > MAX_BYTES) continue;
425
- if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();
426
- chunk.push(json);
427
- items.push(event);
428
- size += n;
429
- }
430
- close();
431
- return out;
432
- };
433
-
434
- // src/errors.ts
435
- var MAX_STACK = 8 * 1024;
436
- var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
437
- var describeError = (error) => {
438
- if (error instanceof Error) return { type: error.name || "Error", message: clip(error.message || String(error), 1e3), stack: error.stack ? clip(error.stack, MAX_STACK) : void 0 };
439
- if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
440
- try {
441
- return { type: "Error", message: clip(JSON.stringify(error) ?? String(error), 1e3) };
442
- } catch {
443
- return { type: "Error", message: String(error) };
444
- }
445
- };
446
241
 
447
242
  // src/flows.ts
448
243
  var CANCEL_EVENT = "$cancel_flow";
@@ -1303,6 +1098,237 @@ var createFlows = (env) => {
1303
1098
  return controller;
1304
1099
  };
1305
1100
 
1101
+ // src/onboarding-report.ts
1102
+ var KIND = { questions: "question", checklist: "checklist", tour: "tour" };
1103
+ var createOnboardingReport = (track, name, options = {}) => {
1104
+ const type = options.type ?? "questions";
1105
+ const kind = KIND[type] ?? "question";
1106
+ const seen = /* @__PURE__ */ new Set();
1107
+ const send = (props) => track(ONBOARDING_EVENT, { code: eventSlug(name), name, type, ...options.order?.length ? { order: options.order } : {}, ...props });
1108
+ return {
1109
+ saw: (step, title) => {
1110
+ if (seen.has(step)) return;
1111
+ seen.add(step);
1112
+ send({ step, title, kind, action: "shown" });
1113
+ },
1114
+ answered: (question, answer, label) => send({ step: question, kind: "question", action: "completed", question, answer, label: label ?? answer }),
1115
+ done: (step) => send({ step, kind, action: "completed" }),
1116
+ closed: (step) => send({ ...step ? { step } : {}, kind, action: "dismissed" })
1117
+ };
1118
+ };
1119
+
1120
+ // src/onboarding-state.ts
1121
+ var none = [];
1122
+ var useOnboardingItem = (flows, id, start) => {
1123
+ const [state, setState] = (0, import_react.useState)(() => flows && id ? flows.onboarding.item(id) : null);
1124
+ (0, import_react.useEffect)(() => {
1125
+ if (!flows || !id) {
1126
+ setState(null);
1127
+ return;
1128
+ }
1129
+ const up = () => {
1130
+ const s2 = flows.onboarding.item(id);
1131
+ setState(s2);
1132
+ if (s2 && !s2.done && !s2.dismissed) flows.onboarding.seen(id);
1133
+ };
1134
+ up();
1135
+ return flows.subscribe(up);
1136
+ }, [flows, id]);
1137
+ return (0, import_react.useMemo)(
1138
+ () => ({
1139
+ item: state?.item ?? null,
1140
+ steps: state?.steps ?? none,
1141
+ done: !!state?.done,
1142
+ dismissed: !!state?.dismissed,
1143
+ eligible: !!state?.eligible,
1144
+ complete: (stepId) => {
1145
+ if (flows && id) flows.onboarding.complete(id, stepId);
1146
+ },
1147
+ dismiss: () => {
1148
+ if (flows && id) flows.onboarding.dismissItem(id);
1149
+ },
1150
+ answer: (question, value) => {
1151
+ if (flows && id) flows.onboarding.answerItem(id, question, value);
1152
+ },
1153
+ start
1154
+ }),
1155
+ [state, flows, id, start]
1156
+ );
1157
+ };
1158
+ var useReportWith = (track, name, options) => {
1159
+ const key = JSON.stringify(options ?? null);
1160
+ return (0, import_react.useMemo)(() => createOnboardingReport(track, name, options), [track, name, key]);
1161
+ };
1162
+
1163
+ // src/identity.ts
1164
+ var UNSIGNED_HELP = "401: sign the user id on your server with userHash(id) from @mrrocket/sdk/next/server and pass identify(id, props, { hash }).";
1165
+ var warnUnsigned = (status, log = (m) => console.warn(m)) => {
1166
+ const g2 = globalThis;
1167
+ if (status !== 401 || g2.__mrrocketUnsigned) return;
1168
+ g2.__mrrocketUnsigned = true;
1169
+ log(`[MRRocket] ${UNSIGNED_HELP}`);
1170
+ };
1171
+ var nextSigned = (signed, id, hash, log = (m) => console.warn(m)) => {
1172
+ const h3 = typeof hash === "string" ? hash.trim().toLowerCase() : "";
1173
+ if (/^[0-9a-f]{64}$/.test(h3)) return { id, hash: h3 };
1174
+ if (h3) log("[MRRocket] identify: that hash is no userHash(id); not sent.");
1175
+ return signed && signed.id === id ? signed : null;
1176
+ };
1177
+ var hashFor = (signed, id) => signed && id && signed.id === id ? signed.hash : void 0;
1178
+ var hashForAll = (signed, ids) => signed && ids.some((id) => id === signed.id) && ids.every((id) => !id || id === signed.id) ? signed.hash : void 0;
1179
+
1180
+ // src/api-client.ts
1181
+ var CHAT_FILE_MAX_BYTES = 10 * 1024 * 1024;
1182
+ var CHAT_FILES_PER_MESSAGE = 4;
1183
+ var CORNERS = {
1184
+ round: { panel: 22, bubble: 18, tail: 6, card: 16, field: 20 },
1185
+ soft: { panel: 14, bubble: 12, tail: 4, card: 10, field: 10 },
1186
+ square: { panel: 6, bubble: 6, tail: 2, card: 4, field: 4 }
1187
+ };
1188
+ var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-by";
1189
+ var inkOn = (hex) => {
1190
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
1191
+ if (!m) return "#ffffff";
1192
+ const [r, g2, b] = [m[1], m[2], m[3]].map((x) => {
1193
+ const c = parseInt(x, 16) / 255;
1194
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
1195
+ });
1196
+ return 0.2126 * r + 0.7152 * g2 + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
1197
+ };
1198
+ var ApiError = class extends Error {
1199
+ constructor(message, status) {
1200
+ super(message);
1201
+ __publicField(this, "status");
1202
+ this.status = status;
1203
+ }
1204
+ };
1205
+ var qs = (params, lang = currentLocale()) => Object.entries({ ...params, lang }).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
1206
+ var withLang = (url) => {
1207
+ const q = qs({});
1208
+ return q ? `${url}?${q}` : url;
1209
+ };
1210
+ var call = async (url, init, signal) => {
1211
+ let res;
1212
+ try {
1213
+ res = await fetch(url, {
1214
+ ...init,
1215
+ signal,
1216
+ // A form (a file upload) sets its own multipart type with the boundary.
1217
+ headers: typeof init?.body === "string" ? { "content-type": "application/json" } : void 0
1218
+ });
1219
+ } catch (e) {
1220
+ if (e.name === "AbortError") throw e;
1221
+ throw new ApiError(t("Could not reach the server. Check your connection and try again."), 0);
1222
+ }
1223
+ if (!res.ok) {
1224
+ warnUnsigned(res.status);
1225
+ let detail = "";
1226
+ try {
1227
+ detail = (await res.json()).error ?? "";
1228
+ } catch {
1229
+ }
1230
+ throw new ApiError(detail || (res.status === 429 ? t("Too many requests. Try again in a minute.") : t("Something went wrong. Please try again.")), res.status);
1231
+ }
1232
+ return await res.json();
1233
+ };
1234
+ var createApi = (c) => {
1235
+ const base = `${c.host}/api/sdk`;
1236
+ const who2 = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
1237
+ const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
1238
+ const post = (path, body, signal) => call(withLang(`${base}${path}`), { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
1239
+ return {
1240
+ list: (o, signal) => call(`${base}/feedback?${qs({ ...me, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
1241
+ post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
1242
+ roadmap: (signal) => call(`${base}/feedback/roadmap?${qs(me)}`, void 0, signal),
1243
+ changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
1244
+ submit: (p) => post("/feedback", p),
1245
+ vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
1246
+ comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
1247
+ comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
1248
+ survey: (url, signal) => call(`${base}/surveys?${qs({ ...me, url })}`, void 0, signal),
1249
+ seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
1250
+ respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
1251
+ /** One chat by id, else the visitor's newest; `conversations` lists them all. */
1252
+ chat: (signal, conversation) => call(`${base}/messages?${qs({ ...me, conversation })}`, void 0, signal),
1253
+ /**
1254
+ * A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). `conversation` is
1255
+ * the chat it answers, or "new" to start one; without it, the running chat or else a new one.
1256
+ */
1257
+ sendChat: (m) => post("/messages", m),
1258
+ /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
1259
+ uploadChatFile: (file, signal) => {
1260
+ const form = new FormData();
1261
+ form.append("key", c.apiKey);
1262
+ form.append("distinctId", c.distinctId);
1263
+ if (c.userHash) form.append("userHash", c.userHash);
1264
+ if ("uri" in file) form.append("file", file);
1265
+ else form.append("file", file, file.name || "file");
1266
+ return call(withLang(`${base}/messages/attachments`), { method: "POST", body: form }, signal);
1267
+ },
1268
+ /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
1269
+ attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me, "")}`,
1270
+ articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
1271
+ article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
1272
+ messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
1273
+ rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
1274
+ };
1275
+ };
1276
+
1277
+ // src/batch.ts
1278
+ var MAX_BATCH = 100;
1279
+ var MAX_BYTES = 6e4;
1280
+ var byteLength = (s2) => {
1281
+ let n = 0;
1282
+ for (let i = 0; i < s2.length; i++) {
1283
+ const c = s2.charCodeAt(i);
1284
+ if (c < 128) n += 1;
1285
+ else if (c < 2048) n += 2;
1286
+ else if (c >= 55296 && c <= 56319 && i + 1 < s2.length && (s2.charCodeAt(i + 1) & 64512) === 56320) {
1287
+ n += 4;
1288
+ i++;
1289
+ } else n += 3;
1290
+ }
1291
+ return n;
1292
+ };
1293
+ var batchRequests = (key, events, sdk, userHash) => {
1294
+ const head = `{"key":${JSON.stringify(key)},${userHash ? `"userHash":${JSON.stringify(userHash)},` : ""}"sdk":${JSON.stringify(sdk)},"events":[`;
1295
+ const empty = byteLength(head) + 2;
1296
+ const out = [];
1297
+ let chunk = [];
1298
+ let items = [];
1299
+ let size = empty;
1300
+ const close = () => {
1301
+ if (chunk.length) out.push({ body: `${head}${chunk.join(",")}]}`, events: items });
1302
+ chunk = [];
1303
+ items = [];
1304
+ size = empty;
1305
+ };
1306
+ for (const event of events) {
1307
+ const json = JSON.stringify(event);
1308
+ const n = byteLength(json) + 1;
1309
+ if (empty + n > MAX_BYTES) continue;
1310
+ if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();
1311
+ chunk.push(json);
1312
+ items.push(event);
1313
+ size += n;
1314
+ }
1315
+ close();
1316
+ return out;
1317
+ };
1318
+
1319
+ // src/errors.ts
1320
+ var MAX_STACK = 8 * 1024;
1321
+ var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
1322
+ var describeError = (error) => {
1323
+ if (error instanceof Error) return { type: error.name || "Error", message: clip(error.message || String(error), 1e3), stack: error.stack ? clip(error.stack, MAX_STACK) : void 0 };
1324
+ if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
1325
+ try {
1326
+ return { type: "Error", message: clip(JSON.stringify(error) ?? String(error), 1e3) };
1327
+ } catch {
1328
+ return { type: "Error", message: String(error) };
1329
+ }
1330
+ };
1331
+
1306
1332
  // src/paywall.ts
1307
1333
  var PAYWALL_EVENT = "$paywall";
1308
1334
  var paywallPages = (config, w, distinctId) => {
@@ -4544,6 +4570,7 @@ var useOnboarding = (id) => {
4544
4570
  const client = useClient();
4545
4571
  return useOnboardingItem(client.flows, id, client.startOnboarding);
4546
4572
  };
4573
+ var useOnboardingReport = (name, options) => useReportWith(useClient().track, name, options);
4547
4574
  var useFlag = (key) => {
4548
4575
  const client = useClient();
4549
4576
  useVersion(client);