@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 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 && !reduced) {
1095
- const id = requestAnimationFrame(() => setVisible(true));
1096
- return () => cancelAnimationFrame(id);
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 { sendMessage, uploadAttachment, isLoading, config, t } = useChat();
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((a) => a.id === id ? { ...a, ...patch } : a)
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((a) => a.status === "ready").map((a) => a.token);
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(value, readyTokens.length > 0 ? { attachmentTokens: readyTokens } : void 0);
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 && !reduced) {
1074
- const id = requestAnimationFrame(() => setVisible(true));
1075
- return () => cancelAnimationFrame(id);
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 { sendMessage, uploadAttachment, isLoading, config, t } = useChat();
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((a) => a.id === id ? { ...a, ...patch } : a)
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((a) => a.status === "ready").map((a) => a.token);
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(value, readyTokens.length > 0 ? { attachmentTokens: readyTokens } : void 0);
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.0.0",
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": "^1.0.0",
57
+ "@customerhero/js": "^2.0.0",
58
58
  "react": ">=18"
59
59
  },
60
60
  "devDependencies": {
61
- "@customerhero/js": "^1.0.0",
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",