@mrrocket/sdk 0.4.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +137 -8
  3. package/dist/api-client.d.ts +213 -0
  4. package/dist/api-client.js +67 -0
  5. package/dist/api-client.js.map +1 -0
  6. package/dist/api.d.ts +9 -109
  7. package/dist/api.js +7 -62
  8. package/dist/api.js.map +1 -1
  9. package/dist/browser/mrrocket-flows.js +64 -0
  10. package/dist/browser/mrrocket-messenger.js +313 -0
  11. package/dist/browser/mrrocket-push.js +1 -0
  12. package/dist/browser/mrrocket-replay.js +3 -3
  13. package/dist/browser/mrrocket-sw.js +1 -0
  14. package/dist/browser/mrrocket.js +2 -1
  15. package/dist/browser.d.ts +3 -2
  16. package/dist/browser.js +6 -1
  17. package/dist/browser.js.map +1 -1
  18. package/dist/capture.d.ts +1 -13
  19. package/dist/capture.js +1 -22
  20. package/dist/capture.js.map +1 -1
  21. package/dist/client.d.ts +36 -1
  22. package/dist/client.js +161 -6
  23. package/dist/client.js.map +1 -1
  24. package/dist/context.d.ts +10 -0
  25. package/dist/context.js.map +1 -1
  26. package/dist/core.d.ts +64 -3
  27. package/dist/core.js +54 -2
  28. package/dist/core.js.map +1 -1
  29. package/dist/feedback.d.ts +19 -4
  30. package/dist/feedback.js +655 -190
  31. package/dist/feedback.js.map +1 -1
  32. package/dist/flows-ui.d.ts +23 -0
  33. package/dist/flows-ui.js +384 -0
  34. package/dist/flows-ui.js.map +1 -0
  35. package/dist/flows-web.d.ts +19 -0
  36. package/dist/flows-web.js +42 -0
  37. package/dist/flows-web.js.map +1 -0
  38. package/dist/flows.d.ts +222 -0
  39. package/dist/flows.js +409 -0
  40. package/dist/flows.js.map +1 -0
  41. package/dist/index.d.ts +5 -2
  42. package/dist/index.js +13 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/messenger-ui.d.ts +47 -0
  45. package/dist/messenger-ui.js +583 -0
  46. package/dist/messenger-ui.js.map +1 -0
  47. package/dist/messenger.d.ts +37 -0
  48. package/dist/messenger.js +69 -0
  49. package/dist/messenger.js.map +1 -0
  50. package/dist/proxy.d.ts +34 -1
  51. package/dist/proxy.js +14 -1
  52. package/dist/proxy.js.map +1 -1
  53. package/dist/push-web.d.ts +43 -0
  54. package/dist/push-web.js +81 -0
  55. package/dist/push-web.js.map +1 -0
  56. package/dist/rage.d.ts +14 -0
  57. package/dist/rage.js +27 -0
  58. package/dist/rage.js.map +1 -0
  59. package/dist/react-native/index.d.cts +640 -3
  60. package/dist/react-native/index.d.ts +640 -3
  61. package/dist/react-native/index.js +2015 -34
  62. package/dist/react-native/index.js.map +1 -1
  63. package/dist/styles-css.d.ts +6 -0
  64. package/dist/styles-css.js +327 -0
  65. package/dist/styles-css.js.map +1 -0
  66. package/dist/styles.d.ts +1 -2
  67. package/dist/styles.js +3 -134
  68. package/dist/styles.js.map +1 -1
  69. package/dist/ui.d.ts +3 -0
  70. package/dist/ui.js +12 -0
  71. package/dist/ui.js.map +1 -1
  72. package/package.json +20 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  All notable changes to this package. It follows semantic versioning; before 1.0 a minor version may change an API.
4
4
 
5
+ ## 0.9.0
6
+
7
+ - **Messenger, after Featurebase's:** `<Messenger />` (Next.js / React), the script tag (`data-messenger="true"`) and React Native (`<MRRocketProvider messenger={{ launcher: true }}>`, `useMessenger()`) show a chat bubble with three tabs: **Home** (greeting, the latest chat, help search, top articles), **Messages** (the chat with your team; it lands in MRRocket › Support, answered by you or the Customer experience agent) and **Help** (your published help articles from Support › Help center, by collection, searchable, read in the panel with "Did this answer your question?"). An answer that drew on an article links it.
8
+ - **New answers while closed:** a short sound (web, Web Audio; `sound={false}`), a count on the bubble and a preview card above it (`preview={false}`); in apps a banner at the top and a light haptic with the optional `expo-haptics`. While the visitor waits for an answer the messenger checks every 8 seconds for ten minutes, then every 30 seconds; with the panel open every 4 seconds.
9
+ - One messenger for the web: plain DOM in `messenger-ui.ts`, loaded with a dynamic import by `<Messenger />` and as a fifth file `sdk-messenger.js` by the script tag (`data-messenger-title`, `-greeting`, `-label`, `-position`, and "false" for `-help`, `-sound`, `-preview`, `-ask-email`; `mrrocket.messenger.open()` / `close()` / `toggle()`). `open()` takes `"home"`, `"messages"`, `"help"` or `{ article: id }`. New props `launcher`, `help`, `sound`, `preview`.
10
+ - **React Native:** the survey (shown a few seconds after start; `surveys={false}`), the feedback sheet (`useFeedback().open()`: top requests with votes, suggest one) and the messenger, drawn by `MRRocketProvider` like the flows. New on the client: `api()`, `getUser()`, `isSurveyDone()` / `markSurveyDone()` (kept on the device with "persistent" consent). Optional peer `expo-haptics`.
11
+ - API: `GET /api/sdk/articles?key&q`, `GET /api/sdk/articles/:id` (blocks, never HTML), `POST /api/sdk/articles/:id/rate`; `GET` / `POST /api/sdk/messages`. `createApi(...)` gained `articles(q)`, `article(id)`, `rateArticle(id, helpful)`, `chat()`, `sendChat({ body, email })`; chat messages may carry `articles`. The fetch client (`api-client`) and the stylesheet (`styles-css`) no longer import React.
12
+ - **Look from MRRocket:** color, light or dark, side, title, greeting, button text and which parts show come from MRRocket › Support › Messenger (`GET /api/sdk/messenger`), so they change without new code; what the site's code passes still wins. The launcher waits up to 2.5 seconds for them.
13
+ - **Tap heatmaps in apps:** `MRRocketProvider` sends taps as `$click` on the current screen (and `$rageclick` on three quick taps on one spot), so app screens show in Heatmaps like pages; the provider then wraps the app in one `View` with `flex: 1` (`heatmaps={false}` leaves it as it is). New `client.tap(x, y)` and option `heatmaps`.
14
+ - **Session recordings in apps:** with the person's consent (`replay` on `MRRocketProvider`, `setReplay(true)`) and the product's recordings on, the app sends a small JPEG of the screen about once a second while it changes (optional peer `react-native-view-shot`), with screens, taps, rage taps and errors on the timeline; MRRocket plays them in Sessions. No frame while the keyboard is open; views wrapped in `<MRRocketMask>` are painted black on the server before a frame is stored, and a frame whose masks cannot be measured is not taken. Sampled like the web; other sessions keep the last minute in memory and upload it on a rage tap or an error. At most 30 minutes per recording.
15
+ - `RageDetector` moved to its own module (`rage.ts`) so the apps share it.
16
+ - Version reported in event batches: `0.9.0`.
17
+
18
+ ## 0.8.0
19
+
20
+ - **Texts in React Native:** `useText(id, fallback)` and `client.getText(id, fallback)` / `client.peekText(id)` / `client.exposeText(id)` return the copy published in MRRocket for `id` with the person's variant of a running text test on top, the same rules as `<Text id>` on the web (`textChanges`). Paywall copy (`paywall.<placement>.headline`, `.body`, `.cta`) and text A/B tests now work in apps; showing a text records `$exposure` for every test that changes it, once per screen. Works from the cached config on a cold start.
21
+ - Version reported in event batches: `0.8.0`.
22
+
23
+ ## 0.7.0
24
+
25
+ - **Cancel flow:** `useCancelFlow().open({ customer, price, onCancel, onSave, onClose })` (Next.js and React Native), `mrrocket.cancelFlow.open(…)` (script tag) show the flow built in MRRocket › Churn › Cancel flow from the config's new `cancelFlow`: business customers get the survey and the offer for their answer (discount, pause, downgrade, talk to us), anyone else the plain confirmation (§ 312k BGB); without a live flow the plain confirmation, so the Cancel button always works. Resolves with `{ outcome: "cancelled" | "saved" | "kept", reason, offer, comment, run }`. MRRocket never changes a subscription: your `onCancel` / `onSave` do, and only after they succeed is the outcome recorded (`$cancel_flow`); MRRocket shows it under Churn as a cancellation or a save.
26
+ - **Onboarding:** the tour, checklist and modal steps of live onboarding flows (config `onboarding`) start on their own when the signed-in person (or a visitor with `consent="cookies"`) does the trigger (`track("Signed up")`, the first page for "First launch"), once per person; `startOnboarding()` starts one by hand, `onboarding={false}` switches the automatic start off. Tour steps point at `data-ck-target` elements on the web; checklist items tick themselves on their event. Progress comes back from MRRocket for signed-in people (`GET /api/sdk/inapp`) and records as `$onboarding`.
27
+ - **In-app messages:** journey steps of the kind "In-app message" show to the signed-in person as a banner or a dialog (`$inapp` shown, clicked, dismissed); the journey moves them on.
28
+ - **Look:** `appearance={{ theme, accent, className }}`, CSS variables `--ckf-*` on `.ckf-root`; script tag `data-theme`, `data-accent`. The web dialogs load lazily (a dynamic import; `sdk-flows.js` for the script tag).
29
+ - React Native: `MRRocketProvider` draws them (`flows={false}` to opt out), new `useCancelFlow`, `useOnboarding`, `MRRocketFlows`, `client.openCancelFlow`, `client.startOnboarding`, `client.flows`.
30
+ - **Push notifications** for Lifecycle journeys' push steps. React Native and Expo: `usePush().register()` / `client.registerForPush({ ask, projectId })` after `identify` asks the permission, gets the Expo push token (`expo-notifications`, new optional peer dependency with `expo-constants`) and sends it to `POST /api/sdk/push`; `reset()` takes the token back; opens of MRRocket notifications record `$push {action: "opened"}` and open the step's link when it has a scheme. Web: `usePush().subscribe()` (Next.js), `mrrocket.push.subscribe()` (script tag) asks the permission from the click, registers the service worker the site hosts (`/mrrocket-sw.js`, built as `dist/browser/mrrocket-sw.js`; `serviceWorker: false` for sites that import it into their own) and sends the subscription made with the workspace's VAPID key (`GET /api/sdk/push`); `unsubscribe()` on sign-out. The sign-up code loads lazily (a dynamic import; `sdk-push.js` for the script tag).
31
+ - **Redirects:** the config's `redirects` (page redirects) and redirect tests (an experiment with `path`, variants with `url`) send the visitor on before the page shows, keeping the query and hash; a running test adds `ck_x=<test>:<variant>`, and the page it lands on records the exposure and drops the parameter. New `redirectFor`, `exposureFrom`, `EXPOSURE_PARAM`; `experimentRedirect(request, config, { distinctId })` and a re-exported `getCockpitConfig` in `@mrrocket/sdk/next/proxy` redirect in `proxy.ts` before the page renders. The script tag removes the `ck-hide` class from `<html>` once it decided (the optional anti-flicker snippet).
32
+ - Version reported in event batches: `0.7.0`.
33
+
34
+ ## 0.6.0
35
+
36
+ - **Feedback board after Featurebase:** `<FeedbackBoard>` gets a top bar with your product's logo and name (from MRRocket), Feedback · Roadmap · Changelog, search, a light/dark toggle and "Submit feedback"; post cards with type chip, excerpt, author and votes; the Roadmap has four columns, Backlog (under consideration), Next up, In progress and Done; a post page with comments, upvote and status; Changelog with dates beside the entries; the submit form and dialogs rise as a sheet on phones, the roadmap scrolls column by column. New props `header` (default true; `false` keeps the old plain heading), `syncUrl` (`?view=` and `?post=` in the address, so a post can be linked), `fullPage` and `initialTab`.
37
+ - **Who posts:** a visitor gives a name and email once before posting, voting or commenting; the board keeps them for this site in localStorage (`ckw-board-who:<key>`) so their votes stay theirs. A visitor your app called `identify()` for with a name or email is not asked. Others see "First L.". The floating widget asks the same way.
38
+ - API: posts carry `author`; the list carries `product` ({ name, logo }); the roadmap carries a `review` column; `createApi(...).post(id)` reads one post. Older servers still work (the new fields are optional).
39
+ - Fix: primary buttons, the floating button and tabs keep their own colors on pages whose CSS styles every `button` (the widget's reset no longer outranks its own classes).
40
+
41
+ ## 0.5.0
42
+
43
+ - **Published texts:** the config carries `texts`, the copy published in MRRocket for texts the site marks: a landing page's headline, subheadline and button (`<Text id="pricing.headline">`, `data-ck-text="pricing.headline"`; `/` is `home`, `/features/ai` is `features-ai`) and a paywall's (`paywall.<placement>.headline`, `.body`, `.cta`). `textChanges` (and with it `<Text>`, `getTexts` and the script tag) applies them first and a running test's variant on top; without them the page keeps its own words. Older versions ignore the field.
44
+ - **Experiments are served:** MRRocket's config now lists the product's running tests with their variants (`control`, `a`, `b` …) and splits, and a shipped winner for everyone; nothing changes in how a visitor is assigned (a stable hash of test and visitor id).
45
+ - `<Text id>` records the exposure of every running test that changes that text, as the script tag already did, so a test that only swaps texts is counted without `useVariant`. New export `experimentsOfText`.
46
+ - Version reported in event batches: `0.5.0`.
47
+
5
48
  ## 0.4.0
6
49
 
7
50
  - **React Native and Expo:** new entry `@mrrocket/sdk/react-native` (one CommonJS file for Metro, no DOM, no `"use client"`, no rrweb). `MRRocketProvider`, `useMRRocket`, `useFlag`, `useFlagVariant`, `useVariant`, `createMRRocket` / `getMRRocket` outside React; `track`, `identify`, `reset`, `screen`, `setConsent`, `captureError`, `flush`. Screen views through `useExpoRouterTracking()` (Expo Router) or `useNavigationTracking()` (React Navigation `onStateChange`). Lifecycle events `$app_open` (with `cold`) and `$app_background`; a new session after 30 minutes in the background. Events are batched, sent every 10 s and when the app goes to the background, and kept on the device while offline (AsyncStorage when installed, else memory; at most 1,000, oldest dropped; retries with backoff). Consent `"anonymous"` (default: no id stored), `"persistent"` (a random install id stored on the device) or `"off"` (nothing sent, stored data deleted). Every event carries the app and device (`platform`, `appVersion`, `build`, `osVersion`, `deviceModel`, `deviceType`, `locale`) from `expo-application`, `expo-device` and `expo-localization` when installed. Uncaught JavaScript errors (through `ErrorUtils`, the app's own handler still runs) and `captureError` go to Build › Errors with the app version as release. The config for flags and experiments is cached, so a cold start shows the same variant as the last run. Install attribution: UTM and referral parameters of the deep link that opened the app the first time, sent once with `$install`; no IDFA, no MMP.
package/README.md CHANGED
@@ -10,7 +10,7 @@ The MRRocket SDK, one package with three ways in:
10
10
 
11
11
  One provider in your layout (or one script tag) gives MRRocket page views, your events, A/B tests, feature flags and errors that the agents can run and read. MRRocket shows every variant of your real pages, because the SDK can render any variant on request.
12
12
 
13
- Status: 0.4.0, MIT licensed, compiled to ESM JavaScript with type declarations (no `transpilePackages`); the React Native entry is CommonJS for Metro. Works with Next.js 15 and 16, React 18 and 19, on Node and on Cloudflare Workers (OpenNext), and with React Native 0.72+ / Expo. The source lives in the MRRocket repo (`prototype/web/packages/cockpit-next`).
13
+ Status: 0.8.0, MIT licensed, compiled to ESM JavaScript with type declarations (no `transpilePackages`); the React Native entry is CommonJS for Metro. Works with Next.js 15 and 16, React 18 and 19, on Node and on Cloudflare Workers (OpenNext), and with React Native 0.72+ / Expo. The source lives in the MRRocket repo (`prototype/web/packages/cockpit-next`).
14
14
 
15
15
  ## Install
16
16
 
@@ -141,18 +141,18 @@ MRRocket shows each variant of your page by loading it with `?ck_preview=<experi
141
141
 
142
142
  ## 7. Feedback board
143
143
 
144
- A Canny-style board for your own `/feedback` page: requests with votes and comments, a roadmap and a changelog. Visitors vote once; a duplicate hint shows similar requests while they type.
144
+ A Featurebase-style board for your own `/feedback` page: your product's logo and name, requests with votes and comments, a roadmap (Backlog, Next up, In progress, Done), a changelog, search and a light/dark toggle. A duplicate hint shows similar requests while visitors type.
145
145
 
146
146
  ```tsx
147
147
  // app/feedback/page.tsx
148
148
  import { FeedbackBoard } from "@mrrocket/sdk/next";
149
149
 
150
150
  export default function Page() {
151
- return <FeedbackBoard title="Feedback" theme="auto" accent="#3f76ff" />;
151
+ return <FeedbackBoard theme="auto" accent="#3f76ff" syncUrl />;
152
152
  }
153
153
  ```
154
154
 
155
- It must sit inside `CockpitProvider`. Call `identify(user.id, { name, email })` and comments and requests carry the user. MRRocket also hosts the same board at `/b/<productId>`.
155
+ It must sit inside `CockpitProvider`. Visitors give a name and email once before they post, vote or comment (kept in their browser); call `identify(user.id, { name, email })` and signed-in users are not asked. `syncUrl` keeps the tab and the open post in the address (`?view=roadmap`, `?post=…`), `fullPage` fills the window, `header={false}` shows a plain heading instead of the top bar. MRRocket also hosts the same board at `/b/<productId>` (Feedback › Public board).
156
156
 
157
157
  ## 8. Feedback widget
158
158
 
@@ -267,7 +267,7 @@ mrrocket.ready(function (m) {
267
267
  To call it before the file has loaded, put this stub above the script tag; the calls wait in a queue and run once it has:
268
268
 
269
269
  ```html
270
- <script>!function(w,m){w[m]=w[m]||{q:[]};"track identify reset captureError setConsent ready".split(" ").forEach(function(k){w[m][k]=w[m][k]||function(){w[m].q.push([k,arguments])}})}(window,"mrrocket");</script>
270
+ <script>!function(w,m){w[m]=w[m]||{q:[]};"track identify reset captureError setConsent ready openCancelFlow startOnboarding".split(" ").forEach(function(k){w[m][k]=w[m][k]||function(){w[m].q.push([k,arguments])}})}(window,"mrrocket");</script>
271
271
  ```
272
272
 
273
273
  Settings are data attributes on the script tag:
@@ -283,11 +283,13 @@ Settings are data attributes on the script tag:
283
283
  | `data-release` | Your release, for Build › Errors | none |
284
284
  | `data-attribution` / `data-referral-param` | Affiliate referrals: `consent`, `always`, `off`; the direct-link parameter | `consent`, `ref` |
285
285
 
286
- **Experiments on texts:** mark an element `data-ck-text="hero-title"` and MRRocket's variant texts replace its text; the original text is the control. `<Text>` and `<Experiment>` (React components) are Next.js-only.
286
+ **Experiments on texts:** mark an element `data-ck-text="hero-title"` and MRRocket's variant texts replace its text; the original text is the control. `<Text>` and `<Experiment>` (React components) are Next.js-only; React Native reads texts with `useText`.
287
287
 
288
288
  **Session recordings:** when the product records and the visitor's replay consent is given (`data-replay="true"` or `setConsent({ replay: true })`), the recorder loads from `sdk-replay.js` (about 26 KB gzip) next to `sdk.js`; pages that do not record never load it. Same privacy defaults as 12.
289
289
 
290
- `sdk.js` is about 8 KB gzip. MRRocket serves it with open CORS and a short cache (browsers 5 minutes, CDN 1 hour), so a new release reaches every site within the hour.
290
+ **Cancel flow, onboarding, in-app messages (0.7.0):** `mrrocket.cancelFlow.open({ … })` and `mrrocket.onboarding.start()` (see 15); `data-onboarding="false"` keeps flows from starting on their own, `data-theme` and `data-accent` style the dialogs. The dialogs load from `sdk-flows.js` (about 5 KB gzip) only when one shows.
291
+
292
+ `sdk.js` is about 11 KB gzip. MRRocket serves it with open CORS and a short cache (browsers 5 minutes, CDN 1 hour), so a new release reaches every site within the hour.
291
293
 
292
294
  ## 14. React Native and Expo
293
295
 
@@ -331,6 +333,25 @@ const { track, identify, reset, screen, setConsent, captureError, flush, ready }
331
333
  const newCheckout = useFlag("new-checkout"); // false until the config is known
332
334
  const plan = useFlagVariant("plan-picker"); // string | null
333
335
  const variant = useVariant("paywall"); // "control" until known; records the exposure once per screen
336
+ const headline = useText("paywall.onboarding.headline", "Unlock everything"); // your words until MRRocket has some
337
+ ```
338
+
339
+ **Texts (paywall copy, text tests):** `useText(id, fallback)` is the app's `<Text id>`. It returns the copy published in MRRocket for `id` (a paywall's `paywall.<placement>.headline`, `.body` and `.cta`, as Conversion › Paywalls shows them), with this person's variant of a running text test on top, the same rules as the website: control keeps the published words, and your `fallback` shows while nothing is published (or on the very first start before the config arrives; later starts use the cached config). Showing the text records `$exposure` for every test that changes `id`, once per screen, so a test that only swaps copy needs no `useVariant`. Outside React: `client.getText(id, fallback)` (records the exposure), or `client.peekText(id)` with `client.exposeText(id)` when you render it later.
340
+
341
+ ```tsx
342
+ const PLACEMENT = "onboarding";
343
+ function Paywall() {
344
+ const headline = useText(`paywall.${PLACEMENT}.headline`, "Unlock everything");
345
+ const body = useText(`paywall.${PLACEMENT}.body`, "All features, cancel anytime.");
346
+ const cta = useText(`paywall.${PLACEMENT}.cta`, "Start free trial");
347
+ return (
348
+ <View>
349
+ <Text style={styles.title}>{headline}</Text>
350
+ <Text>{body}</Text>
351
+ <Button title={cta} onPress={buy} />
352
+ </View>
353
+ );
354
+ }
334
355
  ```
335
356
 
336
357
  Outside React (a store, a background task): `createMRRocket({ apiKey })` returns the one client the provider would use; `getMRRocket()` returns the running one.
@@ -368,7 +389,115 @@ Consent:
368
389
 
369
390
  The SDK does not remember the choice: pass it on every start from your consent screen's stored answer, or call `setConsent()`.
370
391
 
371
- Not in this version (coming later): session recordings and heatmaps, the feedback and survey UI, store install referrer and SKAdNetwork.
392
+ Not in this version (coming later): session recordings and heatmaps, store install referrer and SKAdNetwork. Surveys, the feedback sheet and the messenger: see Messenger below.
393
+
394
+ ## 15. Cancel flow, onboarding and in-app messages (0.7.0)
395
+
396
+ What you build in MRRocket shows inside your app; the config carries it, and the dialogs load only when one shows.
397
+
398
+ **Cancel flow** (Churn › Cancel flow): call it from your Cancel button. Business customers get the survey ("Why are you leaving?") and the offer you set for their answer; anyone else gets the plain confirmation, so a consumer can always cancel in one step (§ 312k BGB). Without a live flow it shows the plain confirmation too, so the button always works.
399
+
400
+ ```tsx
401
+ import { useCancelFlow } from "@mrrocket/sdk/next"; // React Native: from "@mrrocket/sdk/react-native"
402
+
403
+ const cancelFlow = useCancelFlow();
404
+ <button
405
+ onClick={async () => {
406
+ const result = await cancelFlow.open({
407
+ customer: account.isBusiness ? "business" : "consumer",
408
+ price: { amount: 29, currency: "EUR" }, // optional: shows what the discount costs
409
+ onCancel: (r) => api.cancelSubscription({ reason: r.reason, comment: r.comment }),
410
+ onSave: (r) => api.applyOffer(r.offer), // { kind: "discount", percent, months } | { kind: "pause", months } | { kind: "downgrade", plan } | { kind: "contact" }
411
+ });
412
+ // result.outcome: "cancelled" | "saved" | "kept"
413
+ }}
414
+ >
415
+ Cancel subscription
416
+ </button>;
417
+ ```
418
+
419
+ MRRocket never changes a subscription (its payment key is read-only): `onCancel` and `onSave` are your code. Only after they succeed is the outcome recorded, and MRRocket shows it under Churn as a cancellation or a save with the customer's plan and monthly revenue (matched by your `identify()` id, or the email you passed with it). If one throws, the dialog stays open with "Something went wrong. Please try again." Script tag: `mrrocket.cancelFlow.open({ … })`.
420
+
421
+ **Onboarding** (Conversion › Onboarding): the tour, checklist and modal steps of a live flow. A flow starts on its own when the person does its trigger: `track("Signed up")` (or `signup`, `sign_up`) for "Signed up", `track("first_export")` for "First export", and the first page or screen for "First launch" / "First visit", once per person. Automatic starts need a signed-in person (`identify`) or stored progress (`consent="cookies"` / `"persistent"`), so nobody gets the same tour on every page. `useCockpit().startOnboarding()` (script tag `mrrocket.onboarding.start()`, React Native `useOnboarding().start()`) starts one whenever you want; `onboarding={false}` on the provider switches the automatic start off. A tour step points at the element marked `data-ck-target="…"` (web); a checklist item with an event ticks itself when you track it. Emails of the flow go out from MRRocket.
422
+
423
+ **In-app messages** (Lifecycle › Journeys, an "In-app message" step): a signed-in person's pending messages load after `identify()` and show as a banner or a dialog. Clicking or closing it moves the person on in the journey; after 7 days without an answer they go on anyway.
424
+
425
+ **Look:** `appearance={{ theme: "dark", accent: "#0f766e", className: "my-dialogs" }}` on `CockpitProvider` (React Native: `{ theme, accent }`; script tag: `data-theme`, `data-accent`). On the web the dialogs read CSS variables on `.ckf-root`: `--ckf-accent`, `--ckf-bg`, `--ckf-fg`, `--ckf-muted`, `--ckf-line`, `--ckf-radius`, `--ckf-font`. React Native draws them next to your app through `MRRocketProvider` (`flows={false}` to draw `client.flows` yourself with `<MRRocketFlows flows={client.flows} />`); tour steps show as a card there.
426
+
427
+ **Events** (under your analytics consent): `$cancel_flow` `{ action: "shown" | "answered" | "offer" | "finished", run, version, reason?, offer?, outcome?, comment? }`, `$onboarding` `{ flow, step?, action: "started" | "shown" | "completed" | "dismissed" }`, `$inapp` `{ message, journey, action: "shown" | "clicked" | "dismissed" }`. With consent `"off"` the cancel flow still works, but nothing is recorded, and no onboarding or messages show.
428
+
429
+ ## 16. Push notifications (0.7.0)
430
+
431
+ A Lifecycle journey's Push step goes to the phones and browsers where the signed-in person turned notifications on.
432
+
433
+ ```tsx
434
+ // Expo (needs expo-notifications; expo-constants for the EAS project id). After identify():
435
+ import { usePush } from "@mrrocket/sdk/react-native";
436
+ const push = usePush();
437
+ await push.register(); // { status: "registered" | "denied" | "not-asked" | "unavailable" | "not-signed-in" | "error" }
438
+
439
+ // Next.js, from a click, after identify(). Your site serves /mrrocket-sw.js (a copy of dist/browser/mrrocket-sw.js):
440
+ import { usePush as useWebPush } from "@mrrocket/sdk/next";
441
+ const webPush = useWebPush();
442
+ <button onClick={() => webPush.subscribe()}>Turn on notifications</button>;
443
+
444
+ // Script tag
445
+ mrrocket.push.subscribe();
446
+ ```
447
+
448
+ `register({ ask: false })` / `subscribe({ ask: false })` only send an existing token or subscription again. `reset()` on React Native takes the token back; on the web call `push.unsubscribe()` before `reset()`. A site with its own service worker adds `importScripts("/mrrocket-sw.js")` to it and calls `subscribe({ serviceWorker: false })`. Opening a MRRocket notification records `$push { action: "opened", message }`.
449
+
450
+ ## 17. Redirects and redirect tests (0.7.0)
451
+
452
+ Page redirects (Landing pages › Redirect) and redirect tests ("Whole page" in Experiments) come with the config: the SDK sends the visitor on before the page shows, keeping the query and hash, and the page a test's variant lands on records the exposure (`ck_x=<test>:<variant>`, removed from the address). Nothing to add for the script tag (the optional `ck-hide` snippet stops a flash of the old page). In Next.js `<CockpitProvider>` redirects after the first paint; to answer before rendering, use the proxy helper:
453
+
454
+ ```ts
455
+ // proxy.ts
456
+ import { NextResponse, type NextRequest } from "next/server";
457
+ import { experimentRedirect, getCockpitConfig } from "@mrrocket/sdk/next/proxy";
458
+
459
+ export async function proxy(request: NextRequest) {
460
+ const to = experimentRedirect(request, await getCockpitConfig({ timeoutMs: 800 }));
461
+ return to ? NextResponse.redirect(to, 307) : NextResponse.next();
462
+ }
463
+ export const config = { matcher: ["/pricing"] };
464
+ ```
465
+
466
+ A running test needs a stable id on the server (the `ck_id` cookie with `consent="cookies"`, or `{ distinctId }`); without one it is left to the provider.
467
+
468
+ ## 18. Messenger (0.9.0)
469
+
470
+ A chat bubble like Featurebase's or Intercom's messenger, with three tabs: **Home** (greeting, the latest chat, help search, top articles), **Messages** (the chat with your team) and **Help** (your published help articles from Support › Help center, searchable, read in the panel). Every chat is a Support conversation (channel "Chat") in MRRocket's inbox: you answer there, or the Customer experience agent does (Support › Automation › AI answers). A new answer while the panel is closed plays a short sound, shows a count on the bubble and a preview card above it.
471
+
472
+ ```tsx
473
+ import { Messenger, useMessenger } from "@mrrocket/sdk/next";
474
+
475
+ // Once, inside <CockpitProvider>:
476
+ <Messenger title="Acme" greeting="Hi! How can we help?" />;
477
+
478
+ // From your own button ("home", "messages", "help" or { article: id }):
479
+ const messenger = useMessenger();
480
+ <button onClick={() => messenger.open("help")}>Help</button>;
481
+
482
+ // Or with no code at all:
483
+ <a data-ck-messenger>Chat with us</a>;
484
+ ```
485
+
486
+ The look and texts (color, light or dark, side, title, greeting, button text, Help tab, sound, preview, asking for an email) are set per product in MRRocket › Support › Messenger and read by the messenger itself (`GET /api/sdk/messenger`), so they change without a deploy. What you pass in code wins.
487
+
488
+ Props: `title`, `greeting`, `launcher` (default true), `help` (the Help tab, default true; hidden while there is no published article), `sound` (default true), `preview` (default true), `askEmail` (default true), `position` ("bottom-right", "bottom-left"), `label`, `theme`, `accent`. The panel is plain DOM, loaded with a dynamic import after the page rendered.
489
+
490
+ Call `identify(user.id, { name, email })` and chats carry the person's name and email (they are not asked). While the panel is open it reads `GET /api/sdk/messages` every 4 seconds; while a visitor waits for an answer every 8 seconds for ten minutes, then every 30. Articles come from `GET /api/sdk/articles` as blocks, written with textContent, never HTML.
491
+
492
+ On sites with the script tag (Webflow, Framer, WordPress …) add `data-messenger="true"`; the messenger loads as a fifth file, `sdk-messenger.js`, only on pages that use it:
493
+
494
+ ```html
495
+ <script async src="https://app.mrrocket.dev/sdk.js" data-key="mrr_pub_…" data-messenger="true" data-messenger-title="Acme"></script>
496
+ ```
497
+
498
+ Also `data-messenger-greeting`, `-label`, `-position="bottom-left"`, and "false" for `-help`, `-sound`, `-preview`, `-ask-email`. `mrrocket.messenger.open()` opens it from your code.
499
+
500
+ In React Native, `<MRRocketProvider messenger={{ launcher: true }}>` adds the bubble, `useMessenger().open()` opens it from your own button; a new answer shows as a banner at the top, with a light haptic when `expo-haptics` is installed. The provider also shows your live in-app survey (`surveys={false}` to switch off) and the feedback sheet (`useFeedback().open()`). With the person's consent (`replay`) and `react-native-view-shot` installed, the app also records sessions as screen pictures for Sessions: none while the keyboard is open, and `<MRRocketMask>` areas are painted black on the server before storing. Taps go to Heatmaps as clicks on the current screen; the provider then wraps the app in one `View` with `flex: 1` (`heatmaps={false}` to switch off).
372
501
 
373
502
  ## Privacy and consent
374
503
 
@@ -0,0 +1,213 @@
1
+ /**
2
+ * The SDK's fetch client and the shapes it reads, without React, so the script tag's widgets (src/script) use it too.
3
+ * `./api` re-exports all of it with the `useApi` hook.
4
+ */
5
+ export type PostStatus = "review" | "planned" | "progress" | "shipped";
6
+ export declare const STATUS_LABEL: Record<PostStatus, string>;
7
+ export interface FeedbackBoardInfo {
8
+ id: string;
9
+ label: string;
10
+ }
11
+ export interface FeedbackPost {
12
+ id: string;
13
+ title: string;
14
+ body: string;
15
+ board: string;
16
+ status: PostStatus;
17
+ votes: number;
18
+ comments: number;
19
+ voted: boolean;
20
+ createdAt: string;
21
+ /** "Lena K.", "Anonymous" or "Team". Missing from servers before SDK 0.6. */
22
+ author?: string;
23
+ }
24
+ /** The product's public face for the board's header. */
25
+ export interface FeedbackProduct {
26
+ name: string;
27
+ logo: string | null;
28
+ /** The domain's favicon, tried when `logo` does not load. Older servers leave it out. */
29
+ logoFallback?: string | null;
30
+ }
31
+ export interface FeedbackList {
32
+ boards: FeedbackBoardInfo[];
33
+ posts: FeedbackPost[];
34
+ product?: FeedbackProduct;
35
+ }
36
+ export interface FeedbackRoadmap {
37
+ /** `review` (Backlog) comes from servers of SDK 0.6 on. */
38
+ columns: {
39
+ review?: FeedbackPost[];
40
+ planned: FeedbackPost[];
41
+ progress: FeedbackPost[];
42
+ shipped: FeedbackPost[];
43
+ };
44
+ }
45
+ export interface ChangelogEntry {
46
+ id: string;
47
+ title: string;
48
+ body: string;
49
+ publishedAt: string;
50
+ }
51
+ export interface FeedbackComment {
52
+ id: string;
53
+ body: string;
54
+ author: string;
55
+ isTeam: boolean;
56
+ postedAt: string;
57
+ }
58
+ export type SurveyQuestionKind = "nps" | "rating" | "single" | "multi" | "text";
59
+ export interface SurveyQuestion {
60
+ key: string;
61
+ kind: SurveyQuestionKind;
62
+ title: string;
63
+ description?: string;
64
+ required: boolean;
65
+ options?: string[];
66
+ scale?: number;
67
+ low?: string;
68
+ high?: string;
69
+ placeholder?: string;
70
+ }
71
+ export interface SurveyDefinition {
72
+ id: string;
73
+ name: string;
74
+ type: string;
75
+ thanks: string;
76
+ questions: SurveyQuestion[];
77
+ }
78
+ export type SurveyAnswer = number | string | string[];
79
+ /** One message of the messenger's chat. "visitor" is the person on the site; the rest come from the product's team. */
80
+ export interface ChatMessage {
81
+ id: string;
82
+ from: "visitor" | "team" | "ai" | "auto";
83
+ body: string;
84
+ at: string;
85
+ /** Help articles an answer drew on (servers from SDK 0.9 on). */
86
+ articles?: {
87
+ id: string;
88
+ title: string;
89
+ }[];
90
+ }
91
+ /** A published help article in a list (the messenger's Help tab). */
92
+ export interface ArticleSummary {
93
+ id: string;
94
+ title: string;
95
+ collection: string;
96
+ excerpt: string;
97
+ updatedAt: string;
98
+ }
99
+ /** A run of text inside an article block. Links are http(s), mailto or tel only. */
100
+ export interface ArticleSpan {
101
+ text: string;
102
+ href?: string;
103
+ bold?: boolean;
104
+ italic?: boolean;
105
+ code?: boolean;
106
+ }
107
+ /** An article's content as blocks, written with textContent (never parsed as HTML). */
108
+ export type ArticleBlock = {
109
+ type: "h2" | "h3" | "p" | "quote";
110
+ spans: ArticleSpan[];
111
+ } | {
112
+ type: "ul" | "ol";
113
+ items: ArticleSpan[][];
114
+ } | {
115
+ type: "code";
116
+ text: string;
117
+ } | {
118
+ type: "hr";
119
+ };
120
+ export interface Article {
121
+ id: string;
122
+ title: string;
123
+ collection: string;
124
+ updatedAt: string;
125
+ blocks: ArticleBlock[];
126
+ }
127
+ /** The messenger's look and texts as set in MRRocket › Support › Messenger (servers from SDK 0.9 on). */
128
+ export interface MessengerLook {
129
+ accent: string;
130
+ theme: "light" | "dark" | "auto";
131
+ position: "bottom-right" | "bottom-left";
132
+ title: string;
133
+ greeting: string;
134
+ label: string;
135
+ help: boolean;
136
+ sound: boolean;
137
+ preview: boolean;
138
+ askEmail: boolean;
139
+ }
140
+ /** The visitor's newest chat (open or closed) and its messages. */
141
+ export interface ChatThread {
142
+ conversation: {
143
+ id: string;
144
+ status: "open" | "solved";
145
+ } | null;
146
+ messages: ChatMessage[];
147
+ /** The visitor gave an email already: the messenger stops asking. */
148
+ hasEmail: boolean;
149
+ }
150
+ export declare class ApiError extends Error {
151
+ status: number;
152
+ constructor(message: string, status: number);
153
+ }
154
+ interface Conn {
155
+ host: string;
156
+ apiKey: string;
157
+ distinctId: string;
158
+ user?: {
159
+ id?: string;
160
+ [key: string]: unknown;
161
+ };
162
+ }
163
+ /** The small fetch client behind the widgets. Every read takes an AbortSignal so unmounting cancels it. */
164
+ export declare const createApi: (c: Conn) => {
165
+ list: (o: {
166
+ board?: string;
167
+ sort?: "top" | "new";
168
+ q?: string;
169
+ }, signal?: AbortSignal) => Promise<FeedbackList>;
170
+ post: (id: string, signal?: AbortSignal) => Promise<FeedbackPost>;
171
+ roadmap: (signal?: AbortSignal) => Promise<FeedbackRoadmap>;
172
+ changelog: (signal?: AbortSignal) => Promise<{
173
+ entries: ChangelogEntry[];
174
+ }>;
175
+ submit: (p: {
176
+ title: string;
177
+ body?: string;
178
+ board: string;
179
+ }) => Promise<FeedbackPost>;
180
+ vote: (id: string) => Promise<{
181
+ voted: boolean;
182
+ votes: number;
183
+ }>;
184
+ comments: (id: string, signal?: AbortSignal) => Promise<{
185
+ comments: FeedbackComment[];
186
+ }>;
187
+ comment: (id: string, body: string) => Promise<FeedbackComment>;
188
+ survey: (url: string, signal?: AbortSignal) => Promise<{
189
+ survey: SurveyDefinition | null;
190
+ }>;
191
+ seen: (id: string) => Promise<unknown>;
192
+ respond: (id: string, r: {
193
+ answers: Record<string, SurveyAnswer>;
194
+ completed: boolean;
195
+ seconds: number;
196
+ }) => Promise<unknown>;
197
+ chat: (signal?: AbortSignal) => Promise<ChatThread>;
198
+ sendChat: (m: {
199
+ body: string;
200
+ email?: string;
201
+ url?: string;
202
+ }) => Promise<ChatThread>;
203
+ articles: (q?: string, signal?: AbortSignal) => Promise<{
204
+ articles: ArticleSummary[];
205
+ }>;
206
+ article: (id: string, signal?: AbortSignal) => Promise<Article>;
207
+ messengerLook: (signal?: AbortSignal) => Promise<MessengerLook>;
208
+ rateArticle: (id: string, helpful: boolean) => Promise<{
209
+ counted: boolean;
210
+ }>;
211
+ };
212
+ export type Api = ReturnType<typeof createApi>;
213
+ export {};
@@ -0,0 +1,67 @@
1
+ // src/api-client.ts
2
+ var STATUS_LABEL = {
3
+ review: "Under review",
4
+ planned: "Planned",
5
+ progress: "In progress",
6
+ shipped: "Shipped"
7
+ };
8
+ var ApiError = class extends Error {
9
+ status;
10
+ constructor(message, status) {
11
+ super(message);
12
+ this.status = status;
13
+ }
14
+ };
15
+ var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
16
+ var call = async (url, init, signal) => {
17
+ let res;
18
+ try {
19
+ res = await fetch(url, {
20
+ ...init,
21
+ signal,
22
+ headers: init?.body ? { "content-type": "application/json" } : void 0
23
+ });
24
+ } catch (e) {
25
+ if (e.name === "AbortError") throw e;
26
+ throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
27
+ }
28
+ if (!res.ok) {
29
+ let detail = "";
30
+ try {
31
+ detail = (await res.json()).error ?? "";
32
+ } catch {
33
+ }
34
+ throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
35
+ }
36
+ return await res.json();
37
+ };
38
+ var createApi = (c) => {
39
+ const base = `${c.host}/api/sdk`;
40
+ const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
41
+ const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
42
+ return {
43
+ list: (o, signal) => call(`${base}/feedback?${qs({ key: c.apiKey, distinctId: c.distinctId, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
44
+ post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
45
+ roadmap: (signal) => call(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
46
+ changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
47
+ submit: (p) => post("/feedback", p),
48
+ vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
49
+ comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
50
+ comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
51
+ survey: (url, signal) => call(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, void 0, signal),
52
+ seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
53
+ respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
54
+ chat: (signal) => call(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
55
+ sendChat: (m) => post("/messages", m),
56
+ articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
57
+ article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
58
+ messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
59
+ rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
60
+ };
61
+ };
62
+ export {
63
+ ApiError,
64
+ STATUS_LABEL,
65
+ createApi
66
+ };
67
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api-client.ts"],"sourcesContent":["/**\n * The SDK's fetch client and the shapes it reads, without React, so the script tag's widgets (src/script) use it too.\n * `./api` re-exports all of it with the `useApi` hook.\n */\nexport type PostStatus = \"review\" | \"planned\" | \"progress\" | \"shipped\";\n\nexport const STATUS_LABEL: Record<PostStatus, string> = {\n review: \"Under review\",\n planned: \"Planned\",\n progress: \"In progress\",\n shipped: \"Shipped\",\n};\n\nexport interface FeedbackBoardInfo {\n id: string;\n label: string;\n}\n\nexport interface FeedbackPost {\n id: string;\n title: string;\n body: string;\n board: string;\n status: PostStatus;\n votes: number;\n comments: number;\n voted: boolean;\n createdAt: string;\n /** \"Lena K.\", \"Anonymous\" or \"Team\". Missing from servers before SDK 0.6. */\n author?: string;\n}\n\n/** The product's public face for the board's header. */\nexport interface FeedbackProduct {\n name: string;\n logo: string | null;\n /** The domain's favicon, tried when `logo` does not load. Older servers leave it out. */\n logoFallback?: string | null;\n}\n\nexport interface FeedbackList {\n boards: FeedbackBoardInfo[];\n posts: FeedbackPost[];\n product?: FeedbackProduct;\n}\n\nexport interface FeedbackRoadmap {\n /** `review` (Backlog) comes from servers of SDK 0.6 on. */\n columns: { review?: FeedbackPost[]; planned: FeedbackPost[]; progress: FeedbackPost[]; shipped: FeedbackPost[] };\n}\n\nexport interface ChangelogEntry {\n id: string;\n title: string;\n body: string;\n publishedAt: string;\n}\n\nexport interface FeedbackComment {\n id: string;\n body: string;\n author: string;\n isTeam: boolean;\n postedAt: string;\n}\n\nexport type SurveyQuestionKind = \"nps\" | \"rating\" | \"single\" | \"multi\" | \"text\";\n\nexport interface SurveyQuestion {\n key: string;\n kind: SurveyQuestionKind;\n title: string;\n description?: string;\n required: boolean;\n options?: string[];\n scale?: number;\n low?: string;\n high?: string;\n placeholder?: string;\n}\n\nexport interface SurveyDefinition {\n id: string;\n name: string;\n type: string;\n thanks: string;\n questions: SurveyQuestion[];\n}\n\nexport type SurveyAnswer = number | string | string[];\n\n/** One message of the messenger's chat. \"visitor\" is the person on the site; the rest come from the product's team. */\nexport interface ChatMessage {\n id: string;\n from: \"visitor\" | \"team\" | \"ai\" | \"auto\";\n body: string;\n at: string;\n /** Help articles an answer drew on (servers from SDK 0.9 on). */\n articles?: { id: string; title: string }[];\n}\n\n/** A published help article in a list (the messenger's Help tab). */\nexport interface ArticleSummary {\n id: string;\n title: string;\n collection: string;\n excerpt: string;\n updatedAt: string;\n}\n\n/** A run of text inside an article block. Links are http(s), mailto or tel only. */\nexport interface ArticleSpan {\n text: string;\n href?: string;\n bold?: boolean;\n italic?: boolean;\n code?: boolean;\n}\n\n/** An article's content as blocks, written with textContent (never parsed as HTML). */\nexport type ArticleBlock =\n | { type: \"h2\" | \"h3\" | \"p\" | \"quote\"; spans: ArticleSpan[] }\n | { type: \"ul\" | \"ol\"; items: ArticleSpan[][] }\n | { type: \"code\"; text: string }\n | { type: \"hr\" };\n\nexport interface Article {\n id: string;\n title: string;\n collection: string;\n updatedAt: string;\n blocks: ArticleBlock[];\n}\n\n/** The messenger's look and texts as set in MRRocket › Support › Messenger (servers from SDK 0.9 on). */\nexport interface MessengerLook {\n accent: string;\n theme: \"light\" | \"dark\" | \"auto\";\n position: \"bottom-right\" | \"bottom-left\";\n title: string;\n greeting: string;\n label: string;\n help: boolean;\n sound: boolean;\n preview: boolean;\n askEmail: boolean;\n}\n\n/** The visitor's newest chat (open or closed) and its messages. */\nexport interface ChatThread {\n conversation: { id: string; status: \"open\" | \"solved\" } | null;\n messages: ChatMessage[];\n /** The visitor gave an email already: the messenger stops asking. */\n hasEmail: boolean;\n}\n\nexport class ApiError extends Error {\n status: number;\n constructor(message: string, status: number) {\n super(message);\n this.status = status;\n }\n}\n\ninterface Conn {\n host: string;\n apiKey: string;\n distinctId: string;\n user?: { id?: string; [key: string]: unknown };\n}\n\n// Not URLSearchParams: React Native's lacks `set` in older versions, and the apps use this client too.\nconst qs = (params: Record<string, string | undefined>) =>\n Object.entries(params)\n .filter((e): e is [string, string] => !!e[1])\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join(\"&\");\n\nconst call = async <T>(url: string, init: RequestInit | undefined, signal?: AbortSignal): Promise<T> => {\n let res: Response;\n try {\n res = await fetch(url, {\n ...init,\n signal,\n headers: init?.body ? { \"content-type\": \"application/json\" } : undefined,\n });\n } catch (e) {\n if ((e as Error).name === \"AbortError\") throw e;\n throw new ApiError(\"Could not reach the server. Check your connection and try again.\", 0);\n }\n if (!res.ok) {\n let detail = \"\";\n try {\n detail = ((await res.json()) as { error?: string }).error ?? \"\";\n } catch {\n // Not JSON; the status is enough.\n }\n throw new ApiError(detail || (res.status === 429 ? \"Too many requests. Try again in a minute.\" : \"Something went wrong. Please try again.\"), res.status);\n }\n return (await res.json()) as T;\n};\n\n/** The small fetch client behind the widgets. Every read takes an AbortSignal so unmounting cancels it. */\nexport const createApi = (c: Conn) => {\n const base = `${c.host}/api/sdk`;\n const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };\n const post = <T>(path: string, body: Record<string, unknown>, signal?: AbortSignal) =>\n call<T>(`${base}${path}`, { method: \"POST\", body: JSON.stringify({ ...who, ...body }) }, signal);\n return {\n list: (o: { board?: string; sort?: \"top\" | \"new\"; q?: string }, signal?: AbortSignal) =>\n call<FeedbackList>(`${base}/feedback?${qs({ key: c.apiKey, distinctId: c.distinctId, board: o.board, sort: o.sort, q: o.q })}`, undefined, signal),\n post: (id: string, signal?: AbortSignal) =>\n call<FeedbackPost>(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, undefined, signal),\n roadmap: (signal?: AbortSignal) => call<FeedbackRoadmap>(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, undefined, signal),\n changelog: (signal?: AbortSignal) => call<{ entries: ChangelogEntry[] }>(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, undefined, signal),\n submit: (p: { title: string; body?: string; board: string }) => post<FeedbackPost>(\"/feedback\", p),\n vote: (id: string) => post<{ voted: boolean; votes: number }>(`/feedback/${encodeURIComponent(id)}/vote`, {}),\n comments: (id: string, signal?: AbortSignal) =>\n call<{ comments: FeedbackComment[] }>(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, undefined, signal),\n comment: (id: string, body: string) => post<FeedbackComment>(`/feedback/${encodeURIComponent(id)}/comments`, { body }),\n survey: (url: string, signal?: AbortSignal) =>\n call<{ survey: SurveyDefinition | null }>(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, undefined, signal),\n seen: (id: string) => post<unknown>(`/surveys/${encodeURIComponent(id)}/seen`, { user: undefined }),\n respond: (id: string, r: { answers: Record<string, SurveyAnswer>; completed: boolean; seconds: number }) =>\n post<unknown>(`/surveys/${encodeURIComponent(id)}/respond`, r),\n chat: (signal?: AbortSignal) => call<ChatThread>(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, undefined, signal),\n sendChat: (m: { body: string; email?: string; url?: string }) => post<ChatThread>(\"/messages\", m),\n articles: (q?: string, signal?: AbortSignal) => call<{ articles: ArticleSummary[] }>(`${base}/articles?${qs({ key: c.apiKey, q })}`, undefined, signal),\n article: (id: string, signal?: AbortSignal) =>\n call<Article>(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, undefined, signal),\n messengerLook: (signal?: AbortSignal) => call<MessengerLook>(`${base}/messenger?${qs({ key: c.apiKey })}`, undefined, signal),\n rateArticle: (id: string, helpful: boolean) => post<{ counted: boolean }>(`/articles/${encodeURIComponent(id)}/rate`, { helpful }),\n };\n};\n\nexport type Api = ReturnType<typeof createApi>;\n"],"mappings":"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","names":[]}