@solumflow-app/crm-client 0.1.0 → 0.2.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.
- package/README.md +146 -0
- package/dist/client.d.ts +55 -2
- package/dist/generated/api-types.d.ts +247 -35
- package/dist/index.cjs +188 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +186 -8
- package/dist/index.js.map +1 -1
- package/dist/react/crm-booking.d.ts +16 -0
- package/dist/react/crm-chat-widget.d.ts +16 -0
- package/dist/react/crm-form.d.ts +26 -0
- package/dist/react/crm-site-tracking.d.ts +12 -0
- package/dist/react/embed-origin.d.ts +12 -0
- package/dist/react/index.d.ts +20 -0
- package/dist/react.cjs +163 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.js +133 -0
- package/dist/react.js.map +1 -0
- package/dist/tags.d.ts +11 -0
- package/dist/types.d.ts +232 -5
- package/dist/webhooks.cjs +17 -5
- package/dist/webhooks.cjs.map +1 -1
- package/dist/webhooks.js +17 -5
- package/dist/webhooks.js.map +1 -1
- package/package.json +23 -7
|
@@ -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
|
-
|
|
3
|
-
export {
|
|
1
|
+
import type { ApiErrorCode, PublicCategoryDetail, PublicCategoryListItem, PublicEventDetailItem, PublicEventListItem, PublicProductDetail, PublicProductImage, PublicProductListItem, PublicProductVariant } from './generated/api-types';
|
|
2
|
+
import type { PlacementChatWidget, PlacementForm, PlacementSiteTracking } from './generated/api-types';
|
|
3
|
+
export type { ApiErrorCode, ApiScope, 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
|
|
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,181 @@ 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
|
+
/** A field of the catalogue, as `GET /attributes` lists the public ones. */
|
|
342
|
+
export interface CatalogField {
|
|
343
|
+
slug: string;
|
|
344
|
+
label: string;
|
|
345
|
+
type: string;
|
|
346
|
+
multiple: boolean;
|
|
347
|
+
options: {
|
|
348
|
+
value: string;
|
|
349
|
+
label: string;
|
|
350
|
+
}[];
|
|
351
|
+
}
|
|
352
|
+
export interface CatalogFieldResult extends CatalogField {
|
|
353
|
+
public: boolean;
|
|
354
|
+
created: boolean;
|
|
355
|
+
}
|
|
356
|
+
/** One page of published forms. Pass `nextCursor` back as `cursor` for the next. */
|
|
357
|
+
export interface FormList {
|
|
358
|
+
data: PlacementForm[];
|
|
359
|
+
nextCursor: string | null;
|
|
360
|
+
}
|
|
361
|
+
/** One page of chat widgets. Archived ones are left out. */
|
|
362
|
+
export interface ChatWidgetList {
|
|
363
|
+
data: PlacementChatWidget[];
|
|
364
|
+
nextCursor: string | null;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* What may be said about a chat widget through the API. How it looks and what
|
|
368
|
+
* it says is done in the app's builder.
|
|
369
|
+
*
|
|
370
|
+
* Sites are added and removed, never replaced, so placing the chat on one more
|
|
371
|
+
* site cannot take it off the others. An empty list means any site; adding
|
|
372
|
+
* the first one narrows the widget to that site.
|
|
373
|
+
*/
|
|
374
|
+
export interface ChatWidgetInput {
|
|
375
|
+
/** Required when the widget does not exist yet. */
|
|
376
|
+
name?: string;
|
|
377
|
+
status?: 'draft' | 'published';
|
|
378
|
+
/** Origins: scheme, host, optional port. `https://www.example.com`. */
|
|
379
|
+
addAllowedOrigins?: string[];
|
|
380
|
+
removeAllowedOrigins?: string[];
|
|
381
|
+
}
|
|
382
|
+
export interface ChatWidgetResult extends PlacementChatWidget {
|
|
383
|
+
created: boolean;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The tracking script's hosts and consent signal. Hosts are added and removed,
|
|
387
|
+
* never replaced. An empty list counts nothing: only visits from a listed host
|
|
388
|
+
* are kept.
|
|
389
|
+
*/
|
|
390
|
+
export interface SiteTrackingInput {
|
|
391
|
+
/** Hosts, such as `www.example.com` or `*.example.com`. A full address is forgiven. */
|
|
392
|
+
addAllowedHosts?: string[];
|
|
393
|
+
removeAllowedHosts?: string[];
|
|
394
|
+
consentSignal?: PlacementSiteTracking['consentSignal'];
|
|
395
|
+
}
|
|
396
|
+
export interface SiteTrackingResult extends PlacementSiteTracking {
|
|
397
|
+
/** True when this call set tracking up for the account. */
|
|
398
|
+
created: boolean;
|
|
399
|
+
}
|