@customerhero/react 1.0.0 → 1.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/dist/index.cjs +307 -9
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +307 -9
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -100,6 +100,27 @@ function useChat() {
|
|
|
100
100
|
identify: (0, import_react2.useCallback)(
|
|
101
101
|
(payload) => client.identify(payload),
|
|
102
102
|
[client]
|
|
103
|
+
),
|
|
104
|
+
setConsent: (0, import_react2.useCallback)(
|
|
105
|
+
(consent) => client.setConsent(consent),
|
|
106
|
+
[client]
|
|
107
|
+
),
|
|
108
|
+
setTraits: (0, import_react2.useCallback)(
|
|
109
|
+
(traits) => client.setTraits(traits),
|
|
110
|
+
[client]
|
|
111
|
+
),
|
|
112
|
+
submitPreChatForm: (0, import_react2.useCallback)(
|
|
113
|
+
(submission) => client.submitPreChatForm(submission),
|
|
114
|
+
[client]
|
|
115
|
+
),
|
|
116
|
+
cancelPreChatForm: (0, import_react2.useCallback)(() => client.cancelPreChatForm(), [client]),
|
|
117
|
+
fireTrigger: (0, import_react2.useCallback)(
|
|
118
|
+
(triggerId) => client.fireTrigger(triggerId),
|
|
119
|
+
[client]
|
|
120
|
+
),
|
|
121
|
+
consumePendingPrefill: (0, import_react2.useCallback)(
|
|
122
|
+
() => client.consumePendingPrefill(),
|
|
123
|
+
[client]
|
|
103
124
|
)
|
|
104
125
|
};
|
|
105
126
|
}
|
|
@@ -1091,10 +1112,12 @@ function AnimatedMessage({
|
|
|
1091
1112
|
}) {
|
|
1092
1113
|
const [visible, setVisible] = (0, import_react7.useState)(!animate);
|
|
1093
1114
|
(0, import_react7.useEffect)(() => {
|
|
1094
|
-
if (animate
|
|
1095
|
-
|
|
1096
|
-
return
|
|
1115
|
+
if (!animate || reduced) {
|
|
1116
|
+
setVisible(true);
|
|
1117
|
+
return;
|
|
1097
1118
|
}
|
|
1119
|
+
const id = requestAnimationFrame(() => setVisible(true));
|
|
1120
|
+
return () => cancelAnimationFrame(id);
|
|
1098
1121
|
}, [animate, reduced]);
|
|
1099
1122
|
const style = {
|
|
1100
1123
|
alignSelf: isUser ? "flex-end" : "flex-start",
|
|
@@ -1455,7 +1478,15 @@ var import_js2 = require("@customerhero/js");
|
|
|
1455
1478
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1456
1479
|
var MAX_ATTACHMENTS = 3;
|
|
1457
1480
|
function ChatInput() {
|
|
1458
|
-
const {
|
|
1481
|
+
const {
|
|
1482
|
+
sendMessage,
|
|
1483
|
+
uploadAttachment,
|
|
1484
|
+
isLoading,
|
|
1485
|
+
config,
|
|
1486
|
+
t,
|
|
1487
|
+
consumePendingPrefill,
|
|
1488
|
+
pendingPrefill
|
|
1489
|
+
} = useChat();
|
|
1459
1490
|
const reduced = useReducedMotion();
|
|
1460
1491
|
const [value, setValue] = (0, import_react8.useState)("");
|
|
1461
1492
|
const [attachments, setAttachments] = (0, import_react8.useState)([]);
|
|
@@ -1463,6 +1494,11 @@ function ChatInput() {
|
|
|
1463
1494
|
(0, import_react8.useEffect)(() => {
|
|
1464
1495
|
setCaptureSupported((0, import_js2.canCaptureScreenshot)());
|
|
1465
1496
|
}, []);
|
|
1497
|
+
(0, import_react8.useEffect)(() => {
|
|
1498
|
+
if (pendingPrefill === null) return;
|
|
1499
|
+
const text = consumePendingPrefill();
|
|
1500
|
+
if (text !== null) setValue(text);
|
|
1501
|
+
}, [pendingPrefill, consumePendingPrefill]);
|
|
1466
1502
|
(0, import_react8.useEffect)(() => {
|
|
1467
1503
|
return () => {
|
|
1468
1504
|
for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
|
|
@@ -1470,7 +1506,9 @@ function ChatInput() {
|
|
|
1470
1506
|
}, []);
|
|
1471
1507
|
const updateAttachment = (id, patch) => {
|
|
1472
1508
|
setAttachments(
|
|
1473
|
-
(current) => current.map(
|
|
1509
|
+
(current) => current.map(
|
|
1510
|
+
(a) => a.id === id ? { ...a, ...patch } : a
|
|
1511
|
+
)
|
|
1474
1512
|
);
|
|
1475
1513
|
};
|
|
1476
1514
|
const startUpload = async (blob) => {
|
|
@@ -1506,10 +1544,15 @@ function ChatInput() {
|
|
|
1506
1544
|
return current.filter((a) => a.id !== id);
|
|
1507
1545
|
});
|
|
1508
1546
|
};
|
|
1509
|
-
const readyTokens = attachments.filter(
|
|
1547
|
+
const readyTokens = attachments.filter(
|
|
1548
|
+
(a) => a.status === "ready"
|
|
1549
|
+
).map((a) => a.token);
|
|
1510
1550
|
const handleSend = () => {
|
|
1511
1551
|
if (!value.trim() || isLoading) return;
|
|
1512
|
-
sendMessage(
|
|
1552
|
+
sendMessage(
|
|
1553
|
+
value,
|
|
1554
|
+
readyTokens.length > 0 ? { attachmentTokens: readyTokens } : void 0
|
|
1555
|
+
);
|
|
1513
1556
|
for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
|
|
1514
1557
|
setAttachments([]);
|
|
1515
1558
|
setValue("");
|
|
@@ -1797,8 +1840,263 @@ function ConfigError({ message, title }) {
|
|
|
1797
1840
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: { fontSize: 12, color: "#999", margin: 0 }, children: message })
|
|
1798
1841
|
] });
|
|
1799
1842
|
}
|
|
1843
|
+
function fieldKey(field) {
|
|
1844
|
+
if (field.kind === "name" || field.kind === "email" || field.kind === "phone") {
|
|
1845
|
+
return field.kind;
|
|
1846
|
+
}
|
|
1847
|
+
return field.key;
|
|
1848
|
+
}
|
|
1849
|
+
function fieldLabel(field) {
|
|
1850
|
+
if (field.kind === "name") return field.label ?? "Name";
|
|
1851
|
+
if (field.kind === "email") return field.label ?? "Email";
|
|
1852
|
+
if (field.kind === "phone") return field.label ?? "Phone";
|
|
1853
|
+
return field.label;
|
|
1854
|
+
}
|
|
1855
|
+
function validateField(field, value) {
|
|
1856
|
+
const required = "required" in field ? !!field.required : false;
|
|
1857
|
+
if (required && (value === void 0 || value === "" || value === false)) {
|
|
1858
|
+
return "Required";
|
|
1859
|
+
}
|
|
1860
|
+
if (field.kind === "email" && typeof value === "string" && value !== "") {
|
|
1861
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) return "Invalid email";
|
|
1862
|
+
}
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
function PreChatFormView() {
|
|
1866
|
+
const { preChatForm, submitPreChatForm, cancelPreChatForm, config, t } = useChat();
|
|
1867
|
+
const [values, setValues] = (0, import_react9.useState)({});
|
|
1868
|
+
const [errors, setErrors] = (0, import_react9.useState)({});
|
|
1869
|
+
const [submitting, setSubmitting] = (0, import_react9.useState)(false);
|
|
1870
|
+
if (!preChatForm) return null;
|
|
1871
|
+
function setValue(key, value) {
|
|
1872
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
1873
|
+
setErrors((prev) => ({ ...prev, [key]: null }));
|
|
1874
|
+
}
|
|
1875
|
+
async function handleSubmit(e) {
|
|
1876
|
+
e.preventDefault();
|
|
1877
|
+
if (!preChatForm) return;
|
|
1878
|
+
const nextErrors = {};
|
|
1879
|
+
let hasError = false;
|
|
1880
|
+
for (const f of preChatForm.fields) {
|
|
1881
|
+
const k = fieldKey(f);
|
|
1882
|
+
const err = validateField(f, values[k]);
|
|
1883
|
+
nextErrors[k] = err;
|
|
1884
|
+
if (err) hasError = true;
|
|
1885
|
+
}
|
|
1886
|
+
setErrors(nextErrors);
|
|
1887
|
+
if (hasError) return;
|
|
1888
|
+
const submission = {};
|
|
1889
|
+
const properties = {};
|
|
1890
|
+
for (const f of preChatForm.fields) {
|
|
1891
|
+
const k = fieldKey(f);
|
|
1892
|
+
const v = values[k];
|
|
1893
|
+
if (v === void 0 || v === "") continue;
|
|
1894
|
+
if (f.kind === "name" && typeof v === "string") submission.name = v;
|
|
1895
|
+
else if (f.kind === "email" && typeof v === "string")
|
|
1896
|
+
submission.email = v;
|
|
1897
|
+
else if (f.kind === "phone" && typeof v === "string")
|
|
1898
|
+
submission.phone = v;
|
|
1899
|
+
else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
1900
|
+
properties[k] = v;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
if (Object.keys(properties).length > 0) submission.properties = properties;
|
|
1904
|
+
setSubmitting(true);
|
|
1905
|
+
try {
|
|
1906
|
+
await submitPreChatForm(submission);
|
|
1907
|
+
} finally {
|
|
1908
|
+
setSubmitting(false);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
const containerStyle = {
|
|
1912
|
+
flex: 1,
|
|
1913
|
+
overflowY: "auto",
|
|
1914
|
+
padding: 20,
|
|
1915
|
+
display: "flex",
|
|
1916
|
+
flexDirection: "column",
|
|
1917
|
+
gap: 12,
|
|
1918
|
+
background: config.backgroundColor,
|
|
1919
|
+
color: config.textColor
|
|
1920
|
+
};
|
|
1921
|
+
const labelStyle = {
|
|
1922
|
+
fontSize: 13,
|
|
1923
|
+
fontWeight: 500,
|
|
1924
|
+
display: "flex",
|
|
1925
|
+
flexDirection: "column",
|
|
1926
|
+
gap: 4
|
|
1927
|
+
};
|
|
1928
|
+
const inputStyle = {
|
|
1929
|
+
width: "100%",
|
|
1930
|
+
border: "1px solid #d4d4d8",
|
|
1931
|
+
borderRadius: 8,
|
|
1932
|
+
padding: "8px 10px",
|
|
1933
|
+
fontSize: 14,
|
|
1934
|
+
background: "white",
|
|
1935
|
+
color: "#111",
|
|
1936
|
+
boxSizing: "border-box"
|
|
1937
|
+
};
|
|
1938
|
+
const errorStyle = { color: "#dc2626", fontSize: 12 };
|
|
1939
|
+
const buttonRowStyle = {
|
|
1940
|
+
display: "flex",
|
|
1941
|
+
gap: 8,
|
|
1942
|
+
marginTop: 8
|
|
1943
|
+
};
|
|
1944
|
+
const submitStyle = {
|
|
1945
|
+
flex: 1,
|
|
1946
|
+
background: config.primaryColor,
|
|
1947
|
+
color: "white",
|
|
1948
|
+
border: "none",
|
|
1949
|
+
borderRadius: 8,
|
|
1950
|
+
padding: "10px 14px",
|
|
1951
|
+
fontSize: 14,
|
|
1952
|
+
fontWeight: 600,
|
|
1953
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
1954
|
+
opacity: submitting ? 0.7 : 1
|
|
1955
|
+
};
|
|
1956
|
+
const cancelStyle = {
|
|
1957
|
+
background: "transparent",
|
|
1958
|
+
color: config.textColor,
|
|
1959
|
+
border: "1px solid #d4d4d8",
|
|
1960
|
+
borderRadius: 8,
|
|
1961
|
+
padding: "10px 14px",
|
|
1962
|
+
fontSize: 14,
|
|
1963
|
+
cursor: "pointer"
|
|
1964
|
+
};
|
|
1965
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
1966
|
+
"form",
|
|
1967
|
+
{
|
|
1968
|
+
style: containerStyle,
|
|
1969
|
+
onSubmit: handleSubmit,
|
|
1970
|
+
"data-customerhero-prechat-form": true,
|
|
1971
|
+
children: [
|
|
1972
|
+
preChatForm.title && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h3", { style: { fontSize: 16, fontWeight: 600, margin: 0 }, children: preChatForm.title }),
|
|
1973
|
+
preChatForm.description && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: { fontSize: 13, margin: 0, opacity: 0.8 }, children: preChatForm.description }),
|
|
1974
|
+
preChatForm.fields.map((field) => {
|
|
1975
|
+
const k = fieldKey(field);
|
|
1976
|
+
const v = values[k];
|
|
1977
|
+
const err = errors[k];
|
|
1978
|
+
const required = "required" in field ? !!field.required : false;
|
|
1979
|
+
const label = fieldLabel(field);
|
|
1980
|
+
if (field.kind === "textarea") {
|
|
1981
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { style: labelStyle, children: [
|
|
1982
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
|
|
1983
|
+
label,
|
|
1984
|
+
required && " *"
|
|
1985
|
+
] }),
|
|
1986
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1987
|
+
"textarea",
|
|
1988
|
+
{
|
|
1989
|
+
style: { ...inputStyle, minHeight: 80, resize: "vertical" },
|
|
1990
|
+
value: v ?? "",
|
|
1991
|
+
maxLength: field.maxLength,
|
|
1992
|
+
onChange: (e) => setValue(k, e.target.value)
|
|
1993
|
+
}
|
|
1994
|
+
),
|
|
1995
|
+
err && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: errorStyle, children: err })
|
|
1996
|
+
] }, k);
|
|
1997
|
+
}
|
|
1998
|
+
if (field.kind === "select") {
|
|
1999
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { style: labelStyle, children: [
|
|
2000
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
|
|
2001
|
+
label,
|
|
2002
|
+
required && " *"
|
|
2003
|
+
] }),
|
|
2004
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
2005
|
+
"select",
|
|
2006
|
+
{
|
|
2007
|
+
style: inputStyle,
|
|
2008
|
+
value: v ?? "",
|
|
2009
|
+
onChange: (e) => setValue(k, e.target.value),
|
|
2010
|
+
children: [
|
|
2011
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: "", children: "\u2014" }),
|
|
2012
|
+
field.options.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: opt.value, children: opt.label }, opt.value))
|
|
2013
|
+
]
|
|
2014
|
+
}
|
|
2015
|
+
),
|
|
2016
|
+
err && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: errorStyle, children: err })
|
|
2017
|
+
] }, k);
|
|
2018
|
+
}
|
|
2019
|
+
if (field.kind === "consent") {
|
|
2020
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
2021
|
+
"label",
|
|
2022
|
+
{
|
|
2023
|
+
style: {
|
|
2024
|
+
...labelStyle,
|
|
2025
|
+
flexDirection: "row",
|
|
2026
|
+
alignItems: "flex-start",
|
|
2027
|
+
gap: 8
|
|
2028
|
+
},
|
|
2029
|
+
children: [
|
|
2030
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2031
|
+
"input",
|
|
2032
|
+
{
|
|
2033
|
+
type: "checkbox",
|
|
2034
|
+
checked: v === true,
|
|
2035
|
+
onChange: (e) => setValue(k, e.target.checked),
|
|
2036
|
+
style: { marginTop: 3 }
|
|
2037
|
+
}
|
|
2038
|
+
),
|
|
2039
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { fontSize: 13, fontWeight: 400 }, children: [
|
|
2040
|
+
field.label,
|
|
2041
|
+
field.url && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
2042
|
+
" ",
|
|
2043
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2044
|
+
"a",
|
|
2045
|
+
{
|
|
2046
|
+
href: field.url,
|
|
2047
|
+
target: "_blank",
|
|
2048
|
+
rel: "noopener noreferrer",
|
|
2049
|
+
style: { color: config.primaryColor },
|
|
2050
|
+
children: "\u2197"
|
|
2051
|
+
}
|
|
2052
|
+
)
|
|
2053
|
+
] })
|
|
2054
|
+
] }),
|
|
2055
|
+
err && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: errorStyle, children: err })
|
|
2056
|
+
]
|
|
2057
|
+
},
|
|
2058
|
+
k
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
const inputType = field.kind === "email" ? "email" : field.kind === "phone" ? "tel" : "text";
|
|
2062
|
+
const maxLength = field.kind === "text" ? field.maxLength : void 0;
|
|
2063
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { style: labelStyle, children: [
|
|
2064
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
|
|
2065
|
+
label,
|
|
2066
|
+
required && " *"
|
|
2067
|
+
] }),
|
|
2068
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2069
|
+
"input",
|
|
2070
|
+
{
|
|
2071
|
+
type: inputType,
|
|
2072
|
+
style: inputStyle,
|
|
2073
|
+
value: v ?? "",
|
|
2074
|
+
maxLength,
|
|
2075
|
+
onChange: (e) => setValue(k, e.target.value)
|
|
2076
|
+
}
|
|
2077
|
+
),
|
|
2078
|
+
err && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: errorStyle, children: err })
|
|
2079
|
+
] }, k);
|
|
2080
|
+
}),
|
|
2081
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: buttonRowStyle, children: [
|
|
2082
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2083
|
+
"button",
|
|
2084
|
+
{
|
|
2085
|
+
type: "button",
|
|
2086
|
+
onClick: cancelPreChatForm,
|
|
2087
|
+
style: cancelStyle,
|
|
2088
|
+
disabled: submitting,
|
|
2089
|
+
children: t("action_cancel")
|
|
2090
|
+
}
|
|
2091
|
+
),
|
|
2092
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "submit", style: submitStyle, disabled: submitting, children: preChatForm.submitLabel })
|
|
2093
|
+
] })
|
|
2094
|
+
]
|
|
2095
|
+
}
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
1800
2098
|
function ChatWindow() {
|
|
1801
|
-
const { isOpen, config, configError, t, isRtl } = useChat();
|
|
2099
|
+
const { isOpen, config, configError, t, isRtl, preChatFormVisible } = useChat();
|
|
1802
2100
|
const reduced = useReducedMotion();
|
|
1803
2101
|
const [visible, setVisible] = (0, import_react9.useState)(false);
|
|
1804
2102
|
const [shouldRender, setShouldRender] = (0, import_react9.useState)(false);
|
|
@@ -1853,7 +2151,7 @@ function ChatWindow() {
|
|
|
1853
2151
|
};
|
|
1854
2152
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style, dir: isRtl ? "rtl" : "ltr", children: [
|
|
1855
2153
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ChatHeader, {}),
|
|
1856
|
-
configError ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ConfigError, { title: t("unable_to_load"), message: configError }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
2154
|
+
configError ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ConfigError, { title: t("unable_to_load"), message: configError }) : preChatFormVisible ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PreChatFormView, {}) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
1857
2155
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ChatMessages, {}),
|
|
1858
2156
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ChatSuggestions, {}),
|
|
1859
2157
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ChatInput, {})
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { CustomerHeroChatConfig, IdentifyPayload, ActionConfirmationBlock, TranslateFn, ChatState, MessageRating } from '@customerhero/js';
|
|
2
|
+
import { CustomerHeroChatConfig, IdentifyPayload, ActionConfirmationBlock, TranslateFn, ChatState, MessageRating, ConsentSettings, PreChatSubmission } from '@customerhero/js';
|
|
3
3
|
export { ActionConfirmationBlock, IdentifyPayload } from '@customerhero/js';
|
|
4
4
|
|
|
5
5
|
interface ChatWidgetProps extends CustomerHeroChatConfig {
|
|
@@ -38,6 +38,12 @@ interface UseChatReturn extends ChatState {
|
|
|
38
38
|
close: () => void;
|
|
39
39
|
reset: () => void;
|
|
40
40
|
identify: (payload: IdentifyPayload) => void;
|
|
41
|
+
setConsent: (consent: Partial<ConsentSettings>) => void;
|
|
42
|
+
setTraits: (traits: Record<string, string | number | boolean>) => void;
|
|
43
|
+
submitPreChatForm: (submission: PreChatSubmission) => Promise<void>;
|
|
44
|
+
cancelPreChatForm: () => void;
|
|
45
|
+
fireTrigger: (triggerId: string) => void;
|
|
46
|
+
consumePendingPrefill: () => string | null;
|
|
41
47
|
}
|
|
42
48
|
declare function useChat(): UseChatReturn;
|
|
43
49
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { CustomerHeroChatConfig, IdentifyPayload, ActionConfirmationBlock, TranslateFn, ChatState, MessageRating } from '@customerhero/js';
|
|
2
|
+
import { CustomerHeroChatConfig, IdentifyPayload, ActionConfirmationBlock, TranslateFn, ChatState, MessageRating, ConsentSettings, PreChatSubmission } from '@customerhero/js';
|
|
3
3
|
export { ActionConfirmationBlock, IdentifyPayload } from '@customerhero/js';
|
|
4
4
|
|
|
5
5
|
interface ChatWidgetProps extends CustomerHeroChatConfig {
|
|
@@ -38,6 +38,12 @@ interface UseChatReturn extends ChatState {
|
|
|
38
38
|
close: () => void;
|
|
39
39
|
reset: () => void;
|
|
40
40
|
identify: (payload: IdentifyPayload) => void;
|
|
41
|
+
setConsent: (consent: Partial<ConsentSettings>) => void;
|
|
42
|
+
setTraits: (traits: Record<string, string | number | boolean>) => void;
|
|
43
|
+
submitPreChatForm: (submission: PreChatSubmission) => Promise<void>;
|
|
44
|
+
cancelPreChatForm: () => void;
|
|
45
|
+
fireTrigger: (triggerId: string) => void;
|
|
46
|
+
consumePendingPrefill: () => string | null;
|
|
41
47
|
}
|
|
42
48
|
declare function useChat(): UseChatReturn;
|
|
43
49
|
|
package/dist/index.js
CHANGED
|
@@ -79,6 +79,27 @@ function useChat() {
|
|
|
79
79
|
identify: useCallback(
|
|
80
80
|
(payload) => client.identify(payload),
|
|
81
81
|
[client]
|
|
82
|
+
),
|
|
83
|
+
setConsent: useCallback(
|
|
84
|
+
(consent) => client.setConsent(consent),
|
|
85
|
+
[client]
|
|
86
|
+
),
|
|
87
|
+
setTraits: useCallback(
|
|
88
|
+
(traits) => client.setTraits(traits),
|
|
89
|
+
[client]
|
|
90
|
+
),
|
|
91
|
+
submitPreChatForm: useCallback(
|
|
92
|
+
(submission) => client.submitPreChatForm(submission),
|
|
93
|
+
[client]
|
|
94
|
+
),
|
|
95
|
+
cancelPreChatForm: useCallback(() => client.cancelPreChatForm(), [client]),
|
|
96
|
+
fireTrigger: useCallback(
|
|
97
|
+
(triggerId) => client.fireTrigger(triggerId),
|
|
98
|
+
[client]
|
|
99
|
+
),
|
|
100
|
+
consumePendingPrefill: useCallback(
|
|
101
|
+
() => client.consumePendingPrefill(),
|
|
102
|
+
[client]
|
|
82
103
|
)
|
|
83
104
|
};
|
|
84
105
|
}
|
|
@@ -1070,10 +1091,12 @@ function AnimatedMessage({
|
|
|
1070
1091
|
}) {
|
|
1071
1092
|
const [visible, setVisible] = useState5(!animate);
|
|
1072
1093
|
useEffect5(() => {
|
|
1073
|
-
if (animate
|
|
1074
|
-
|
|
1075
|
-
return
|
|
1094
|
+
if (!animate || reduced) {
|
|
1095
|
+
setVisible(true);
|
|
1096
|
+
return;
|
|
1076
1097
|
}
|
|
1098
|
+
const id = requestAnimationFrame(() => setVisible(true));
|
|
1099
|
+
return () => cancelAnimationFrame(id);
|
|
1077
1100
|
}, [animate, reduced]);
|
|
1078
1101
|
const style = {
|
|
1079
1102
|
alignSelf: isUser ? "flex-end" : "flex-start",
|
|
@@ -1441,7 +1464,15 @@ import {
|
|
|
1441
1464
|
import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1442
1465
|
var MAX_ATTACHMENTS = 3;
|
|
1443
1466
|
function ChatInput() {
|
|
1444
|
-
const {
|
|
1467
|
+
const {
|
|
1468
|
+
sendMessage,
|
|
1469
|
+
uploadAttachment,
|
|
1470
|
+
isLoading,
|
|
1471
|
+
config,
|
|
1472
|
+
t,
|
|
1473
|
+
consumePendingPrefill,
|
|
1474
|
+
pendingPrefill
|
|
1475
|
+
} = useChat();
|
|
1445
1476
|
const reduced = useReducedMotion();
|
|
1446
1477
|
const [value, setValue] = useState6("");
|
|
1447
1478
|
const [attachments, setAttachments] = useState6([]);
|
|
@@ -1449,6 +1480,11 @@ function ChatInput() {
|
|
|
1449
1480
|
useEffect6(() => {
|
|
1450
1481
|
setCaptureSupported(canCaptureScreenshot());
|
|
1451
1482
|
}, []);
|
|
1483
|
+
useEffect6(() => {
|
|
1484
|
+
if (pendingPrefill === null) return;
|
|
1485
|
+
const text = consumePendingPrefill();
|
|
1486
|
+
if (text !== null) setValue(text);
|
|
1487
|
+
}, [pendingPrefill, consumePendingPrefill]);
|
|
1452
1488
|
useEffect6(() => {
|
|
1453
1489
|
return () => {
|
|
1454
1490
|
for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
|
|
@@ -1456,7 +1492,9 @@ function ChatInput() {
|
|
|
1456
1492
|
}, []);
|
|
1457
1493
|
const updateAttachment = (id, patch) => {
|
|
1458
1494
|
setAttachments(
|
|
1459
|
-
(current) => current.map(
|
|
1495
|
+
(current) => current.map(
|
|
1496
|
+
(a) => a.id === id ? { ...a, ...patch } : a
|
|
1497
|
+
)
|
|
1460
1498
|
);
|
|
1461
1499
|
};
|
|
1462
1500
|
const startUpload = async (blob) => {
|
|
@@ -1492,10 +1530,15 @@ function ChatInput() {
|
|
|
1492
1530
|
return current.filter((a) => a.id !== id);
|
|
1493
1531
|
});
|
|
1494
1532
|
};
|
|
1495
|
-
const readyTokens = attachments.filter(
|
|
1533
|
+
const readyTokens = attachments.filter(
|
|
1534
|
+
(a) => a.status === "ready"
|
|
1535
|
+
).map((a) => a.token);
|
|
1496
1536
|
const handleSend = () => {
|
|
1497
1537
|
if (!value.trim() || isLoading) return;
|
|
1498
|
-
sendMessage(
|
|
1538
|
+
sendMessage(
|
|
1539
|
+
value,
|
|
1540
|
+
readyTokens.length > 0 ? { attachmentTokens: readyTokens } : void 0
|
|
1541
|
+
);
|
|
1499
1542
|
for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
|
|
1500
1543
|
setAttachments([]);
|
|
1501
1544
|
setValue("");
|
|
@@ -1783,8 +1826,263 @@ function ConfigError({ message, title }) {
|
|
|
1783
1826
|
/* @__PURE__ */ jsx9("p", { style: { fontSize: 12, color: "#999", margin: 0 }, children: message })
|
|
1784
1827
|
] });
|
|
1785
1828
|
}
|
|
1829
|
+
function fieldKey(field) {
|
|
1830
|
+
if (field.kind === "name" || field.kind === "email" || field.kind === "phone") {
|
|
1831
|
+
return field.kind;
|
|
1832
|
+
}
|
|
1833
|
+
return field.key;
|
|
1834
|
+
}
|
|
1835
|
+
function fieldLabel(field) {
|
|
1836
|
+
if (field.kind === "name") return field.label ?? "Name";
|
|
1837
|
+
if (field.kind === "email") return field.label ?? "Email";
|
|
1838
|
+
if (field.kind === "phone") return field.label ?? "Phone";
|
|
1839
|
+
return field.label;
|
|
1840
|
+
}
|
|
1841
|
+
function validateField(field, value) {
|
|
1842
|
+
const required = "required" in field ? !!field.required : false;
|
|
1843
|
+
if (required && (value === void 0 || value === "" || value === false)) {
|
|
1844
|
+
return "Required";
|
|
1845
|
+
}
|
|
1846
|
+
if (field.kind === "email" && typeof value === "string" && value !== "") {
|
|
1847
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) return "Invalid email";
|
|
1848
|
+
}
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
function PreChatFormView() {
|
|
1852
|
+
const { preChatForm, submitPreChatForm, cancelPreChatForm, config, t } = useChat();
|
|
1853
|
+
const [values, setValues] = useState7({});
|
|
1854
|
+
const [errors, setErrors] = useState7({});
|
|
1855
|
+
const [submitting, setSubmitting] = useState7(false);
|
|
1856
|
+
if (!preChatForm) return null;
|
|
1857
|
+
function setValue(key, value) {
|
|
1858
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
1859
|
+
setErrors((prev) => ({ ...prev, [key]: null }));
|
|
1860
|
+
}
|
|
1861
|
+
async function handleSubmit(e) {
|
|
1862
|
+
e.preventDefault();
|
|
1863
|
+
if (!preChatForm) return;
|
|
1864
|
+
const nextErrors = {};
|
|
1865
|
+
let hasError = false;
|
|
1866
|
+
for (const f of preChatForm.fields) {
|
|
1867
|
+
const k = fieldKey(f);
|
|
1868
|
+
const err = validateField(f, values[k]);
|
|
1869
|
+
nextErrors[k] = err;
|
|
1870
|
+
if (err) hasError = true;
|
|
1871
|
+
}
|
|
1872
|
+
setErrors(nextErrors);
|
|
1873
|
+
if (hasError) return;
|
|
1874
|
+
const submission = {};
|
|
1875
|
+
const properties = {};
|
|
1876
|
+
for (const f of preChatForm.fields) {
|
|
1877
|
+
const k = fieldKey(f);
|
|
1878
|
+
const v = values[k];
|
|
1879
|
+
if (v === void 0 || v === "") continue;
|
|
1880
|
+
if (f.kind === "name" && typeof v === "string") submission.name = v;
|
|
1881
|
+
else if (f.kind === "email" && typeof v === "string")
|
|
1882
|
+
submission.email = v;
|
|
1883
|
+
else if (f.kind === "phone" && typeof v === "string")
|
|
1884
|
+
submission.phone = v;
|
|
1885
|
+
else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
1886
|
+
properties[k] = v;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (Object.keys(properties).length > 0) submission.properties = properties;
|
|
1890
|
+
setSubmitting(true);
|
|
1891
|
+
try {
|
|
1892
|
+
await submitPreChatForm(submission);
|
|
1893
|
+
} finally {
|
|
1894
|
+
setSubmitting(false);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
const containerStyle = {
|
|
1898
|
+
flex: 1,
|
|
1899
|
+
overflowY: "auto",
|
|
1900
|
+
padding: 20,
|
|
1901
|
+
display: "flex",
|
|
1902
|
+
flexDirection: "column",
|
|
1903
|
+
gap: 12,
|
|
1904
|
+
background: config.backgroundColor,
|
|
1905
|
+
color: config.textColor
|
|
1906
|
+
};
|
|
1907
|
+
const labelStyle = {
|
|
1908
|
+
fontSize: 13,
|
|
1909
|
+
fontWeight: 500,
|
|
1910
|
+
display: "flex",
|
|
1911
|
+
flexDirection: "column",
|
|
1912
|
+
gap: 4
|
|
1913
|
+
};
|
|
1914
|
+
const inputStyle = {
|
|
1915
|
+
width: "100%",
|
|
1916
|
+
border: "1px solid #d4d4d8",
|
|
1917
|
+
borderRadius: 8,
|
|
1918
|
+
padding: "8px 10px",
|
|
1919
|
+
fontSize: 14,
|
|
1920
|
+
background: "white",
|
|
1921
|
+
color: "#111",
|
|
1922
|
+
boxSizing: "border-box"
|
|
1923
|
+
};
|
|
1924
|
+
const errorStyle = { color: "#dc2626", fontSize: 12 };
|
|
1925
|
+
const buttonRowStyle = {
|
|
1926
|
+
display: "flex",
|
|
1927
|
+
gap: 8,
|
|
1928
|
+
marginTop: 8
|
|
1929
|
+
};
|
|
1930
|
+
const submitStyle = {
|
|
1931
|
+
flex: 1,
|
|
1932
|
+
background: config.primaryColor,
|
|
1933
|
+
color: "white",
|
|
1934
|
+
border: "none",
|
|
1935
|
+
borderRadius: 8,
|
|
1936
|
+
padding: "10px 14px",
|
|
1937
|
+
fontSize: 14,
|
|
1938
|
+
fontWeight: 600,
|
|
1939
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
1940
|
+
opacity: submitting ? 0.7 : 1
|
|
1941
|
+
};
|
|
1942
|
+
const cancelStyle = {
|
|
1943
|
+
background: "transparent",
|
|
1944
|
+
color: config.textColor,
|
|
1945
|
+
border: "1px solid #d4d4d8",
|
|
1946
|
+
borderRadius: 8,
|
|
1947
|
+
padding: "10px 14px",
|
|
1948
|
+
fontSize: 14,
|
|
1949
|
+
cursor: "pointer"
|
|
1950
|
+
};
|
|
1951
|
+
return /* @__PURE__ */ jsxs6(
|
|
1952
|
+
"form",
|
|
1953
|
+
{
|
|
1954
|
+
style: containerStyle,
|
|
1955
|
+
onSubmit: handleSubmit,
|
|
1956
|
+
"data-customerhero-prechat-form": true,
|
|
1957
|
+
children: [
|
|
1958
|
+
preChatForm.title && /* @__PURE__ */ jsx9("h3", { style: { fontSize: 16, fontWeight: 600, margin: 0 }, children: preChatForm.title }),
|
|
1959
|
+
preChatForm.description && /* @__PURE__ */ jsx9("p", { style: { fontSize: 13, margin: 0, opacity: 0.8 }, children: preChatForm.description }),
|
|
1960
|
+
preChatForm.fields.map((field) => {
|
|
1961
|
+
const k = fieldKey(field);
|
|
1962
|
+
const v = values[k];
|
|
1963
|
+
const err = errors[k];
|
|
1964
|
+
const required = "required" in field ? !!field.required : false;
|
|
1965
|
+
const label = fieldLabel(field);
|
|
1966
|
+
if (field.kind === "textarea") {
|
|
1967
|
+
return /* @__PURE__ */ jsxs6("label", { style: labelStyle, children: [
|
|
1968
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
1969
|
+
label,
|
|
1970
|
+
required && " *"
|
|
1971
|
+
] }),
|
|
1972
|
+
/* @__PURE__ */ jsx9(
|
|
1973
|
+
"textarea",
|
|
1974
|
+
{
|
|
1975
|
+
style: { ...inputStyle, minHeight: 80, resize: "vertical" },
|
|
1976
|
+
value: v ?? "",
|
|
1977
|
+
maxLength: field.maxLength,
|
|
1978
|
+
onChange: (e) => setValue(k, e.target.value)
|
|
1979
|
+
}
|
|
1980
|
+
),
|
|
1981
|
+
err && /* @__PURE__ */ jsx9("span", { style: errorStyle, children: err })
|
|
1982
|
+
] }, k);
|
|
1983
|
+
}
|
|
1984
|
+
if (field.kind === "select") {
|
|
1985
|
+
return /* @__PURE__ */ jsxs6("label", { style: labelStyle, children: [
|
|
1986
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
1987
|
+
label,
|
|
1988
|
+
required && " *"
|
|
1989
|
+
] }),
|
|
1990
|
+
/* @__PURE__ */ jsxs6(
|
|
1991
|
+
"select",
|
|
1992
|
+
{
|
|
1993
|
+
style: inputStyle,
|
|
1994
|
+
value: v ?? "",
|
|
1995
|
+
onChange: (e) => setValue(k, e.target.value),
|
|
1996
|
+
children: [
|
|
1997
|
+
/* @__PURE__ */ jsx9("option", { value: "", children: "\u2014" }),
|
|
1998
|
+
field.options.map((opt) => /* @__PURE__ */ jsx9("option", { value: opt.value, children: opt.label }, opt.value))
|
|
1999
|
+
]
|
|
2000
|
+
}
|
|
2001
|
+
),
|
|
2002
|
+
err && /* @__PURE__ */ jsx9("span", { style: errorStyle, children: err })
|
|
2003
|
+
] }, k);
|
|
2004
|
+
}
|
|
2005
|
+
if (field.kind === "consent") {
|
|
2006
|
+
return /* @__PURE__ */ jsxs6(
|
|
2007
|
+
"label",
|
|
2008
|
+
{
|
|
2009
|
+
style: {
|
|
2010
|
+
...labelStyle,
|
|
2011
|
+
flexDirection: "row",
|
|
2012
|
+
alignItems: "flex-start",
|
|
2013
|
+
gap: 8
|
|
2014
|
+
},
|
|
2015
|
+
children: [
|
|
2016
|
+
/* @__PURE__ */ jsx9(
|
|
2017
|
+
"input",
|
|
2018
|
+
{
|
|
2019
|
+
type: "checkbox",
|
|
2020
|
+
checked: v === true,
|
|
2021
|
+
onChange: (e) => setValue(k, e.target.checked),
|
|
2022
|
+
style: { marginTop: 3 }
|
|
2023
|
+
}
|
|
2024
|
+
),
|
|
2025
|
+
/* @__PURE__ */ jsxs6("span", { style: { fontSize: 13, fontWeight: 400 }, children: [
|
|
2026
|
+
field.label,
|
|
2027
|
+
field.url && /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
2028
|
+
" ",
|
|
2029
|
+
/* @__PURE__ */ jsx9(
|
|
2030
|
+
"a",
|
|
2031
|
+
{
|
|
2032
|
+
href: field.url,
|
|
2033
|
+
target: "_blank",
|
|
2034
|
+
rel: "noopener noreferrer",
|
|
2035
|
+
style: { color: config.primaryColor },
|
|
2036
|
+
children: "\u2197"
|
|
2037
|
+
}
|
|
2038
|
+
)
|
|
2039
|
+
] })
|
|
2040
|
+
] }),
|
|
2041
|
+
err && /* @__PURE__ */ jsx9("span", { style: errorStyle, children: err })
|
|
2042
|
+
]
|
|
2043
|
+
},
|
|
2044
|
+
k
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
const inputType = field.kind === "email" ? "email" : field.kind === "phone" ? "tel" : "text";
|
|
2048
|
+
const maxLength = field.kind === "text" ? field.maxLength : void 0;
|
|
2049
|
+
return /* @__PURE__ */ jsxs6("label", { style: labelStyle, children: [
|
|
2050
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
2051
|
+
label,
|
|
2052
|
+
required && " *"
|
|
2053
|
+
] }),
|
|
2054
|
+
/* @__PURE__ */ jsx9(
|
|
2055
|
+
"input",
|
|
2056
|
+
{
|
|
2057
|
+
type: inputType,
|
|
2058
|
+
style: inputStyle,
|
|
2059
|
+
value: v ?? "",
|
|
2060
|
+
maxLength,
|
|
2061
|
+
onChange: (e) => setValue(k, e.target.value)
|
|
2062
|
+
}
|
|
2063
|
+
),
|
|
2064
|
+
err && /* @__PURE__ */ jsx9("span", { style: errorStyle, children: err })
|
|
2065
|
+
] }, k);
|
|
2066
|
+
}),
|
|
2067
|
+
/* @__PURE__ */ jsxs6("div", { style: buttonRowStyle, children: [
|
|
2068
|
+
/* @__PURE__ */ jsx9(
|
|
2069
|
+
"button",
|
|
2070
|
+
{
|
|
2071
|
+
type: "button",
|
|
2072
|
+
onClick: cancelPreChatForm,
|
|
2073
|
+
style: cancelStyle,
|
|
2074
|
+
disabled: submitting,
|
|
2075
|
+
children: t("action_cancel")
|
|
2076
|
+
}
|
|
2077
|
+
),
|
|
2078
|
+
/* @__PURE__ */ jsx9("button", { type: "submit", style: submitStyle, disabled: submitting, children: preChatForm.submitLabel })
|
|
2079
|
+
] })
|
|
2080
|
+
]
|
|
2081
|
+
}
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
1786
2084
|
function ChatWindow() {
|
|
1787
|
-
const { isOpen, config, configError, t, isRtl } = useChat();
|
|
2085
|
+
const { isOpen, config, configError, t, isRtl, preChatFormVisible } = useChat();
|
|
1788
2086
|
const reduced = useReducedMotion();
|
|
1789
2087
|
const [visible, setVisible] = useState7(false);
|
|
1790
2088
|
const [shouldRender, setShouldRender] = useState7(false);
|
|
@@ -1839,7 +2137,7 @@ function ChatWindow() {
|
|
|
1839
2137
|
};
|
|
1840
2138
|
return /* @__PURE__ */ jsxs6("div", { style, dir: isRtl ? "rtl" : "ltr", children: [
|
|
1841
2139
|
/* @__PURE__ */ jsx9(ChatHeader, {}),
|
|
1842
|
-
configError ? /* @__PURE__ */ jsx9(ConfigError, { title: t("unable_to_load"), message: configError }) : /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
2140
|
+
configError ? /* @__PURE__ */ jsx9(ConfigError, { title: t("unable_to_load"), message: configError }) : preChatFormVisible ? /* @__PURE__ */ jsx9(PreChatFormView, {}) : /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
1843
2141
|
/* @__PURE__ */ jsx9(ChatMessages, {}),
|
|
1844
2142
|
/* @__PURE__ */ jsx9(ChatSuggestions, {}),
|
|
1845
2143
|
/* @__PURE__ */ jsx9(ChatInput, {})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@customerhero/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "React components for embedding the CustomerHero chat widget.",
|
|
6
6
|
"keywords": [
|
|
@@ -54,11 +54,11 @@
|
|
|
54
54
|
"test": "vitest run"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@customerhero/js": "^
|
|
57
|
+
"@customerhero/js": "^2.0.0",
|
|
58
58
|
"react": ">=18"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@customerhero/js": "^
|
|
61
|
+
"@customerhero/js": "^2.0.0",
|
|
62
62
|
"@testing-library/react": "^16.1.0",
|
|
63
63
|
"@types/react": "^19.0.0",
|
|
64
64
|
"@types/react-dom": "^19.0.0",
|