@mrrocket/sdk 0.8.0 → 0.10.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 (45) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +35 -1
  3. package/dist/api-client.d.ts +221 -0
  4. package/dist/api-client.js +79 -0
  5. package/dist/api-client.js.map +1 -0
  6. package/dist/api.d.ts +2 -122
  7. package/dist/api.js +2 -61
  8. package/dist/api.js.map +1 -1
  9. package/dist/browser/mrrocket-messenger.js +348 -0
  10. package/dist/browser/mrrocket-replay.js +3 -3
  11. package/dist/browser/mrrocket.js +1 -1
  12. package/dist/browser.d.ts +2 -1
  13. package/dist/browser.js +3 -0
  14. package/dist/browser.js.map +1 -1
  15. package/dist/capture.d.ts +1 -13
  16. package/dist/capture.js +1 -22
  17. package/dist/capture.js.map +1 -1
  18. package/dist/core.d.ts +1 -1
  19. package/dist/core.js +1 -1
  20. package/dist/core.js.map +1 -1
  21. package/dist/feedback.js +132 -24
  22. package/dist/feedback.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/messenger-ui.d.ts +49 -0
  27. package/dist/messenger-ui.js +619 -0
  28. package/dist/messenger-ui.js.map +1 -0
  29. package/dist/messenger.d.ts +37 -0
  30. package/dist/messenger.js +69 -0
  31. package/dist/messenger.js.map +1 -0
  32. package/dist/rage.d.ts +14 -0
  33. package/dist/rage.js +27 -0
  34. package/dist/rage.js.map +1 -0
  35. package/dist/react-native/index.d.cts +349 -4
  36. package/dist/react-native/index.d.ts +349 -4
  37. package/dist/react-native/index.js +1345 -31
  38. package/dist/react-native/index.js.map +1 -1
  39. package/dist/styles-css.d.ts +6 -0
  40. package/dist/styles-css.js +362 -0
  41. package/dist/styles-css.js.map +1 -0
  42. package/dist/styles.d.ts +1 -2
  43. package/dist/styles.js +3 -237
  44. package/dist/styles.js.map +1 -1
  45. package/package.json +17 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
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.10.0
6
+
7
+ - **Messenger, new look (web and apps):** Home opens under a head in your color with the greeting large on it, then cards: the recent chat with the team's mark, "Send us a message" with how soon you reply, and help search with the top articles. The chat has a round send button in one rounded field, softer bubbles and the reply time in its head; the launcher is larger, turns into a cross when open and takes a dark text on a light color. React Native draws the same: line icons with the optional `react-native-svg` (text glyphs without it).
8
+ - **More from MRRocket › Support › Messenger:** `replyTime` ("minutes", "hours", "day") and, for apps, `appLauncher` (a chat button in the app) plus the side, the button text and Sound (a light haptic in apps), which React Native now reads too. What the code passes still wins.
9
+ - New in `api-client`: `replyLine(replyTime)` and `inkOn(color)`.
10
+ - Version reported in event batches: `0.10.0`.
11
+
12
+ ## 0.9.0
13
+
14
+ - **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.
15
+ - **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.
16
+ - 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`.
17
+ - **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`.
18
+ - 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.
19
+ - **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.
20
+ - **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`.
21
+ - **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.
22
+ - `RageDetector` moved to its own module (`rage.ts`) so the apps share it.
23
+ - Version reported in event batches: `0.9.0`.
24
+
5
25
  ## 0.8.0
6
26
 
7
27
  - **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.
package/README.md CHANGED
@@ -389,7 +389,7 @@ Consent:
389
389
 
390
390
  The SDK does not remember the choice: pass it on every start from your consent screen's stored answer, or call `setConsent()`.
391
391
 
392
- 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
393
 
394
394
  ## 15. Cancel flow, onboarding and in-app messages (0.7.0)
395
395
 
@@ -465,6 +465,40 @@ export const config = { matcher: ["/pricing"] };
465
465
 
466
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
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).
501
+
468
502
  ## Privacy and consent
469
503
 
470
504
  ```tsx
@@ -0,0 +1,221 @@
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
+ /** How soon the team usually answers (MRRocket from 0.10; older servers leave it out). */
136
+ replyTime?: "minutes" | "hours" | "day";
137
+ /** A round chat button in the app (React Native) too. */
138
+ appLauncher?: boolean;
139
+ help: boolean;
140
+ sound: boolean;
141
+ preview: boolean;
142
+ askEmail: boolean;
143
+ }
144
+ /** The line under "Send us a message" and in the chat's head. */
145
+ export declare const replyLine: (t: MessengerLook["replyTime"]) => "We usually reply in a few minutes" | "We usually reply within a day" | "We usually reply in a few hours";
146
+ /** Black or white, whichever reads better on the accent (a light custom color gets dark text). */
147
+ export declare const inkOn: (hex: string) => "#ffffff" | "#111214";
148
+ /** The visitor's newest chat (open or closed) and its messages. */
149
+ export interface ChatThread {
150
+ conversation: {
151
+ id: string;
152
+ status: "open" | "solved";
153
+ } | null;
154
+ messages: ChatMessage[];
155
+ /** The visitor gave an email already: the messenger stops asking. */
156
+ hasEmail: boolean;
157
+ }
158
+ export declare class ApiError extends Error {
159
+ status: number;
160
+ constructor(message: string, status: number);
161
+ }
162
+ interface Conn {
163
+ host: string;
164
+ apiKey: string;
165
+ distinctId: string;
166
+ user?: {
167
+ id?: string;
168
+ [key: string]: unknown;
169
+ };
170
+ }
171
+ /** The small fetch client behind the widgets. Every read takes an AbortSignal so unmounting cancels it. */
172
+ export declare const createApi: (c: Conn) => {
173
+ list: (o: {
174
+ board?: string;
175
+ sort?: "top" | "new";
176
+ q?: string;
177
+ }, signal?: AbortSignal) => Promise<FeedbackList>;
178
+ post: (id: string, signal?: AbortSignal) => Promise<FeedbackPost>;
179
+ roadmap: (signal?: AbortSignal) => Promise<FeedbackRoadmap>;
180
+ changelog: (signal?: AbortSignal) => Promise<{
181
+ entries: ChangelogEntry[];
182
+ }>;
183
+ submit: (p: {
184
+ title: string;
185
+ body?: string;
186
+ board: string;
187
+ }) => Promise<FeedbackPost>;
188
+ vote: (id: string) => Promise<{
189
+ voted: boolean;
190
+ votes: number;
191
+ }>;
192
+ comments: (id: string, signal?: AbortSignal) => Promise<{
193
+ comments: FeedbackComment[];
194
+ }>;
195
+ comment: (id: string, body: string) => Promise<FeedbackComment>;
196
+ survey: (url: string, signal?: AbortSignal) => Promise<{
197
+ survey: SurveyDefinition | null;
198
+ }>;
199
+ seen: (id: string) => Promise<unknown>;
200
+ respond: (id: string, r: {
201
+ answers: Record<string, SurveyAnswer>;
202
+ completed: boolean;
203
+ seconds: number;
204
+ }) => Promise<unknown>;
205
+ chat: (signal?: AbortSignal) => Promise<ChatThread>;
206
+ sendChat: (m: {
207
+ body: string;
208
+ email?: string;
209
+ url?: string;
210
+ }) => Promise<ChatThread>;
211
+ articles: (q?: string, signal?: AbortSignal) => Promise<{
212
+ articles: ArticleSummary[];
213
+ }>;
214
+ article: (id: string, signal?: AbortSignal) => Promise<Article>;
215
+ messengerLook: (signal?: AbortSignal) => Promise<MessengerLook>;
216
+ rateArticle: (id: string, helpful: boolean) => Promise<{
217
+ counted: boolean;
218
+ }>;
219
+ };
220
+ export type Api = ReturnType<typeof createApi>;
221
+ export {};
@@ -0,0 +1,79 @@
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 replyLine = (t) => t === "minutes" ? "We usually reply in a few minutes" : t === "day" ? "We usually reply within a day" : "We usually reply in a few hours";
9
+ var inkOn = (hex) => {
10
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
11
+ if (!m) return "#ffffff";
12
+ const [r, g, b] = [m[1], m[2], m[3]].map((x) => {
13
+ const c = parseInt(x, 16) / 255;
14
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
15
+ });
16
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
17
+ };
18
+ var ApiError = class extends Error {
19
+ status;
20
+ constructor(message, status) {
21
+ super(message);
22
+ this.status = status;
23
+ }
24
+ };
25
+ var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
26
+ var call = async (url, init, signal) => {
27
+ let res;
28
+ try {
29
+ res = await fetch(url, {
30
+ ...init,
31
+ signal,
32
+ headers: init?.body ? { "content-type": "application/json" } : void 0
33
+ });
34
+ } catch (e) {
35
+ if (e.name === "AbortError") throw e;
36
+ throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
37
+ }
38
+ if (!res.ok) {
39
+ let detail = "";
40
+ try {
41
+ detail = (await res.json()).error ?? "";
42
+ } catch {
43
+ }
44
+ throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
45
+ }
46
+ return await res.json();
47
+ };
48
+ var createApi = (c) => {
49
+ const base = `${c.host}/api/sdk`;
50
+ const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
51
+ const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
52
+ return {
53
+ 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),
54
+ post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
55
+ roadmap: (signal) => call(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
56
+ changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
57
+ submit: (p) => post("/feedback", p),
58
+ vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
59
+ comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
60
+ comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
61
+ survey: (url, signal) => call(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, void 0, signal),
62
+ seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
63
+ respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
64
+ chat: (signal) => call(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
65
+ sendChat: (m) => post("/messages", m),
66
+ articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
67
+ article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
68
+ messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
69
+ rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
70
+ };
71
+ };
72
+ export {
73
+ ApiError,
74
+ STATUS_LABEL,
75
+ createApi,
76
+ inkOn,
77
+ replyLine
78
+ };
79
+ //# 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 /** How soon the team usually answers (MRRocket from 0.10; older servers leave it out). */\n replyTime?: \"minutes\" | \"hours\" | \"day\";\n /** A round chat button in the app (React Native) too. */\n appLauncher?: boolean;\n help: boolean;\n sound: boolean;\n preview: boolean;\n askEmail: boolean;\n}\n\n/** The line under \"Send us a message\" and in the chat's head. */\nexport const replyLine = (t: MessengerLook[\"replyTime\"]) =>\n t === \"minutes\" ? \"We usually reply in a few minutes\" : t === \"day\" ? \"We usually reply within a day\" : \"We usually reply in a few hours\";\n\n/** Black or white, whichever reads better on the accent (a light custom color gets dark text). */\nexport const inkOn = (hex: string) => {\n const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);\n if (!m) return \"#ffffff\";\n const [r, g, b] = [m[1], m[2], m[3]].map((x) => {\n const c = parseInt(x, 16) / 255;\n return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;\n });\n return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.45 ? \"#111214\" : \"#ffffff\";\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":[]}
package/dist/api.d.ts CHANGED
@@ -1,124 +1,5 @@
1
- export type PostStatus = "review" | "planned" | "progress" | "shipped";
2
- export declare const STATUS_LABEL: Record<PostStatus, string>;
3
- export interface FeedbackBoardInfo {
4
- id: string;
5
- label: string;
6
- }
7
- export interface FeedbackPost {
8
- id: string;
9
- title: string;
10
- body: string;
11
- board: string;
12
- status: PostStatus;
13
- votes: number;
14
- comments: number;
15
- voted: boolean;
16
- createdAt: string;
17
- /** "Lena K.", "Anonymous" or "Team". Missing from servers before SDK 0.6. */
18
- author?: string;
19
- }
20
- /** The product's public face for the board's header. */
21
- export interface FeedbackProduct {
22
- name: string;
23
- logo: string | null;
24
- }
25
- export interface FeedbackList {
26
- boards: FeedbackBoardInfo[];
27
- posts: FeedbackPost[];
28
- product?: FeedbackProduct;
29
- }
30
- export interface FeedbackRoadmap {
31
- /** `review` (Backlog) comes from servers of SDK 0.6 on. */
32
- columns: {
33
- review?: FeedbackPost[];
34
- planned: FeedbackPost[];
35
- progress: FeedbackPost[];
36
- shipped: FeedbackPost[];
37
- };
38
- }
39
- export interface ChangelogEntry {
40
- id: string;
41
- title: string;
42
- body: string;
43
- publishedAt: string;
44
- }
45
- export interface FeedbackComment {
46
- id: string;
47
- body: string;
48
- author: string;
49
- isTeam: boolean;
50
- postedAt: string;
51
- }
52
- export type SurveyQuestionKind = "nps" | "rating" | "single" | "multi" | "text";
53
- export interface SurveyQuestion {
54
- key: string;
55
- kind: SurveyQuestionKind;
56
- title: string;
57
- description?: string;
58
- required: boolean;
59
- options?: string[];
60
- scale?: number;
61
- low?: string;
62
- high?: string;
63
- placeholder?: string;
64
- }
65
- export interface SurveyDefinition {
66
- id: string;
67
- name: string;
68
- type: string;
69
- thanks: string;
70
- questions: SurveyQuestion[];
71
- }
72
- export type SurveyAnswer = number | string | string[];
73
- export declare class ApiError extends Error {
74
- status: number;
75
- constructor(message: string, status: number);
76
- }
77
- interface Conn {
78
- host: string;
79
- apiKey: string;
80
- distinctId: string;
81
- user?: {
82
- id?: string;
83
- [key: string]: unknown;
84
- };
85
- }
86
- /** The small fetch client behind the widgets. Every read takes an AbortSignal so unmounting cancels it. */
87
- export declare const createApi: (c: Conn) => {
88
- list: (o: {
89
- board?: string;
90
- sort?: "top" | "new";
91
- q?: string;
92
- }, signal?: AbortSignal) => Promise<FeedbackList>;
93
- post: (id: string, signal?: AbortSignal) => Promise<FeedbackPost>;
94
- roadmap: (signal?: AbortSignal) => Promise<FeedbackRoadmap>;
95
- changelog: (signal?: AbortSignal) => Promise<{
96
- entries: ChangelogEntry[];
97
- }>;
98
- submit: (p: {
99
- title: string;
100
- body?: string;
101
- board: string;
102
- }) => Promise<FeedbackPost>;
103
- vote: (id: string) => Promise<{
104
- voted: boolean;
105
- votes: number;
106
- }>;
107
- comments: (id: string, signal?: AbortSignal) => Promise<{
108
- comments: FeedbackComment[];
109
- }>;
110
- comment: (id: string, body: string) => Promise<FeedbackComment>;
111
- survey: (url: string, signal?: AbortSignal) => Promise<{
112
- survey: SurveyDefinition | null;
113
- }>;
114
- seen: (id: string) => Promise<unknown>;
115
- respond: (id: string, r: {
116
- answers: Record<string, SurveyAnswer>;
117
- completed: boolean;
118
- seconds: number;
119
- }) => Promise<unknown>;
120
- };
121
- export type Api = ReturnType<typeof createApi>;
1
+ import { type Api } from "./api-client.js";
2
+ export * from "./api-client.js";
122
3
  /** Who writes: the provider's visitor, or the name and email a visitor gave the board (see useBoardIdentity). */
123
4
  export interface Writer {
124
5
  distinctId: string;
@@ -130,4 +11,3 @@ export interface Writer {
130
11
  }
131
12
  /** The client for the provider's key, host and visitor, or for `as` when given. */
132
13
  export declare const useApi: (as?: Writer | null) => Api;
133
- export {};
package/dist/api.js CHANGED
@@ -1,64 +1,8 @@
1
1
  // src/api.ts
2
2
  import { useMemo } from "react";
3
+ import { createApi } from "./api-client.js";
3
4
  import { useCtx } from "./context.js";
4
- var STATUS_LABEL = {
5
- review: "Under review",
6
- planned: "Planned",
7
- progress: "In progress",
8
- shipped: "Shipped"
9
- };
10
- var ApiError = class extends Error {
11
- status;
12
- constructor(message, status) {
13
- super(message);
14
- this.status = status;
15
- }
16
- };
17
- var qs = (params) => {
18
- const p = new URLSearchParams();
19
- for (const [k, v] of Object.entries(params)) if (v) p.set(k, v);
20
- return p.toString();
21
- };
22
- var call = async (url, init, signal) => {
23
- let res;
24
- try {
25
- res = await fetch(url, {
26
- ...init,
27
- signal,
28
- headers: init?.body ? { "content-type": "application/json" } : void 0
29
- });
30
- } catch (e) {
31
- if (e.name === "AbortError") throw e;
32
- throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
33
- }
34
- if (!res.ok) {
35
- let detail = "";
36
- try {
37
- detail = (await res.json()).error ?? "";
38
- } catch {
39
- }
40
- throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
41
- }
42
- return await res.json();
43
- };
44
- var createApi = (c) => {
45
- const base = `${c.host}/api/sdk`;
46
- const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
47
- const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
48
- return {
49
- 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),
50
- post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
51
- roadmap: (signal) => call(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
52
- changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
53
- submit: (p) => post("/feedback", p),
54
- vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
55
- comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
56
- comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
57
- survey: (url, signal) => call(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, void 0, signal),
58
- seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
59
- respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r)
60
- };
61
- };
5
+ export * from "./api-client.js";
62
6
  var useApi = (as) => {
63
7
  const ctx = useCtx();
64
8
  const distinctId = as?.distinctId ?? ctx.distinctId;
@@ -67,9 +11,6 @@ var useApi = (as) => {
67
11
  return useMemo(() => createApi({ apiKey, host, distinctId, user }), [apiKey, host, distinctId, user]);
68
12
  };
69
13
  export {
70
- ApiError,
71
- STATUS_LABEL,
72
- createApi,
73
14
  useApi
74
15
  };
75
16
  //# sourceMappingURL=api.js.map
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useCtx } from \"./context\";\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}\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\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\nconst qs = (params: Record<string, string | undefined>) => {\n const p = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) if (v) p.set(k, v);\n return p.toString();\n};\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 };\n};\n\nexport type Api = ReturnType<typeof createApi>;\n\n/** Who writes: the provider's visitor, or the name and email a visitor gave the board (see useBoardIdentity). */\nexport interface Writer {\n distinctId: string;\n user?: { id?: string; name?: string; email?: string };\n}\n\n/** The client for the provider's key, host and visitor, or for `as` when given. */\nexport const useApi = (as?: Writer | null): Api => {\n const ctx = useCtx();\n const distinctId = as?.distinctId ?? ctx.distinctId;\n const user = as ? as.user : ctx.user;\n const { apiKey, host } = ctx;\n return useMemo(() => createApi({ apiKey, host, distinctId, user }), [apiKey, host, distinctId, user]);\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/api.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { type Api, createApi } from \"./api-client\";\nimport { useCtx } from \"./context\";\n\nexport * from \"./api-client\";\n\n/** Who writes: the provider's visitor, or the name and email a visitor gave the board (see useBoardIdentity). */\nexport interface Writer {\n distinctId: string;\n user?: { id?: string; name?: string; email?: string };\n}\n\n/** The client for the provider's key, host and visitor, or for `as` when given. */\nexport const useApi = (as?: Writer | null): Api => {\n const ctx = useCtx();\n const distinctId = as?.distinctId ?? ctx.distinctId;\n const user = as ? as.user : ctx.user;\n const { apiKey, host } = ctx;\n return useMemo(() => createApi({ apiKey, host, distinctId, user }), [apiKey, host, distinctId, user]);\n};\n"],"mappings":";AAAA,SAAS,eAAe;AACxB,SAAmB,iBAAiB;AACpC,SAAS,cAAc;AAEvB,cAAc;AASP,IAAM,SAAS,CAAC,OAA4B;AAC/C,QAAM,MAAM,OAAO;AACnB,QAAM,aAAa,IAAI,cAAc,IAAI;AACzC,QAAM,OAAO,KAAK,GAAG,OAAO,IAAI;AAChC,QAAM,EAAE,QAAQ,KAAK,IAAI;AACzB,SAAO,QAAQ,MAAM,UAAU,EAAE,QAAQ,MAAM,YAAY,KAAK,CAAC,GAAG,CAAC,QAAQ,MAAM,YAAY,IAAI,CAAC;AACxG;","names":[]}