@solumflow-app/crm-client 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Placing an account's forms, booking pages, chat and tracking on a React
3
+ * site, without pasting HTML. React 19 or later: the scripts are rendered as
4
+ * `<script async>` elements, which React 19 loads once per page however many
5
+ * components ask for them.
6
+ *
7
+ * ```tsx
8
+ * <CrmEmbedProvider appOrigin="https://app.example.com">
9
+ * <CrmForm formId="…" title="Contact" />
10
+ * <CrmBooking url="https://app.example.com/book/acme" title="Book" />
11
+ * <CrmChatWidget widgetId="…" />
12
+ * <CrmSiteTracking publicKey="…" />
13
+ * </CrmEmbedProvider>
14
+ * ```
15
+ */
16
+ export { CrmBooking } from './crm-booking';
17
+ export { CrmChatWidget } from './crm-chat-widget';
18
+ export { CrmForm } from './crm-form';
19
+ export { CrmSiteTracking } from './crm-site-tracking';
20
+ export { CrmEmbedProvider } from './embed-origin';
package/dist/react.cjs ADDED
@@ -0,0 +1,163 @@
1
+ 'use client';
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/react/index.ts
22
+ var react_exports = {};
23
+ __export(react_exports, {
24
+ CrmBooking: () => CrmBooking,
25
+ CrmChatWidget: () => CrmChatWidget,
26
+ CrmEmbedProvider: () => CrmEmbedProvider,
27
+ CrmForm: () => CrmForm,
28
+ CrmSiteTracking: () => CrmSiteTracking
29
+ });
30
+ module.exports = __toCommonJS(react_exports);
31
+
32
+ // src/react/embed-origin.tsx
33
+ var import_react = require("react");
34
+ var import_jsx_runtime = require("react/jsx-runtime");
35
+ var EmbedOriginContext = (0, import_react.createContext)(null);
36
+ function CrmEmbedProvider(props) {
37
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmbedOriginContext.Provider, { value: normaliseOrigin(props.appOrigin), children: props.children });
38
+ }
39
+ function useEmbedOrigin(appOrigin) {
40
+ const fromProvider = (0, import_react.useContext)(EmbedOriginContext);
41
+ const origin = appOrigin ? normaliseOrigin(appOrigin) : fromProvider;
42
+ if (!origin) {
43
+ throw new Error(
44
+ 'crm-client/react: no app origin. Pass `appOrigin` to the component or wrap the page in <CrmEmbedProvider appOrigin="https://app.example.com">. Never the page\u2019s own origin: that is your site, not the app.'
45
+ );
46
+ }
47
+ return origin;
48
+ }
49
+ function normaliseOrigin(value) {
50
+ return value.trim().replace(/\/+$/, "");
51
+ }
52
+
53
+ // src/react/crm-booking.tsx
54
+ var import_jsx_runtime2 = require("react/jsx-runtime");
55
+ function CrmBooking(props) {
56
+ const origin = useEmbedOrigin(props.appOrigin);
57
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
58
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
59
+ "iframe",
60
+ {
61
+ src: `${props.url}?embed=1`,
62
+ "data-service-booking-embed": "",
63
+ title: props.title,
64
+ className: props.className,
65
+ style: { width: "100%", border: 0 }
66
+ }
67
+ ),
68
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("script", { async: true, src: `${origin}/booking-embed.js` })
69
+ ] });
70
+ }
71
+
72
+ // src/react/crm-chat-widget.tsx
73
+ var import_jsx_runtime3 = require("react/jsx-runtime");
74
+ function CrmChatWidget(props) {
75
+ const origin = useEmbedOrigin(props.appOrigin);
76
+ const hideOn = props.hideOnPaths ?? [];
77
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
78
+ "script",
79
+ {
80
+ async: true,
81
+ src: `${origin}/embed/v1/chat.js`,
82
+ "data-chat-widget-id": props.widgetId,
83
+ ...hideOn.length > 0 ? { "data-chat-hide-on": hideOn.join(",") } : {},
84
+ ...props.loadOnInteraction ? { "data-chat-load-on-interaction": "true" } : {}
85
+ }
86
+ );
87
+ }
88
+
89
+ // src/react/crm-form.tsx
90
+ var import_react2 = require("react");
91
+ var import_jsx_runtime4 = require("react/jsx-runtime");
92
+ function CrmForm(props) {
93
+ const origin = useEmbedOrigin(props.appOrigin);
94
+ const frame = (0, import_react2.useRef)(null);
95
+ (0, import_react2.useEffect)(() => {
96
+ function onMessage(event) {
97
+ const element = frame.current;
98
+ if (!element || event.origin !== origin || event.source !== element.contentWindow) {
99
+ return;
100
+ }
101
+ const data = event.data;
102
+ if (data?.type === "crm-form:resize" && typeof data.height === "number") {
103
+ element.style.height = `${Math.max(0, data.height)}px`;
104
+ return;
105
+ }
106
+ if (data?.type === "crm-form:submit") {
107
+ redirectTo(data.redirectUrl);
108
+ }
109
+ }
110
+ window.addEventListener("message", onMessage);
111
+ return () => window.removeEventListener("message", onMessage);
112
+ }, [origin]);
113
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
114
+ "iframe",
115
+ {
116
+ ref: frame,
117
+ src: `${origin}/f/${encodeURIComponent(props.formId)}`,
118
+ title: props.title,
119
+ loading: "lazy",
120
+ className: props.className,
121
+ style: {
122
+ width: "100%",
123
+ border: 0,
124
+ display: "block",
125
+ minHeight: 200
126
+ }
127
+ }
128
+ );
129
+ }
130
+ function redirectTo(redirectUrl) {
131
+ if (typeof redirectUrl !== "string" || !redirectUrl) {
132
+ return;
133
+ }
134
+ try {
135
+ const url = new URL(redirectUrl);
136
+ if (url.protocol === "https:") {
137
+ window.top.location.href = url.href;
138
+ }
139
+ } catch {
140
+ }
141
+ }
142
+
143
+ // src/react/crm-site-tracking.tsx
144
+ var import_jsx_runtime5 = require("react/jsx-runtime");
145
+ function CrmSiteTracking(props) {
146
+ const origin = useEmbedOrigin(props.appOrigin);
147
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
148
+ "script",
149
+ {
150
+ async: true,
151
+ src: `${origin}/api/site-tracking/script?k=${encodeURIComponent(props.publicKey)}`
152
+ }
153
+ );
154
+ }
155
+ // Annotate the CommonJS export names for ESM import in node:
156
+ 0 && (module.exports = {
157
+ CrmBooking,
158
+ CrmChatWidget,
159
+ CrmEmbedProvider,
160
+ CrmForm,
161
+ CrmSiteTracking
162
+ });
163
+ //# sourceMappingURL=react.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/index.ts","../src/react/embed-origin.tsx","../src/react/crm-booking.tsx","../src/react/crm-chat-widget.tsx","../src/react/crm-form.tsx","../src/react/crm-site-tracking.tsx"],"sourcesContent":["/**\n * Placing an account's forms, booking pages, chat and tracking on a React\n * site, without pasting HTML. React 19 or later: the scripts are rendered as\n * `<script async>` elements, which React 19 loads once per page however many\n * components ask for them.\n *\n * ```tsx\n * <CrmEmbedProvider appOrigin=\"https://app.example.com\">\n * <CrmForm formId=\"…\" title=\"Contact\" />\n * <CrmBooking url=\"https://app.example.com/book/acme\" title=\"Book\" />\n * <CrmChatWidget widgetId=\"…\" />\n * <CrmSiteTracking publicKey=\"…\" />\n * </CrmEmbedProvider>\n * ```\n */\nexport { CrmBooking } from './crm-booking';\nexport { CrmChatWidget } from './crm-chat-widget';\nexport { CrmForm } from './crm-form';\nexport { CrmSiteTracking } from './crm-site-tracking';\nexport { CrmEmbedProvider } from './embed-origin';\n","'use client';\n\nimport { type ReactNode, createContext, useContext } from 'react';\n\nconst EmbedOriginContext = createContext<string | null>(null);\n\n/**\n * The app's address, once for every component below it, so a site does not\n * repeat it on each form, booking page and chat. The same origin as the\n * client's `baseUrl`: `https://app.example.com`, without a path.\n */\nexport function CrmEmbedProvider(props: {\n appOrigin: string;\n children: ReactNode;\n}) {\n return (\n <EmbedOriginContext.Provider value={normaliseOrigin(props.appOrigin)}>\n {props.children}\n </EmbedOriginContext.Provider>\n );\n}\n\n/** The origin from the prop, else from the provider; refuses to guess one. */\nexport function useEmbedOrigin(appOrigin: string | undefined) {\n const fromProvider = useContext(EmbedOriginContext);\n const origin = appOrigin ? normaliseOrigin(appOrigin) : fromProvider;\n\n if (!origin) {\n throw new Error(\n 'crm-client/react: no app origin. Pass `appOrigin` to the component or ' +\n 'wrap the page in <CrmEmbedProvider appOrigin=\"https://app.example.com\">. ' +\n 'Never the page’s own origin: that is your site, not the app.',\n );\n }\n\n return origin;\n}\n\nfunction normaliseOrigin(value: string) {\n return value.trim().replace(/\\/+$/, '');\n}\n","import { useEmbedOrigin } from './embed-origin';\n\n/**\n * A booking page, embedded: the iframe the \"Share booking\" dialog hands out,\n * plus the script that makes it as tall as what is inside it.\n *\n * `url` is a booking page's address as the API lists it\n * (`getBookingPages()`), and it must be one of the app's own: the script only\n * listens to frames on that origin. React 19 loads the script once per page,\n * however many of these are on it.\n */\nexport function CrmBooking(props: {\n url: string;\n /** The frame's accessible name, such as the service's name. */\n title: string;\n appOrigin?: string;\n className?: string;\n}) {\n const origin = useEmbedOrigin(props.appOrigin);\n\n return (\n <>\n <iframe\n src={`${props.url}?embed=1`}\n data-service-booking-embed=\"\"\n title={props.title}\n className={props.className}\n style={{ width: '100%', border: 0 }}\n />\n <script async src={`${origin}/booking-embed.js`} />\n </>\n );\n}\n","import { useEmbedOrigin } from './embed-origin';\n\n/**\n * The chat widget of the account, in the corner of every page it is rendered\n * on. Put it in the layout, once: the chat script reads its settings off its\n * own tag and runs once per page.\n *\n * Only a published widget answers, and only on a site in its list of allowed\n * origins (an empty list allows any site). `hideOnPaths` and\n * `loadOnInteraction` are the two switches the Embed tab puts on the tag; pass\n * what `getChatWidgets()` shows in its snippet, or leave them out.\n */\nexport function CrmChatWidget(props: {\n widgetId: string;\n appOrigin?: string;\n hideOnPaths?: readonly string[];\n loadOnInteraction?: boolean;\n}) {\n const origin = useEmbedOrigin(props.appOrigin);\n const hideOn = props.hideOnPaths ?? [];\n\n return (\n <script\n async\n src={`${origin}/embed/v1/chat.js`}\n data-chat-widget-id={props.widgetId}\n {...(hideOn.length > 0 ? { 'data-chat-hide-on': hideOn.join(',') } : {})}\n {...(props.loadOnInteraction\n ? { 'data-chat-load-on-interaction': 'true' }\n : {})}\n />\n );\n}\n","'use client';\n\nimport { useEffect, useRef } from 'react';\n\nimport { useEmbedOrigin } from './embed-origin';\n\n/**\n * A published form, drawn in place.\n *\n * The iframe is drawn here rather than left to `embed/v1/embed.js`, because\n * that loader looks for forms once, when it loads: a form that appears after a\n * client-side navigation would stay an empty box. What the loader does besides\n * drawing the frame is honoured here, on the same contract:\n *\n * - `crm-form:resize` sets the frame's height, so it never scrolls inside;\n * - `crm-form:submit` with an `https://` `redirectUrl` sends the whole page\n * there, and nothing else ever navigates it.\n *\n * Both are trusted only from the app's own origin *and* from this frame's own\n * window: another frame on the page can send the same shape.\n */\nexport function CrmForm(props: {\n formId: string;\n appOrigin?: string;\n /**\n * The frame's accessible name, in your site's own language: what a screen\n * reader announces before the form. Required, because this package has no\n * language of its own to fall back on.\n */\n title: string;\n className?: string;\n}) {\n const origin = useEmbedOrigin(props.appOrigin);\n const frame = useRef<HTMLIFrameElement>(null);\n\n // An effect because the page's message channel is outside React: this\n // subscribes to it for as long as the frame is on the page, and lets go.\n useEffect(() => {\n function onMessage(event: MessageEvent) {\n const element = frame.current;\n\n if (\n !element ||\n event.origin !== origin ||\n event.source !== element.contentWindow\n ) {\n return;\n }\n\n const data = event.data as {\n type?: unknown;\n height?: unknown;\n redirectUrl?: unknown;\n } | null;\n\n if (data?.type === 'crm-form:resize' && typeof data.height === 'number') {\n element.style.height = `${Math.max(0, data.height)}px`;\n return;\n }\n\n if (data?.type === 'crm-form:submit') {\n redirectTo(data.redirectUrl);\n }\n }\n\n window.addEventListener('message', onMessage);\n\n return () => window.removeEventListener('message', onMessage);\n }, [origin]);\n\n return (\n <iframe\n ref={frame}\n src={`${origin}/f/${encodeURIComponent(props.formId)}`}\n title={props.title}\n loading=\"lazy\"\n className={props.className}\n style={{\n width: '100%',\n border: 0,\n display: 'block',\n minHeight: 200,\n }}\n />\n );\n}\n\nfunction redirectTo(redirectUrl: unknown) {\n if (typeof redirectUrl !== 'string' || !redirectUrl) {\n return;\n }\n\n try {\n const url = new URL(redirectUrl);\n\n if (url.protocol === 'https:') {\n window.top!.location.href = url.href;\n }\n } catch {\n // A malformed address is ignored; the page never navigates on one.\n }\n}\n","import { useEmbedOrigin } from './embed-origin';\n\n/**\n * The website tracking script. Put it in the layout, once.\n *\n * It does nothing until the visitor has given the consent the account waits\n * for, and visits only count from hosts on the account's list -- an empty list\n * counts nothing. `publicKey` is public by design; it is in the page source of\n * every tracked site.\n */\nexport function CrmSiteTracking(props: {\n publicKey: string;\n appOrigin?: string;\n}) {\n const origin = useEmbedOrigin(props.appOrigin);\n\n return (\n <script\n async\n src={`${origin}/api/site-tracking/script?k=${encodeURIComponent(props.publicKey)}`}\n />\n );\n}\n"],"mappings":"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","names":["import_jsx_runtime","import_jsx_runtime","import_react","import_jsx_runtime","import_jsx_runtime"]}
package/dist/react.js ADDED
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+
3
+ // src/react/embed-origin.tsx
4
+ import { createContext, useContext } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ var EmbedOriginContext = createContext(null);
7
+ function CrmEmbedProvider(props) {
8
+ return /* @__PURE__ */ jsx(EmbedOriginContext.Provider, { value: normaliseOrigin(props.appOrigin), children: props.children });
9
+ }
10
+ function useEmbedOrigin(appOrigin) {
11
+ const fromProvider = useContext(EmbedOriginContext);
12
+ const origin = appOrigin ? normaliseOrigin(appOrigin) : fromProvider;
13
+ if (!origin) {
14
+ throw new Error(
15
+ 'crm-client/react: no app origin. Pass `appOrigin` to the component or wrap the page in <CrmEmbedProvider appOrigin="https://app.example.com">. Never the page\u2019s own origin: that is your site, not the app.'
16
+ );
17
+ }
18
+ return origin;
19
+ }
20
+ function normaliseOrigin(value) {
21
+ return value.trim().replace(/\/+$/, "");
22
+ }
23
+
24
+ // src/react/crm-booking.tsx
25
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
26
+ function CrmBooking(props) {
27
+ const origin = useEmbedOrigin(props.appOrigin);
28
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
29
+ /* @__PURE__ */ jsx2(
30
+ "iframe",
31
+ {
32
+ src: `${props.url}?embed=1`,
33
+ "data-service-booking-embed": "",
34
+ title: props.title,
35
+ className: props.className,
36
+ style: { width: "100%", border: 0 }
37
+ }
38
+ ),
39
+ /* @__PURE__ */ jsx2("script", { async: true, src: `${origin}/booking-embed.js` })
40
+ ] });
41
+ }
42
+
43
+ // src/react/crm-chat-widget.tsx
44
+ import { jsx as jsx3 } from "react/jsx-runtime";
45
+ function CrmChatWidget(props) {
46
+ const origin = useEmbedOrigin(props.appOrigin);
47
+ const hideOn = props.hideOnPaths ?? [];
48
+ return /* @__PURE__ */ jsx3(
49
+ "script",
50
+ {
51
+ async: true,
52
+ src: `${origin}/embed/v1/chat.js`,
53
+ "data-chat-widget-id": props.widgetId,
54
+ ...hideOn.length > 0 ? { "data-chat-hide-on": hideOn.join(",") } : {},
55
+ ...props.loadOnInteraction ? { "data-chat-load-on-interaction": "true" } : {}
56
+ }
57
+ );
58
+ }
59
+
60
+ // src/react/crm-form.tsx
61
+ import { useEffect, useRef } from "react";
62
+ import { jsx as jsx4 } from "react/jsx-runtime";
63
+ function CrmForm(props) {
64
+ const origin = useEmbedOrigin(props.appOrigin);
65
+ const frame = useRef(null);
66
+ useEffect(() => {
67
+ function onMessage(event) {
68
+ const element = frame.current;
69
+ if (!element || event.origin !== origin || event.source !== element.contentWindow) {
70
+ return;
71
+ }
72
+ const data = event.data;
73
+ if (data?.type === "crm-form:resize" && typeof data.height === "number") {
74
+ element.style.height = `${Math.max(0, data.height)}px`;
75
+ return;
76
+ }
77
+ if (data?.type === "crm-form:submit") {
78
+ redirectTo(data.redirectUrl);
79
+ }
80
+ }
81
+ window.addEventListener("message", onMessage);
82
+ return () => window.removeEventListener("message", onMessage);
83
+ }, [origin]);
84
+ return /* @__PURE__ */ jsx4(
85
+ "iframe",
86
+ {
87
+ ref: frame,
88
+ src: `${origin}/f/${encodeURIComponent(props.formId)}`,
89
+ title: props.title,
90
+ loading: "lazy",
91
+ className: props.className,
92
+ style: {
93
+ width: "100%",
94
+ border: 0,
95
+ display: "block",
96
+ minHeight: 200
97
+ }
98
+ }
99
+ );
100
+ }
101
+ function redirectTo(redirectUrl) {
102
+ if (typeof redirectUrl !== "string" || !redirectUrl) {
103
+ return;
104
+ }
105
+ try {
106
+ const url = new URL(redirectUrl);
107
+ if (url.protocol === "https:") {
108
+ window.top.location.href = url.href;
109
+ }
110
+ } catch {
111
+ }
112
+ }
113
+
114
+ // src/react/crm-site-tracking.tsx
115
+ import { jsx as jsx5 } from "react/jsx-runtime";
116
+ function CrmSiteTracking(props) {
117
+ const origin = useEmbedOrigin(props.appOrigin);
118
+ return /* @__PURE__ */ jsx5(
119
+ "script",
120
+ {
121
+ async: true,
122
+ src: `${origin}/api/site-tracking/script?k=${encodeURIComponent(props.publicKey)}`
123
+ }
124
+ );
125
+ }
126
+ export {
127
+ CrmBooking,
128
+ CrmChatWidget,
129
+ CrmEmbedProvider,
130
+ CrmForm,
131
+ CrmSiteTracking
132
+ };
133
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/embed-origin.tsx","../src/react/crm-booking.tsx","../src/react/crm-chat-widget.tsx","../src/react/crm-form.tsx","../src/react/crm-site-tracking.tsx"],"sourcesContent":["'use client';\n\nimport { type ReactNode, createContext, useContext } from 'react';\n\nconst EmbedOriginContext = createContext<string | null>(null);\n\n/**\n * The app's address, once for every component below it, so a site does not\n * repeat it on each form, booking page and chat. The same origin as the\n * client's `baseUrl`: `https://app.example.com`, without a path.\n */\nexport function CrmEmbedProvider(props: {\n appOrigin: string;\n children: ReactNode;\n}) {\n return (\n <EmbedOriginContext.Provider value={normaliseOrigin(props.appOrigin)}>\n {props.children}\n </EmbedOriginContext.Provider>\n );\n}\n\n/** The origin from the prop, else from the provider; refuses to guess one. */\nexport function useEmbedOrigin(appOrigin: string | undefined) {\n const fromProvider = useContext(EmbedOriginContext);\n const origin = appOrigin ? normaliseOrigin(appOrigin) : fromProvider;\n\n if (!origin) {\n throw new Error(\n 'crm-client/react: no app origin. Pass `appOrigin` to the component or ' +\n 'wrap the page in <CrmEmbedProvider appOrigin=\"https://app.example.com\">. ' +\n 'Never the page’s own origin: that is your site, not the app.',\n );\n }\n\n return origin;\n}\n\nfunction normaliseOrigin(value: string) {\n return value.trim().replace(/\\/+$/, '');\n}\n","import { useEmbedOrigin } from './embed-origin';\n\n/**\n * A booking page, embedded: the iframe the \"Share booking\" dialog hands out,\n * plus the script that makes it as tall as what is inside it.\n *\n * `url` is a booking page's address as the API lists it\n * (`getBookingPages()`), and it must be one of the app's own: the script only\n * listens to frames on that origin. React 19 loads the script once per page,\n * however many of these are on it.\n */\nexport function CrmBooking(props: {\n url: string;\n /** The frame's accessible name, such as the service's name. */\n title: string;\n appOrigin?: string;\n className?: string;\n}) {\n const origin = useEmbedOrigin(props.appOrigin);\n\n return (\n <>\n <iframe\n src={`${props.url}?embed=1`}\n data-service-booking-embed=\"\"\n title={props.title}\n className={props.className}\n style={{ width: '100%', border: 0 }}\n />\n <script async src={`${origin}/booking-embed.js`} />\n </>\n );\n}\n","import { useEmbedOrigin } from './embed-origin';\n\n/**\n * The chat widget of the account, in the corner of every page it is rendered\n * on. Put it in the layout, once: the chat script reads its settings off its\n * own tag and runs once per page.\n *\n * Only a published widget answers, and only on a site in its list of allowed\n * origins (an empty list allows any site). `hideOnPaths` and\n * `loadOnInteraction` are the two switches the Embed tab puts on the tag; pass\n * what `getChatWidgets()` shows in its snippet, or leave them out.\n */\nexport function CrmChatWidget(props: {\n widgetId: string;\n appOrigin?: string;\n hideOnPaths?: readonly string[];\n loadOnInteraction?: boolean;\n}) {\n const origin = useEmbedOrigin(props.appOrigin);\n const hideOn = props.hideOnPaths ?? [];\n\n return (\n <script\n async\n src={`${origin}/embed/v1/chat.js`}\n data-chat-widget-id={props.widgetId}\n {...(hideOn.length > 0 ? { 'data-chat-hide-on': hideOn.join(',') } : {})}\n {...(props.loadOnInteraction\n ? { 'data-chat-load-on-interaction': 'true' }\n : {})}\n />\n );\n}\n","'use client';\n\nimport { useEffect, useRef } from 'react';\n\nimport { useEmbedOrigin } from './embed-origin';\n\n/**\n * A published form, drawn in place.\n *\n * The iframe is drawn here rather than left to `embed/v1/embed.js`, because\n * that loader looks for forms once, when it loads: a form that appears after a\n * client-side navigation would stay an empty box. What the loader does besides\n * drawing the frame is honoured here, on the same contract:\n *\n * - `crm-form:resize` sets the frame's height, so it never scrolls inside;\n * - `crm-form:submit` with an `https://` `redirectUrl` sends the whole page\n * there, and nothing else ever navigates it.\n *\n * Both are trusted only from the app's own origin *and* from this frame's own\n * window: another frame on the page can send the same shape.\n */\nexport function CrmForm(props: {\n formId: string;\n appOrigin?: string;\n /**\n * The frame's accessible name, in your site's own language: what a screen\n * reader announces before the form. Required, because this package has no\n * language of its own to fall back on.\n */\n title: string;\n className?: string;\n}) {\n const origin = useEmbedOrigin(props.appOrigin);\n const frame = useRef<HTMLIFrameElement>(null);\n\n // An effect because the page's message channel is outside React: this\n // subscribes to it for as long as the frame is on the page, and lets go.\n useEffect(() => {\n function onMessage(event: MessageEvent) {\n const element = frame.current;\n\n if (\n !element ||\n event.origin !== origin ||\n event.source !== element.contentWindow\n ) {\n return;\n }\n\n const data = event.data as {\n type?: unknown;\n height?: unknown;\n redirectUrl?: unknown;\n } | null;\n\n if (data?.type === 'crm-form:resize' && typeof data.height === 'number') {\n element.style.height = `${Math.max(0, data.height)}px`;\n return;\n }\n\n if (data?.type === 'crm-form:submit') {\n redirectTo(data.redirectUrl);\n }\n }\n\n window.addEventListener('message', onMessage);\n\n return () => window.removeEventListener('message', onMessage);\n }, [origin]);\n\n return (\n <iframe\n ref={frame}\n src={`${origin}/f/${encodeURIComponent(props.formId)}`}\n title={props.title}\n loading=\"lazy\"\n className={props.className}\n style={{\n width: '100%',\n border: 0,\n display: 'block',\n minHeight: 200,\n }}\n />\n );\n}\n\nfunction redirectTo(redirectUrl: unknown) {\n if (typeof redirectUrl !== 'string' || !redirectUrl) {\n return;\n }\n\n try {\n const url = new URL(redirectUrl);\n\n if (url.protocol === 'https:') {\n window.top!.location.href = url.href;\n }\n } catch {\n // A malformed address is ignored; the page never navigates on one.\n }\n}\n","import { useEmbedOrigin } from './embed-origin';\n\n/**\n * The website tracking script. Put it in the layout, once.\n *\n * It does nothing until the visitor has given the consent the account waits\n * for, and visits only count from hosts on the account's list -- an empty list\n * counts nothing. `publicKey` is public by design; it is in the page source of\n * every tracked site.\n */\nexport function CrmSiteTracking(props: {\n publicKey: string;\n appOrigin?: string;\n}) {\n const origin = useEmbedOrigin(props.appOrigin);\n\n return (\n <script\n async\n src={`${origin}/api/site-tracking/script?k=${encodeURIComponent(props.publicKey)}`}\n />\n );\n}\n"],"mappings":"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","names":["jsx","jsx","jsx","jsx"]}
package/dist/tags.d.ts CHANGED
@@ -2,6 +2,17 @@
2
2
  export declare function productsTag(): string;
3
3
  /** One product, by whichever key it was fetched with. */
4
4
  export declare function productTag(slugOrId: string): string;
5
+ /**
6
+ * Everything that lists categories, and every category page.
7
+ *
8
+ * A category delivery clears both this and the products collection, because a
9
+ * category page *is* a list of products: renaming a category, publishing one,
10
+ * or moving a product into one all change what a product listing filtered by
11
+ * that category answers.
12
+ */
13
+ export declare function categoriesTag(): string;
14
+ /** One category, by whichever key it was fetched with. */
15
+ export declare function categoryTag(slugOrId: string): string;
5
16
  export declare function eventsTag(): string;
6
17
  export declare function eventTag(idOrSlug: string): string;
7
18
  /**
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { ApiErrorCode, PublicEventDetailItem, PublicEventListItem, PublicProductDetail, PublicProductImage, PublicProductListItem } from './generated/api-types';
2
- export type { ApiErrorCode, ApiScope, ApiWebhookEvent, ApiWebhookPayload, PublicProductCategory, PublicProductPrice, PublicProductSummary, } from './generated/api-types';
3
- export { ALL_API_ERROR_CODES, ALL_API_SCOPES, ALL_API_WEBHOOK_EVENTS, } from './generated/api-types';
1
+ import type { ApiErrorCode, PublicCategoryDetail, PublicCategoryListItem, PublicContentText, PublicCta, PublicEventDetailItem, PublicEventListItem, PublicProductDetail, PublicProductField, PublicProductImage, PublicProductListItem, PublicProductVariant, PublicRepeater } from './generated/api-types';
2
+ import type { PlacementChatWidget, PlacementForm, PlacementSiteTracking } from './generated/api-types';
3
+ export type { ApiErrorCode, ApiScope, PublicContentFile, PublicContentIcon, PublicContentLink, PublicContentText, PublicCta, PublicRepeater, PublicRepeaterItem, PublicRepeaterItemField, ApiWebhookEvent, ApiWebhookPayload, ProductType, PublicCategoryStep, PublicProductCategory, PublicProductPrice, PublicProductSummary, PublicProductVariantAxis, PlacementBookingPage, PlacementChatWidget, PlacementForm, PlacementSiteTracking, } from './generated/api-types';
4
+ export { ALL_API_ERROR_CODES, ALL_API_SCOPES, ALL_API_WEBHOOK_EVENTS, ALL_PRODUCT_TYPES, } from './generated/api-types';
4
5
  /**
5
6
  * The difference between what the projection holds and what goes over the wire.
6
7
  *
@@ -15,13 +16,36 @@ export interface ProductListItem extends Omit<PublicProductListItem, 'imagePath'
15
16
  /** Ready to put in an `<img>`; null when the seller uploaded no picture. */
16
17
  imageUrl: string | null;
17
18
  }
18
- export interface ProductDetail extends Omit<PublicProductDetail, 'imagePath' | 'images'> {
19
+ export interface ProductDetail extends Omit<PublicProductDetail, 'imagePath' | 'images' | 'variants'> {
19
20
  imageUrl: string | null;
20
21
  images: ProductImage[];
22
+ variants: ProductVariant[];
23
+ }
24
+ export interface ProductVariant extends Omit<PublicProductVariant, 'images'> {
25
+ images: ProductImage[];
21
26
  }
22
27
  export interface ProductImage extends Omit<PublicProductImage, 'path'> {
23
28
  url: string;
24
29
  }
30
+ export interface CategoryListItem extends Omit<PublicCategoryListItem, 'imagePath'> {
31
+ /** Ready to put in an `<img>`; null when the seller uploaded no picture. */
32
+ imageUrl: string | null;
33
+ }
34
+ /**
35
+ * One category page.
36
+ *
37
+ * `social` is subtracted and restated for the same reason the images are: the
38
+ * service holds a storage path and the route hands over a finished URL.
39
+ */
40
+ export interface CategoryDetail extends Omit<PublicCategoryDetail, 'imagePath' | 'social' | 'products'> {
41
+ imageUrl: string | null;
42
+ social: {
43
+ title: string;
44
+ description: string | null;
45
+ imageUrl: string | null;
46
+ };
47
+ products: ProductListItem[];
48
+ }
25
49
  export interface EventListItem extends Omit<PublicEventListItem, 'coverImagePath'> {
26
50
  coverImageUrl: string | null;
27
51
  }
@@ -36,6 +60,22 @@ export interface ProductList {
36
60
  /** Null on the last page. Pass it back as `cursor` for the one after this. */
37
61
  nextCursor: string | null;
38
62
  }
63
+ export interface CategoryList {
64
+ data: CategoryListItem[];
65
+ /** Null on the last page. Pass it back as `cursor` for the one after this. */
66
+ nextCursor: string | null;
67
+ }
68
+ /**
69
+ * One category page and the first page of its products.
70
+ *
71
+ * The cursor is beside the category rather than inside it, because it pages the
72
+ * products and not the category: a shop asking for the next page sends it back
73
+ * on the same address and gets the same category with the next lot on it.
74
+ */
75
+ export interface CategoryPage {
76
+ data: CategoryDetail;
77
+ nextCursor: string | null;
78
+ }
39
79
  /**
40
80
  * The events listing, which does not have the shape of the others.
41
81
  *
@@ -132,6 +172,14 @@ export interface ContactInput {
132
172
  lastName?: string;
133
173
  phone?: string;
134
174
  company?: string;
175
+ /**
176
+ * The slug of the account's object for people a new contact is made in
177
+ * ("leden"). Somebody already known stays where they are. Left out, the
178
+ * account's own choice applies. A slug the account does not have, or one
179
+ * that is not an object for people, is refused with 422
180
+ * (`invalid_request`, `fields[0].path === 'object'`).
181
+ */
182
+ object?: string;
135
183
  }
136
184
  export interface ContactResult {
137
185
  id: string;
@@ -139,7 +187,8 @@ export interface ContactResult {
139
187
  created: boolean;
140
188
  }
141
189
  export interface RequestInput {
142
- contact: ContactInput;
190
+ /** Where a request's contact lands is the account's choice alone. */
191
+ contact: Omit<ContactInput, 'object'>;
143
192
  message: string;
144
193
  productId?: string;
145
194
  source?: string;
@@ -170,3 +219,182 @@ export interface ApiErrorBody {
170
219
  }[];
171
220
  };
172
221
  }
222
+ /**
223
+ * One price of a product written through the catalogue routes. `externalRef`
224
+ * keeps a renamed price itself; without one, a price is recognised by its
225
+ * label.
226
+ */
227
+ export interface CatalogPriceInput {
228
+ externalRef?: string;
229
+ label: string;
230
+ amountCents: number;
231
+ compareAtCents?: number | null;
232
+ kind?: 'one_off' | 'recurring';
233
+ recurringInterval?: 'day' | 'week' | 'month' | 'year';
234
+ recurringIntervalCount?: number;
235
+ isDefault?: boolean;
236
+ active?: boolean;
237
+ }
238
+ export interface CatalogVariantInput {
239
+ externalRef: string;
240
+ /** The value per axis, keyed by the axis slug. */
241
+ options: Record<string, string>;
242
+ sku?: string | null;
243
+ gtin?: string | null;
244
+ prices?: CatalogPriceInput[];
245
+ position?: number;
246
+ active?: boolean;
247
+ }
248
+ /**
249
+ * A product as a catalogue sync sends it. Every field but `source` may be
250
+ * left out, and left out means "leave it as it is"; `null` empties a field.
251
+ */
252
+ export interface CatalogProductInput {
253
+ /** Which system the references belong to, e.g. "coolflames-payload". */
254
+ source: string;
255
+ name?: string;
256
+ slug?: string | null;
257
+ description?: string | null;
258
+ longDescription?: string | null;
259
+ sku?: string | null;
260
+ gtin?: string | null;
261
+ productType?: 'digital' | 'physical';
262
+ /** A new product is a `draft` unless this says otherwise. */
263
+ availability?: 'draft' | 'internal' | 'scheduled' | 'public';
264
+ publishedAt?: string | null;
265
+ visibility?: 'public' | 'unlisted';
266
+ metaTitle?: string | null;
267
+ metaDescription?: string | null;
268
+ /** Category slugs; replaces the product's categories. */
269
+ categories?: string[];
270
+ createMissingCategories?: boolean;
271
+ /** Field values by field slug. */
272
+ fields?: Record<string, unknown>;
273
+ publicFields?: string[];
274
+ addMissingOptions?: boolean;
275
+ /** Replaces the product's prices as a whole. */
276
+ prices?: CatalogPriceInput[];
277
+ /** https only; fetched after the answer, in this order. */
278
+ images?: {
279
+ url: string;
280
+ alt?: string | null;
281
+ }[];
282
+ variantAxes?: {
283
+ slug: string;
284
+ label?: string;
285
+ }[];
286
+ /** Replaces the product's variants; one left out is archived. */
287
+ variants?: CatalogVariantInput[];
288
+ /** `false` brings back a product the API archived. */
289
+ archived?: false;
290
+ }
291
+ export interface CatalogProductResult {
292
+ id: string;
293
+ source: string;
294
+ externalRef: string;
295
+ /** False when the reference named a product that was already here. */
296
+ created: boolean;
297
+ name: string;
298
+ slug: string | null;
299
+ availability: 'draft' | 'internal' | 'scheduled' | 'public';
300
+ variants: {
301
+ externalRef: string;
302
+ id: string;
303
+ created: boolean;
304
+ }[];
305
+ /** `pending` while pictures are being fetched after the answer. */
306
+ images: 'pending' | 'unchanged';
307
+ }
308
+ export interface CatalogArchiveResult {
309
+ id: string;
310
+ archived: true;
311
+ alreadyArchived: boolean;
312
+ }
313
+ export interface CatalogCategoryInput {
314
+ name?: string;
315
+ description?: string | null;
316
+ /** `null` puts the category at the top of the tree. */
317
+ parentSlug?: string | null;
318
+ availability?: 'draft' | 'internal' | 'scheduled' | 'public';
319
+ publishedAt?: string | null;
320
+ metaTitle?: string | null;
321
+ metaDescription?: string | null;
322
+ }
323
+ export interface CatalogCategoryResult {
324
+ id: string;
325
+ slug: string;
326
+ name: string;
327
+ parentId: string | null;
328
+ created: boolean;
329
+ }
330
+ export interface CatalogFieldInput {
331
+ label?: string;
332
+ type?: 'text' | 'number' | 'select' | 'checkbox' | 'url' | 'date';
333
+ multiple?: boolean;
334
+ options?: {
335
+ value: string;
336
+ label?: string;
337
+ }[];
338
+ removeOptions?: string[];
339
+ public?: boolean;
340
+ }
341
+ /**
342
+ * A field of the catalogue, as `GET /attributes` lists the public ones. A
343
+ * `repeater` also says how it is drawn (`display`, `preset`) and what one
344
+ * entry holds (`itemFields`, `titleKey`).
345
+ */
346
+ export type CatalogField = PublicProductField;
347
+ /**
348
+ * What `fields[slug]` holds for a content field, by the `type` the field has in
349
+ * `GET /attributes`: `content_text`, `repeater` or `cta`. `null` when the block
350
+ * is published and empty. Narrow on the field's type, not on the value.
351
+ */
352
+ export type ContentFieldValue = PublicContentText | PublicRepeater | PublicCta;
353
+ export interface CatalogFieldResult extends CatalogField {
354
+ public: boolean;
355
+ created: boolean;
356
+ }
357
+ /** One page of published forms. Pass `nextCursor` back as `cursor` for the next. */
358
+ export interface FormList {
359
+ data: PlacementForm[];
360
+ nextCursor: string | null;
361
+ }
362
+ /** One page of chat widgets. Archived ones are left out. */
363
+ export interface ChatWidgetList {
364
+ data: PlacementChatWidget[];
365
+ nextCursor: string | null;
366
+ }
367
+ /**
368
+ * What may be said about a chat widget through the API. How it looks and what
369
+ * it says is done in the app's builder.
370
+ *
371
+ * Sites are added and removed, never replaced, so placing the chat on one more
372
+ * site cannot take it off the others. An empty list means any site; adding
373
+ * the first one narrows the widget to that site.
374
+ */
375
+ export interface ChatWidgetInput {
376
+ /** Required when the widget does not exist yet. */
377
+ name?: string;
378
+ status?: 'draft' | 'published';
379
+ /** Origins: scheme, host, optional port. `https://www.example.com`. */
380
+ addAllowedOrigins?: string[];
381
+ removeAllowedOrigins?: string[];
382
+ }
383
+ export interface ChatWidgetResult extends PlacementChatWidget {
384
+ created: boolean;
385
+ }
386
+ /**
387
+ * The tracking script's hosts and consent signal. Hosts are added and removed,
388
+ * never replaced. An empty list counts nothing: only visits from a listed host
389
+ * are kept.
390
+ */
391
+ export interface SiteTrackingInput {
392
+ /** Hosts, such as `www.example.com` or `*.example.com`. A full address is forgiven. */
393
+ addAllowedHosts?: string[];
394
+ removeAllowedHosts?: string[];
395
+ consentSignal?: PlacementSiteTracking['consentSignal'];
396
+ }
397
+ export interface SiteTrackingResult extends PlacementSiteTracking {
398
+ /** True when this call set tracking up for the account. */
399
+ created: boolean;
400
+ }