@stromcom/react-snippet 0.1.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/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/index.cjs +222 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +191 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
- package/src/StromcomConf.jsx +95 -0
- package/src/StromcomHome.jsx +26 -0
- package/src/StromcomProvider.jsx +105 -0
- package/src/StromcomThread.jsx +45 -0
- package/src/StromcomUser.jsx +41 -0
- package/src/__tests__/StromcomConf.test.jsx +52 -0
- package/src/__tests__/StromcomHome.test.jsx +57 -0
- package/src/__tests__/StromcomProvider.test.jsx +137 -0
- package/src/__tests__/StromcomThread.test.jsx +75 -0
- package/src/__tests__/StromcomUser.test.jsx +63 -0
- package/src/index.d.ts +85 -0
- package/src/index.js +5 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// src/StromcomProvider.jsx
|
|
2
|
+
import { createContext, useContext, useEffect } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
var StromcomContext = createContext(null);
|
|
5
|
+
var LOADER_URLS = {
|
|
6
|
+
production: "https://cdn.stromcom.cz/loader.js",
|
|
7
|
+
staging: "https://cdn.staging.stromcom.cz/loader.js"
|
|
8
|
+
};
|
|
9
|
+
function setupLayer(dataLayer) {
|
|
10
|
+
var _a;
|
|
11
|
+
if (typeof window === "undefined") {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if ((_a = window[dataLayer]) == null ? void 0 : _a.__sc_ready) {
|
|
15
|
+
return window[dataLayer];
|
|
16
|
+
}
|
|
17
|
+
const dl = dataLayer + "DL";
|
|
18
|
+
window[dl] = window[dl] || {};
|
|
19
|
+
window[dataLayer] = window[dataLayer] || {};
|
|
20
|
+
const layer = window[dataLayer];
|
|
21
|
+
const stub = (method, dlKey, isArray) => {
|
|
22
|
+
dlKey = dlKey || method;
|
|
23
|
+
if (isArray) {
|
|
24
|
+
window[dl][dlKey] = window[dl][dlKey] || [];
|
|
25
|
+
}
|
|
26
|
+
layer[method] = (...args) => {
|
|
27
|
+
if (isArray) {
|
|
28
|
+
window[dl][dlKey].push(args);
|
|
29
|
+
} else {
|
|
30
|
+
window[dl][dlKey] = args;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
stub("initUser", "user");
|
|
35
|
+
stub("thread", "threads", true);
|
|
36
|
+
stub("conf");
|
|
37
|
+
stub("home", "home", true);
|
|
38
|
+
layer.__sc_ready = true;
|
|
39
|
+
return layer;
|
|
40
|
+
}
|
|
41
|
+
function StromcomProvider({
|
|
42
|
+
clientKey,
|
|
43
|
+
clientSecret,
|
|
44
|
+
dataLayer = "stromCom",
|
|
45
|
+
environment = "production",
|
|
46
|
+
children
|
|
47
|
+
}) {
|
|
48
|
+
const layer = setupLayer(dataLayer);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
var _a;
|
|
51
|
+
if (typeof window === "undefined") return;
|
|
52
|
+
if (document.querySelector(`script[data-ck="${clientKey}"][data-type="stromcom"]`)) return;
|
|
53
|
+
const base = (_a = LOADER_URLS[environment]) != null ? _a : environment;
|
|
54
|
+
const dl = dataLayer + "DL";
|
|
55
|
+
const script = document.createElement("script");
|
|
56
|
+
script.async = true;
|
|
57
|
+
script.dataset.type = "stromcom";
|
|
58
|
+
script.dataset.l = dataLayer;
|
|
59
|
+
script.dataset.dl = dl;
|
|
60
|
+
script.dataset.ck = clientKey;
|
|
61
|
+
script.dataset.cs = clientSecret;
|
|
62
|
+
script.src = `${base}?${clientKey}`;
|
|
63
|
+
document.head.appendChild(script);
|
|
64
|
+
return () => script.remove();
|
|
65
|
+
}, [clientKey, clientSecret, dataLayer, environment]);
|
|
66
|
+
return /* @__PURE__ */ jsx(StromcomContext.Provider, { value: layer, children });
|
|
67
|
+
}
|
|
68
|
+
function useStromcom() {
|
|
69
|
+
return useContext(StromcomContext);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/StromcomUser.jsx
|
|
73
|
+
import { useEffect as useEffect2 } from "react";
|
|
74
|
+
function StromcomUser({ code, name, emailAddress, readOnly, avatarURL }) {
|
|
75
|
+
const sc = useStromcom();
|
|
76
|
+
useEffect2(() => {
|
|
77
|
+
if (!sc) return;
|
|
78
|
+
const opts = { code };
|
|
79
|
+
if (name !== void 0) opts.name = name;
|
|
80
|
+
if (emailAddress !== void 0) opts.emailAddress = emailAddress;
|
|
81
|
+
if (readOnly !== void 0) opts.readOnly = readOnly;
|
|
82
|
+
if (avatarURL !== void 0) opts.avatarURL = avatarURL;
|
|
83
|
+
sc.initUser(opts);
|
|
84
|
+
}, [sc, code, name, emailAddress, readOnly, avatarURL]);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/StromcomConf.jsx
|
|
89
|
+
import { useEffect as useEffect3 } from "react";
|
|
90
|
+
function StromcomConf({
|
|
91
|
+
notificationRenderer,
|
|
92
|
+
onNotification,
|
|
93
|
+
pageCSSPath,
|
|
94
|
+
notificationElementTargetElement,
|
|
95
|
+
notificationElementShowAlways,
|
|
96
|
+
notificationElementPosition,
|
|
97
|
+
notificationElementStyles,
|
|
98
|
+
notificationElementCSSPath,
|
|
99
|
+
notificationElementNoShadowRoot,
|
|
100
|
+
homeBeforeRender,
|
|
101
|
+
notificationElementBeforeRender,
|
|
102
|
+
notificationElementAfterRender,
|
|
103
|
+
theme
|
|
104
|
+
}) {
|
|
105
|
+
const sc = useStromcom();
|
|
106
|
+
useEffect3(() => {
|
|
107
|
+
if (!sc) return;
|
|
108
|
+
const opts = {};
|
|
109
|
+
if (notificationRenderer !== void 0) opts.notificationRenderer = notificationRenderer;
|
|
110
|
+
if (onNotification !== void 0) opts.onNotification = onNotification;
|
|
111
|
+
if (pageCSSPath !== void 0) opts.pageCSSPath = pageCSSPath;
|
|
112
|
+
if (notificationElementTargetElement !== void 0)
|
|
113
|
+
opts.notificationElementTargetElement = notificationElementTargetElement;
|
|
114
|
+
if (notificationElementShowAlways !== void 0)
|
|
115
|
+
opts.notificationElementShowAlways = notificationElementShowAlways;
|
|
116
|
+
if (notificationElementPosition !== void 0)
|
|
117
|
+
opts.notificationElementPosition = notificationElementPosition;
|
|
118
|
+
if (notificationElementStyles !== void 0)
|
|
119
|
+
opts.notificationElementStyles = notificationElementStyles;
|
|
120
|
+
if (notificationElementCSSPath !== void 0)
|
|
121
|
+
opts.notificationElementCSSPath = notificationElementCSSPath;
|
|
122
|
+
if (notificationElementNoShadowRoot !== void 0)
|
|
123
|
+
opts.notificationElementNoShadowRoot = notificationElementNoShadowRoot;
|
|
124
|
+
if (homeBeforeRender !== void 0) opts.homeBeforeRender = homeBeforeRender;
|
|
125
|
+
if (notificationElementBeforeRender !== void 0)
|
|
126
|
+
opts.notificationElementBeforeRender = notificationElementBeforeRender;
|
|
127
|
+
if (notificationElementAfterRender !== void 0)
|
|
128
|
+
opts.notificationElementAfterRender = notificationElementAfterRender;
|
|
129
|
+
if (theme !== void 0) opts.theme = theme;
|
|
130
|
+
sc.conf(opts);
|
|
131
|
+
}, [
|
|
132
|
+
sc,
|
|
133
|
+
notificationRenderer,
|
|
134
|
+
onNotification,
|
|
135
|
+
pageCSSPath,
|
|
136
|
+
notificationElementTargetElement,
|
|
137
|
+
notificationElementShowAlways,
|
|
138
|
+
notificationElementPosition,
|
|
139
|
+
notificationElementStyles,
|
|
140
|
+
notificationElementCSSPath,
|
|
141
|
+
notificationElementNoShadowRoot,
|
|
142
|
+
homeBeforeRender,
|
|
143
|
+
notificationElementBeforeRender,
|
|
144
|
+
notificationElementAfterRender,
|
|
145
|
+
theme
|
|
146
|
+
]);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/StromcomThread.jsx
|
|
151
|
+
import { useEffect as useEffect4, useRef } from "react";
|
|
152
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
153
|
+
function StromcomThread({ code, name, url, userHint, className, style, ...rest }) {
|
|
154
|
+
const sc = useStromcom();
|
|
155
|
+
const containerRef = useRef(null);
|
|
156
|
+
const initializedRef = useRef(false);
|
|
157
|
+
useEffect4(() => {
|
|
158
|
+
if (!sc || !containerRef.current || initializedRef.current) return;
|
|
159
|
+
const opts = { code };
|
|
160
|
+
if (name !== void 0) opts.name = name;
|
|
161
|
+
if (url !== void 0) opts.url = url;
|
|
162
|
+
if (userHint !== void 0) opts.userHint = userHint;
|
|
163
|
+
sc.thread(containerRef.current, opts);
|
|
164
|
+
initializedRef.current = true;
|
|
165
|
+
}, [sc]);
|
|
166
|
+
return /* @__PURE__ */ jsx2("div", { ref: containerRef, className, style, ...rest });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/StromcomHome.jsx
|
|
170
|
+
import { useEffect as useEffect5, useRef as useRef2 } from "react";
|
|
171
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
172
|
+
function StromcomHome({ className, style, ...rest }) {
|
|
173
|
+
const sc = useStromcom();
|
|
174
|
+
const containerRef = useRef2(null);
|
|
175
|
+
const initializedRef = useRef2(false);
|
|
176
|
+
useEffect5(() => {
|
|
177
|
+
if (!sc || !containerRef.current || initializedRef.current) return;
|
|
178
|
+
sc.home(containerRef.current);
|
|
179
|
+
initializedRef.current = true;
|
|
180
|
+
}, [sc]);
|
|
181
|
+
return /* @__PURE__ */ jsx3("div", { ref: containerRef, className, style, ...rest });
|
|
182
|
+
}
|
|
183
|
+
export {
|
|
184
|
+
StromcomConf,
|
|
185
|
+
StromcomHome,
|
|
186
|
+
StromcomProvider,
|
|
187
|
+
StromcomThread,
|
|
188
|
+
StromcomUser,
|
|
189
|
+
useStromcom
|
|
190
|
+
};
|
|
191
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/StromcomProvider.jsx","../src/StromcomUser.jsx","../src/StromcomConf.jsx","../src/StromcomThread.jsx","../src/StromcomHome.jsx"],"sourcesContent":["import { createContext, useContext, useEffect } from 'react';\n\nexport const StromcomContext = createContext(null);\n\nconst LOADER_URLS = {\n production: 'https://cdn.stromcom.cz/loader.js',\n staging: 'https://cdn.staging.stromcom.cz/loader.js',\n};\n\n/**\n * Sets up the stromCom stub layer synchronously so that child components can\n * queue calls (initUser, thread, conf, home) before the async loader.js arrives.\n * Mirrors what the PHP snippet() method generates.\n */\nfunction setupLayer(dataLayer) {\n if (typeof window === 'undefined') {\n return null;\n }\n\n if (window[dataLayer]?.__sc_ready) {\n return window[dataLayer];\n }\n\n const dl = dataLayer + 'DL';\n window[dl] = window[dl] || {};\n window[dataLayer] = window[dataLayer] || {};\n const layer = window[dataLayer];\n\n const stub = (method, dlKey, isArray) => {\n dlKey = dlKey || method;\n if (isArray) {\n window[dl][dlKey] = window[dl][dlKey] || [];\n }\n\n layer[method] = (...args) => {\n if (isArray) {\n window[dl][dlKey].push(args);\n } else {\n window[dl][dlKey] = args;\n }\n };\n };\n\n stub('initUser', 'user');\n stub('thread', 'threads', true);\n stub('conf');\n stub('home', 'home', true);\n\n layer.__sc_ready = true;\n return layer;\n}\n\n/**\n * Wraps the app and loads the Stromcom widget SDK.\n *\n * @param {object} props\n * @param {string} props.clientKey - Client key from the Stromcom dashboard\n * @param {string} props.clientSecret - Bearer token from the Stromcom dashboard\n * @param {string} [props.dataLayer] - Global JS variable name (default: \"stromCom\")\n * @param {string} [props.environment] - \"production\" | \"staging\" | full custom loader URL\n * @param {React.ReactNode} props.children\n *\n * @example\n * <StromcomProvider clientKey=\"ck_xxx\" clientSecret=\"cs_xxx\">\n * <App />\n * </StromcomProvider>\n */\nexport function StromcomProvider({\n clientKey,\n clientSecret,\n dataLayer = 'stromCom',\n environment = 'production',\n children,\n}) {\n // Set up stubs synchronously so children can queue calls immediately on render.\n const layer = setupLayer(dataLayer);\n\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n // Avoid injecting the script twice (React StrictMode, HMR, etc.)\n if (document.querySelector(`script[data-ck=\"${clientKey}\"][data-type=\"stromcom\"]`)) return;\n\n const base = LOADER_URLS[environment] ?? environment;\n const dl = dataLayer + 'DL';\n\n const script = document.createElement('script');\n script.async = true;\n script.dataset.type = 'stromcom';\n script.dataset.l = dataLayer;\n script.dataset.dl = dl;\n script.dataset.ck = clientKey;\n script.dataset.cs = clientSecret;\n script.src = `${base}?${clientKey}`;\n document.head.appendChild(script);\n\n return () => script.remove();\n }, [clientKey, clientSecret, dataLayer, environment]);\n\n return <StromcomContext.Provider value={layer}>{children}</StromcomContext.Provider>;\n}\n\nexport function useStromcom() {\n return useContext(StromcomContext);\n}\n","import { useEffect } from 'react';\nimport { useStromcom } from './StromcomProvider.jsx';\n\n/**\n * Identifies the currently logged-in user.\n * Re-calls initUser whenever any prop changes.\n *\n * IMPORTANT: The `code` should already be hashed server-side (HMAC + base-62).\n * Never pass your raw internal user ID directly from the client.\n *\n * @param {object} props\n * @param {string} props.code - Hashed unique user identifier (max 100 chars, [a-zA-Z0-9-_])\n * @param {string} [props.name] - Display name\n * @param {string} [props.emailAddress]- Used for email notifications\n * @param {boolean} [props.readOnly] - When true, user can read but not send messages\n * @param {string} [props.avatarURL] - Full URL to avatar image\n *\n * @example\n * <StromcomUser\n * code={user.stromcomHash}\n * name={user.name}\n * emailAddress={user.email}\n * />\n */\nexport function StromcomUser({ code, name, emailAddress, readOnly, avatarURL }) {\n const sc = useStromcom();\n\n useEffect(() => {\n if (!sc) return;\n\n const opts = { code };\n if (name !== undefined) opts.name = name;\n if (emailAddress !== undefined) opts.emailAddress = emailAddress;\n if (readOnly !== undefined) opts.readOnly = readOnly;\n if (avatarURL !== undefined) opts.avatarURL = avatarURL;\n\n sc.initUser(opts);\n }, [sc, code, name, emailAddress, readOnly, avatarURL]);\n\n return null;\n}\n","import { useEffect } from 'react';\nimport { useStromcom } from './StromcomProvider.jsx';\n\n/**\n * Sends SDK configuration to the Stromcom widget.\n * Should be placed once inside <StromcomProvider>, before threads or the home component.\n * Re-sends conf whenever any option prop changes.\n *\n * Options that accept functions (notificationRenderer, onNotification, etc.) take\n * actual JS functions here — no need for the string-wrapping used in the PHP library.\n *\n * @param {object} [props.notificationRenderer] - Function(notification) for custom notification rendering\n * @param {object} [props.onNotification] - Function(count) called when unread count changes\n * @param {string} [props.pageCSSPath] - URL of CSS file injected into the widget iframe\n * @param {Function} [props.notificationElementTargetElement] - Function/Promise/Element returning the icon mount target\n * @param {boolean} [props.notificationElementShowAlways] - Show icon even with zero notifications\n * @param {number} [props.notificationElementPosition] - Icon position: 1=top-left 2=top-right 3=bottom-right 4=bottom-left\n * @param {object} [props.notificationElementStyles] - Inline styles object for the default notification button\n * @param {string} [props.notificationElementCSSPath] - CSS URL for the default notification renderer\n * @param {boolean} [props.notificationElementNoShadowRoot]- Disable shadow-root for notifications\n * @param {Function} [props.homeBeforeRender] - Callback before notification center opens\n * @param {Function} [props.notificationElementBeforeRender]- Callback before notification icon renders\n * @param {Function} [props.notificationElementAfterRender] - Callback after notification icon renders\n * @param {('stromcom-light'|'stromcom-dark'|null)} [props.theme] - Theme. null follows browser preference.\n *\n * @example\n * <StromcomConf\n * notificationElementPosition={4}\n * onNotification={(count) => setBadge(count)}\n * />\n */\nexport function StromcomConf({\n notificationRenderer,\n onNotification,\n pageCSSPath,\n notificationElementTargetElement,\n notificationElementShowAlways,\n notificationElementPosition,\n notificationElementStyles,\n notificationElementCSSPath,\n notificationElementNoShadowRoot,\n homeBeforeRender,\n notificationElementBeforeRender,\n notificationElementAfterRender,\n theme,\n}) {\n const sc = useStromcom();\n\n useEffect(() => {\n if (!sc) return;\n\n const opts = {};\n\n if (notificationRenderer !== undefined) opts.notificationRenderer = notificationRenderer;\n if (onNotification !== undefined) opts.onNotification = onNotification;\n if (pageCSSPath !== undefined) opts.pageCSSPath = pageCSSPath;\n if (notificationElementTargetElement !== undefined)\n opts.notificationElementTargetElement = notificationElementTargetElement;\n if (notificationElementShowAlways !== undefined)\n opts.notificationElementShowAlways = notificationElementShowAlways;\n if (notificationElementPosition !== undefined)\n opts.notificationElementPosition = notificationElementPosition;\n if (notificationElementStyles !== undefined)\n opts.notificationElementStyles = notificationElementStyles;\n if (notificationElementCSSPath !== undefined)\n opts.notificationElementCSSPath = notificationElementCSSPath;\n if (notificationElementNoShadowRoot !== undefined)\n opts.notificationElementNoShadowRoot = notificationElementNoShadowRoot;\n if (homeBeforeRender !== undefined) opts.homeBeforeRender = homeBeforeRender;\n if (notificationElementBeforeRender !== undefined)\n opts.notificationElementBeforeRender = notificationElementBeforeRender;\n if (notificationElementAfterRender !== undefined)\n opts.notificationElementAfterRender = notificationElementAfterRender;\n if (theme !== undefined) opts.theme = theme;\n\n sc.conf(opts);\n }, [\n sc,\n notificationRenderer,\n onNotification,\n pageCSSPath,\n notificationElementTargetElement,\n notificationElementShowAlways,\n notificationElementPosition,\n notificationElementStyles,\n notificationElementCSSPath,\n notificationElementNoShadowRoot,\n homeBeforeRender,\n notificationElementBeforeRender,\n notificationElementAfterRender,\n theme,\n ]);\n\n return null;\n}\n","import { useEffect, useRef } from 'react';\nimport { useStromcom } from './StromcomProvider.jsx';\n\n/**\n * Embeds a Stromcom conversation thread into the rendered div.\n * The thread is initialized once on mount.\n *\n * IMPORTANT: The `code` should already be hashed server-side (HMAC + base-62).\n *\n * @param {object} props\n * @param {string} props.code - Hashed unique thread identifier (max 100 chars, [a-zA-Z0-9-_])\n * @param {string} [props.name] - Display name shown in the thread header\n * @param {string} [props.url] - Canonical URL of the page (shown as link in header)\n * @param {boolean} [props.userHint] - Enable @mention suggestions (default: true)\n * @param {string} [props.className] - CSS class for the container div\n * @param {object} [props.style] - Inline styles for the container div\n *\n * @example\n * <StromcomThread\n * code={order.stromcomHash}\n * name={`Order #${order.id}`}\n * url={window.location.href}\n * style={{ height: '400px' }}\n * />\n */\nexport function StromcomThread({ code, name, url, userHint, className, style, ...rest }) {\n const sc = useStromcom();\n const containerRef = useRef(null);\n const initializedRef = useRef(false);\n\n useEffect(() => {\n if (!sc || !containerRef.current || initializedRef.current) return;\n\n const opts = { code };\n if (name !== undefined) opts.name = name;\n if (url !== undefined) opts.url = url;\n if (userHint !== undefined) opts.userHint = userHint;\n\n sc.thread(containerRef.current, opts);\n initializedRef.current = true;\n // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally omit option deps, thread cannot be re-initialized\n }, [sc]);\n\n return <div ref={containerRef} className={className} style={style} {...rest} />;\n}\n","import { useEffect, useRef } from 'react';\nimport { useStromcom } from './StromcomProvider.jsx';\n\n/**\n * Embeds the Stromcom notification center into the rendered div.\n * Initialized once on mount.\n *\n * @param {object} [props.className] - CSS class for the container div\n * @param {object} [props.style] - Inline styles for the container div\n *\n * @example\n * <StromcomHome style={{ position: 'fixed', bottom: 24, right: 24 }} />\n */\nexport function StromcomHome({ className, style, ...rest }) {\n const sc = useStromcom();\n const containerRef = useRef(null);\n const initializedRef = useRef(false);\n\n useEffect(() => {\n if (!sc || !containerRef.current || initializedRef.current) return;\n sc.home(containerRef.current);\n initializedRef.current = true;\n }, [sc]);\n\n return <div ref={containerRef} className={className} style={style} {...rest} />;\n}\n"],"mappings":"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","names":["useEffect","useEffect","useEffect","useEffect","useEffect","jsx","useEffect","useEffect","useRef","jsx","useRef","useEffect"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stromcom/react-snippet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React integration for the STROMCOM chat widget. Loads the SDK, identifies users, and embeds threads as plain React components.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"stromcom",
|
|
7
|
+
"react",
|
|
8
|
+
"chat",
|
|
9
|
+
"widget",
|
|
10
|
+
"snippet",
|
|
11
|
+
"messaging",
|
|
12
|
+
"support"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://www.stromcom.cz",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "STROMCOM",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/stromcom/react-snippet.git"
|
|
20
|
+
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"lint": "eslint .",
|
|
45
|
+
"format": "prettier --write .",
|
|
46
|
+
"format:check": "prettier --check .",
|
|
47
|
+
"prepublishOnly": "npm run build"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": ">=17.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@eslint/js": "^9.39.4",
|
|
54
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
55
|
+
"@testing-library/react": "^16.3.2",
|
|
56
|
+
"eslint": "^9.39.4",
|
|
57
|
+
"eslint-config-prettier": "^10.1.8",
|
|
58
|
+
"eslint-plugin-react": "^7.37.5",
|
|
59
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
60
|
+
"globals": "^17.7.0",
|
|
61
|
+
"jsdom": "^29.1.1",
|
|
62
|
+
"prettier": "^3.9.4",
|
|
63
|
+
"react": "^18.3.1",
|
|
64
|
+
"react-dom": "^18.3.1",
|
|
65
|
+
"tsup": "^8.3.5",
|
|
66
|
+
"typescript": "^5.6.3",
|
|
67
|
+
"vitest": "^4.1.9"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=16"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useStromcom } from './StromcomProvider.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sends SDK configuration to the Stromcom widget.
|
|
6
|
+
* Should be placed once inside <StromcomProvider>, before threads or the home component.
|
|
7
|
+
* Re-sends conf whenever any option prop changes.
|
|
8
|
+
*
|
|
9
|
+
* Options that accept functions (notificationRenderer, onNotification, etc.) take
|
|
10
|
+
* actual JS functions here — no need for the string-wrapping used in the PHP library.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} [props.notificationRenderer] - Function(notification) for custom notification rendering
|
|
13
|
+
* @param {object} [props.onNotification] - Function(count) called when unread count changes
|
|
14
|
+
* @param {string} [props.pageCSSPath] - URL of CSS file injected into the widget iframe
|
|
15
|
+
* @param {Function} [props.notificationElementTargetElement] - Function/Promise/Element returning the icon mount target
|
|
16
|
+
* @param {boolean} [props.notificationElementShowAlways] - Show icon even with zero notifications
|
|
17
|
+
* @param {number} [props.notificationElementPosition] - Icon position: 1=top-left 2=top-right 3=bottom-right 4=bottom-left
|
|
18
|
+
* @param {object} [props.notificationElementStyles] - Inline styles object for the default notification button
|
|
19
|
+
* @param {string} [props.notificationElementCSSPath] - CSS URL for the default notification renderer
|
|
20
|
+
* @param {boolean} [props.notificationElementNoShadowRoot]- Disable shadow-root for notifications
|
|
21
|
+
* @param {Function} [props.homeBeforeRender] - Callback before notification center opens
|
|
22
|
+
* @param {Function} [props.notificationElementBeforeRender]- Callback before notification icon renders
|
|
23
|
+
* @param {Function} [props.notificationElementAfterRender] - Callback after notification icon renders
|
|
24
|
+
* @param {('stromcom-light'|'stromcom-dark'|null)} [props.theme] - Theme. null follows browser preference.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <StromcomConf
|
|
28
|
+
* notificationElementPosition={4}
|
|
29
|
+
* onNotification={(count) => setBadge(count)}
|
|
30
|
+
* />
|
|
31
|
+
*/
|
|
32
|
+
export function StromcomConf({
|
|
33
|
+
notificationRenderer,
|
|
34
|
+
onNotification,
|
|
35
|
+
pageCSSPath,
|
|
36
|
+
notificationElementTargetElement,
|
|
37
|
+
notificationElementShowAlways,
|
|
38
|
+
notificationElementPosition,
|
|
39
|
+
notificationElementStyles,
|
|
40
|
+
notificationElementCSSPath,
|
|
41
|
+
notificationElementNoShadowRoot,
|
|
42
|
+
homeBeforeRender,
|
|
43
|
+
notificationElementBeforeRender,
|
|
44
|
+
notificationElementAfterRender,
|
|
45
|
+
theme,
|
|
46
|
+
}) {
|
|
47
|
+
const sc = useStromcom();
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!sc) return;
|
|
51
|
+
|
|
52
|
+
const opts = {};
|
|
53
|
+
|
|
54
|
+
if (notificationRenderer !== undefined) opts.notificationRenderer = notificationRenderer;
|
|
55
|
+
if (onNotification !== undefined) opts.onNotification = onNotification;
|
|
56
|
+
if (pageCSSPath !== undefined) opts.pageCSSPath = pageCSSPath;
|
|
57
|
+
if (notificationElementTargetElement !== undefined)
|
|
58
|
+
opts.notificationElementTargetElement = notificationElementTargetElement;
|
|
59
|
+
if (notificationElementShowAlways !== undefined)
|
|
60
|
+
opts.notificationElementShowAlways = notificationElementShowAlways;
|
|
61
|
+
if (notificationElementPosition !== undefined)
|
|
62
|
+
opts.notificationElementPosition = notificationElementPosition;
|
|
63
|
+
if (notificationElementStyles !== undefined)
|
|
64
|
+
opts.notificationElementStyles = notificationElementStyles;
|
|
65
|
+
if (notificationElementCSSPath !== undefined)
|
|
66
|
+
opts.notificationElementCSSPath = notificationElementCSSPath;
|
|
67
|
+
if (notificationElementNoShadowRoot !== undefined)
|
|
68
|
+
opts.notificationElementNoShadowRoot = notificationElementNoShadowRoot;
|
|
69
|
+
if (homeBeforeRender !== undefined) opts.homeBeforeRender = homeBeforeRender;
|
|
70
|
+
if (notificationElementBeforeRender !== undefined)
|
|
71
|
+
opts.notificationElementBeforeRender = notificationElementBeforeRender;
|
|
72
|
+
if (notificationElementAfterRender !== undefined)
|
|
73
|
+
opts.notificationElementAfterRender = notificationElementAfterRender;
|
|
74
|
+
if (theme !== undefined) opts.theme = theme;
|
|
75
|
+
|
|
76
|
+
sc.conf(opts);
|
|
77
|
+
}, [
|
|
78
|
+
sc,
|
|
79
|
+
notificationRenderer,
|
|
80
|
+
onNotification,
|
|
81
|
+
pageCSSPath,
|
|
82
|
+
notificationElementTargetElement,
|
|
83
|
+
notificationElementShowAlways,
|
|
84
|
+
notificationElementPosition,
|
|
85
|
+
notificationElementStyles,
|
|
86
|
+
notificationElementCSSPath,
|
|
87
|
+
notificationElementNoShadowRoot,
|
|
88
|
+
homeBeforeRender,
|
|
89
|
+
notificationElementBeforeRender,
|
|
90
|
+
notificationElementAfterRender,
|
|
91
|
+
theme,
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useStromcom } from './StromcomProvider.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Embeds the Stromcom notification center into the rendered div.
|
|
6
|
+
* Initialized once on mount.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} [props.className] - CSS class for the container div
|
|
9
|
+
* @param {object} [props.style] - Inline styles for the container div
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <StromcomHome style={{ position: 'fixed', bottom: 24, right: 24 }} />
|
|
13
|
+
*/
|
|
14
|
+
export function StromcomHome({ className, style, ...rest }) {
|
|
15
|
+
const sc = useStromcom();
|
|
16
|
+
const containerRef = useRef(null);
|
|
17
|
+
const initializedRef = useRef(false);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!sc || !containerRef.current || initializedRef.current) return;
|
|
21
|
+
sc.home(containerRef.current);
|
|
22
|
+
initializedRef.current = true;
|
|
23
|
+
}, [sc]);
|
|
24
|
+
|
|
25
|
+
return <div ref={containerRef} className={className} style={style} {...rest} />;
|
|
26
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export const StromcomContext = createContext(null);
|
|
4
|
+
|
|
5
|
+
const LOADER_URLS = {
|
|
6
|
+
production: 'https://cdn.stromcom.cz/loader.js',
|
|
7
|
+
staging: 'https://cdn.staging.stromcom.cz/loader.js',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sets up the stromCom stub layer synchronously so that child components can
|
|
12
|
+
* queue calls (initUser, thread, conf, home) before the async loader.js arrives.
|
|
13
|
+
* Mirrors what the PHP snippet() method generates.
|
|
14
|
+
*/
|
|
15
|
+
function setupLayer(dataLayer) {
|
|
16
|
+
if (typeof window === 'undefined') {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (window[dataLayer]?.__sc_ready) {
|
|
21
|
+
return window[dataLayer];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dl = dataLayer + 'DL';
|
|
25
|
+
window[dl] = window[dl] || {};
|
|
26
|
+
window[dataLayer] = window[dataLayer] || {};
|
|
27
|
+
const layer = window[dataLayer];
|
|
28
|
+
|
|
29
|
+
const stub = (method, dlKey, isArray) => {
|
|
30
|
+
dlKey = dlKey || method;
|
|
31
|
+
if (isArray) {
|
|
32
|
+
window[dl][dlKey] = window[dl][dlKey] || [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
layer[method] = (...args) => {
|
|
36
|
+
if (isArray) {
|
|
37
|
+
window[dl][dlKey].push(args);
|
|
38
|
+
} else {
|
|
39
|
+
window[dl][dlKey] = args;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
stub('initUser', 'user');
|
|
45
|
+
stub('thread', 'threads', true);
|
|
46
|
+
stub('conf');
|
|
47
|
+
stub('home', 'home', true);
|
|
48
|
+
|
|
49
|
+
layer.__sc_ready = true;
|
|
50
|
+
return layer;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wraps the app and loads the Stromcom widget SDK.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} props
|
|
57
|
+
* @param {string} props.clientKey - Client key from the Stromcom dashboard
|
|
58
|
+
* @param {string} props.clientSecret - Bearer token from the Stromcom dashboard
|
|
59
|
+
* @param {string} [props.dataLayer] - Global JS variable name (default: "stromCom")
|
|
60
|
+
* @param {string} [props.environment] - "production" | "staging" | full custom loader URL
|
|
61
|
+
* @param {React.ReactNode} props.children
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* <StromcomProvider clientKey="ck_xxx" clientSecret="cs_xxx">
|
|
65
|
+
* <App />
|
|
66
|
+
* </StromcomProvider>
|
|
67
|
+
*/
|
|
68
|
+
export function StromcomProvider({
|
|
69
|
+
clientKey,
|
|
70
|
+
clientSecret,
|
|
71
|
+
dataLayer = 'stromCom',
|
|
72
|
+
environment = 'production',
|
|
73
|
+
children,
|
|
74
|
+
}) {
|
|
75
|
+
// Set up stubs synchronously so children can queue calls immediately on render.
|
|
76
|
+
const layer = setupLayer(dataLayer);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (typeof window === 'undefined') return;
|
|
80
|
+
|
|
81
|
+
// Avoid injecting the script twice (React StrictMode, HMR, etc.)
|
|
82
|
+
if (document.querySelector(`script[data-ck="${clientKey}"][data-type="stromcom"]`)) return;
|
|
83
|
+
|
|
84
|
+
const base = LOADER_URLS[environment] ?? environment;
|
|
85
|
+
const dl = dataLayer + 'DL';
|
|
86
|
+
|
|
87
|
+
const script = document.createElement('script');
|
|
88
|
+
script.async = true;
|
|
89
|
+
script.dataset.type = 'stromcom';
|
|
90
|
+
script.dataset.l = dataLayer;
|
|
91
|
+
script.dataset.dl = dl;
|
|
92
|
+
script.dataset.ck = clientKey;
|
|
93
|
+
script.dataset.cs = clientSecret;
|
|
94
|
+
script.src = `${base}?${clientKey}`;
|
|
95
|
+
document.head.appendChild(script);
|
|
96
|
+
|
|
97
|
+
return () => script.remove();
|
|
98
|
+
}, [clientKey, clientSecret, dataLayer, environment]);
|
|
99
|
+
|
|
100
|
+
return <StromcomContext.Provider value={layer}>{children}</StromcomContext.Provider>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function useStromcom() {
|
|
104
|
+
return useContext(StromcomContext);
|
|
105
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useStromcom } from './StromcomProvider.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Embeds a Stromcom conversation thread into the rendered div.
|
|
6
|
+
* The thread is initialized once on mount.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: The `code` should already be hashed server-side (HMAC + base-62).
|
|
9
|
+
*
|
|
10
|
+
* @param {object} props
|
|
11
|
+
* @param {string} props.code - Hashed unique thread identifier (max 100 chars, [a-zA-Z0-9-_])
|
|
12
|
+
* @param {string} [props.name] - Display name shown in the thread header
|
|
13
|
+
* @param {string} [props.url] - Canonical URL of the page (shown as link in header)
|
|
14
|
+
* @param {boolean} [props.userHint] - Enable @mention suggestions (default: true)
|
|
15
|
+
* @param {string} [props.className] - CSS class for the container div
|
|
16
|
+
* @param {object} [props.style] - Inline styles for the container div
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <StromcomThread
|
|
20
|
+
* code={order.stromcomHash}
|
|
21
|
+
* name={`Order #${order.id}`}
|
|
22
|
+
* url={window.location.href}
|
|
23
|
+
* style={{ height: '400px' }}
|
|
24
|
+
* />
|
|
25
|
+
*/
|
|
26
|
+
export function StromcomThread({ code, name, url, userHint, className, style, ...rest }) {
|
|
27
|
+
const sc = useStromcom();
|
|
28
|
+
const containerRef = useRef(null);
|
|
29
|
+
const initializedRef = useRef(false);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!sc || !containerRef.current || initializedRef.current) return;
|
|
33
|
+
|
|
34
|
+
const opts = { code };
|
|
35
|
+
if (name !== undefined) opts.name = name;
|
|
36
|
+
if (url !== undefined) opts.url = url;
|
|
37
|
+
if (userHint !== undefined) opts.userHint = userHint;
|
|
38
|
+
|
|
39
|
+
sc.thread(containerRef.current, opts);
|
|
40
|
+
initializedRef.current = true;
|
|
41
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally omit option deps, thread cannot be re-initialized
|
|
42
|
+
}, [sc]);
|
|
43
|
+
|
|
44
|
+
return <div ref={containerRef} className={className} style={style} {...rest} />;
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useStromcom } from './StromcomProvider.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Identifies the currently logged-in user.
|
|
6
|
+
* Re-calls initUser whenever any prop changes.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: The `code` should already be hashed server-side (HMAC + base-62).
|
|
9
|
+
* Never pass your raw internal user ID directly from the client.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} props
|
|
12
|
+
* @param {string} props.code - Hashed unique user identifier (max 100 chars, [a-zA-Z0-9-_])
|
|
13
|
+
* @param {string} [props.name] - Display name
|
|
14
|
+
* @param {string} [props.emailAddress]- Used for email notifications
|
|
15
|
+
* @param {boolean} [props.readOnly] - When true, user can read but not send messages
|
|
16
|
+
* @param {string} [props.avatarURL] - Full URL to avatar image
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <StromcomUser
|
|
20
|
+
* code={user.stromcomHash}
|
|
21
|
+
* name={user.name}
|
|
22
|
+
* emailAddress={user.email}
|
|
23
|
+
* />
|
|
24
|
+
*/
|
|
25
|
+
export function StromcomUser({ code, name, emailAddress, readOnly, avatarURL }) {
|
|
26
|
+
const sc = useStromcom();
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!sc) return;
|
|
30
|
+
|
|
31
|
+
const opts = { code };
|
|
32
|
+
if (name !== undefined) opts.name = name;
|
|
33
|
+
if (emailAddress !== undefined) opts.emailAddress = emailAddress;
|
|
34
|
+
if (readOnly !== undefined) opts.readOnly = readOnly;
|
|
35
|
+
if (avatarURL !== undefined) opts.avatarURL = avatarURL;
|
|
36
|
+
|
|
37
|
+
sc.initUser(opts);
|
|
38
|
+
}, [sc, code, name, emailAddress, readOnly, avatarURL]);
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { render, cleanup } from '@testing-library/react';
|
|
3
|
+
import { StromcomContext } from '../StromcomProvider.jsx';
|
|
4
|
+
import { StromcomConf } from '../StromcomConf.jsx';
|
|
5
|
+
|
|
6
|
+
function renderWithLayer(layer, props) {
|
|
7
|
+
return render(
|
|
8
|
+
<StromcomContext.Provider value={layer}>
|
|
9
|
+
<StromcomConf {...props} />
|
|
10
|
+
</StromcomContext.Provider>,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
afterEach(() => cleanup());
|
|
15
|
+
|
|
16
|
+
describe('StromcomConf', () => {
|
|
17
|
+
it('sends only the options that are provided', () => {
|
|
18
|
+
const layer = { conf: vi.fn() };
|
|
19
|
+
renderWithLayer(layer, { notificationElementPosition: 4 });
|
|
20
|
+
|
|
21
|
+
expect(layer.conf).toHaveBeenCalledTimes(1);
|
|
22
|
+
expect(layer.conf).toHaveBeenCalledWith({ notificationElementPosition: 4 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('forwards function options as-is (no string-wrapping)', () => {
|
|
26
|
+
const layer = { conf: vi.fn() };
|
|
27
|
+
const onNotification = vi.fn();
|
|
28
|
+
renderWithLayer(layer, { onNotification });
|
|
29
|
+
|
|
30
|
+
expect(layer.conf).toHaveBeenCalledWith({ onNotification });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('re-sends conf when an option changes', () => {
|
|
34
|
+
const layer = { conf: vi.fn() };
|
|
35
|
+
const { rerender } = renderWithLayer(layer, { theme: 'stromcom-light' });
|
|
36
|
+
|
|
37
|
+
rerender(
|
|
38
|
+
<StromcomContext.Provider value={layer}>
|
|
39
|
+
<StromcomConf theme="stromcom-dark" />
|
|
40
|
+
</StromcomContext.Provider>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(layer.conf).toHaveBeenCalledTimes(2);
|
|
44
|
+
expect(layer.conf).toHaveBeenLastCalledWith({ theme: 'stromcom-dark' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('does not call conf and renders nothing when there is no layer', () => {
|
|
48
|
+
const { container } = render(<StromcomConf theme="stromcom-dark" />);
|
|
49
|
+
|
|
50
|
+
expect(container.firstChild).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|