strapi-plugin-hubspot 0.16.3 → 0.16.5

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/_chunks/{Forms-BBaw53Tz.mjs → Forms-DyV6KyiO.mjs} +20 -6
  3. package/dist/_chunks/{Forms-CIJrUB_n.js → Forms-uUN4XjhW.js} +20 -6
  4. package/dist/_chunks/{HubspotFormInput-DSTuQ_bx.js → HubspotFormInput-BkWat_wu.js} +1 -1
  5. package/dist/_chunks/{HubspotFormInput-DIo6hCNz.mjs → HubspotFormInput-DEXPX-6j.mjs} +1 -1
  6. package/dist/_chunks/{HubspotObjectInput-BIqC8tl2.js → HubspotObjectInput-CFDYW-TM.js} +3 -3
  7. package/dist/_chunks/{HubspotObjectInput-BA16dswz.mjs → HubspotObjectInput-DL8fYK2x.mjs} +3 -3
  8. package/dist/_chunks/{HubspotPropertyInput-C77Op1Wm.js → HubspotPropertyInput-CFMkbQeT.js} +3 -3
  9. package/dist/_chunks/{HubspotPropertyInput-Dsn7ikop.mjs → HubspotPropertyInput-WU5FMJj3.mjs} +3 -3
  10. package/dist/_chunks/{Settings-PVhJVPqq.js → Settings-B_hhdpHa.js} +2 -2
  11. package/dist/_chunks/{Settings-DxX9Uh_2.mjs → Settings-CiXEps_C.mjs} +2 -2
  12. package/dist/_chunks/{en-Djd3TID0.mjs → en-BIUBYzHv.mjs} +5 -1
  13. package/dist/_chunks/{en-sfzopB5K.js → en-CFhEtgZJ.js} +5 -1
  14. package/dist/_chunks/{fr-C7t1elmR.mjs → fr-CMYcnFH0.mjs} +5 -1
  15. package/dist/_chunks/{fr-sF5Josfh.js → fr-zdUuuV0M.js} +5 -1
  16. package/dist/_chunks/{index-B8GYi6sw.mjs → index-BeBn7bJa.mjs} +6 -6
  17. package/dist/_chunks/{index-XEuSPuUj.js → index-CG_WQzSa.js} +6 -6
  18. package/dist/_chunks/{objectLabels-wBwfLdZt.js → objectLabels-BGq-Ypgn.js} +1 -1
  19. package/dist/_chunks/{objectLabels-DybqhBdT.mjs → objectLabels-CS_0QCNz.mjs} +1 -1
  20. package/dist/_chunks/{useHubspotSchema-WvgW3od8.js → useHubspotSchema-BZP2qkAu.js} +1 -1
  21. package/dist/_chunks/{useHubspotSchema-DtcHHUOb.mjs → useHubspotSchema-CP7LRdzO.mjs} +1 -1
  22. package/dist/admin/index.js +1 -1
  23. package/dist/admin/index.mjs +1 -1
  24. package/dist/admin/src/builder/types.d.ts +1 -1
  25. package/dist/server/index.js +99 -25
  26. package/dist/server/index.mjs +99 -25
  27. package/dist/server/src/hsforms.d.ts +53 -7
  28. package/dist/server/src/importHubspot.d.ts +9 -0
  29. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.16.5 — 2026-09-23
6
+
7
+ ### Fixed
8
+ - Admin TypeScript build failed on Submissions (`unknown` used as ReactNode).
9
+ Same conversion-fail behaviour as 0.16.4; this is the first npm publish of
10
+ that fix.
11
+
12
+ ## 0.16.4 — 2026-09-23
13
+
14
+ ### Fixed
15
+ - A linked HubSpot form that refused the Forms API (GDPR communications,
16
+ CAPTCHA, extra required fields) silently fell back to a CRM upsert + note,
17
+ so the contact looked updated with no native submission. The submit now
18
+ sends `objectTypeId`, the form's communication consents, skips the
19
+ email-only retry when HubSpot requires other fields, and refuses CAPTCHA
20
+ forms with a stored error. Strapi Submissions distinguish **Conversion**
21
+ vs **Contact only**.
22
+
5
23
  ## 0.16.3 — 2026-09-22
6
24
 
7
25
  ### Changed
@@ -5,12 +5,12 @@ import { u as useIntl } from "./index-CEh8vkxY.mjs";
5
5
  import { Typography, Flex, Box, SingleSelect, SingleSelectOption, MultiSelect, MultiSelectOption, TextInput, IconButton, Button, Combobox, ComboboxOption, Field, Textarea, Checkbox, Divider, Loader, Badge, Table, Thead, Tr, Th, Tbody, Td, Modal } from "@strapi/design-system";
6
6
  import { Trash, Plus, ArrowClockwise, ArrowLeft, Drag, ArrowUp, ArrowDown, List, Duplicate, Download } from "@strapi/icons";
7
7
  import { useFetchClient } from "@strapi/strapi/admin";
8
- import { g as getTranslation, P as PLUGIN_ID } from "./index-B8GYi6sw.mjs";
8
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-BeBn7bJa.mjs";
9
9
  import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, useDroppable, closestCorners } from "@dnd-kit/core";
10
10
  import { arrayMove, sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable } from "@dnd-kit/sortable";
11
11
  import { CSS } from "@dnd-kit/utilities";
12
- import { o as objectLabel } from "./objectLabels-DybqhBdT.mjs";
13
- import { u as useHubspotSchema } from "./useHubspotSchema-DtcHHUOb.mjs";
12
+ import { o as objectLabel } from "./objectLabels-CS_0QCNz.mjs";
13
+ import { u as useHubspotSchema } from "./useHubspotSchema-CP7LRdzO.mjs";
14
14
  const OPERATORS = [
15
15
  "eq",
16
16
  "neq",
@@ -705,6 +705,7 @@ const FormEditor = () => {
705
705
  const locale = searchParams.get("locale") ?? void 0;
706
706
  const [entry, setEntry] = React.useState(null);
707
707
  const [published, setPublished] = React.useState(false);
708
+ const [publishedHubspotFormId, setPublishedHubspotFormId] = React.useState(null);
708
709
  const [locales, setLocales] = React.useState([]);
709
710
  const [selection, setSelection] = React.useState({ kind: "form" });
710
711
  const [dirty, setDirty] = React.useState(false);
@@ -741,6 +742,7 @@ const FormEditor = () => {
741
742
  if (cancelled) return;
742
743
  setEntry(data.form);
743
744
  setPublished(data.published);
745
+ setPublishedHubspotFormId(data.publishedHubspotFormId ?? null);
744
746
  setDirty(false);
745
747
  }).catch(async () => {
746
748
  if (cancelled || !locale) return;
@@ -851,6 +853,7 @@ const FormEditor = () => {
851
853
  try {
852
854
  const { data } = await post(`/${PLUGIN_ID}/builder/forms/${documentId}/publish${query}`);
853
855
  setPublished(true);
856
+ setPublishedHubspotFormId(entry.hubspotFormId ?? null);
854
857
  setErrors([]);
855
858
  setProblems([]);
856
859
  const added = Array.isArray(data?.addedFields) ? data.addedFields.length : 0;
@@ -1339,7 +1342,10 @@ const FormEditor = () => {
1339
1342
  ),
1340
1343
  onChange: (id) => patchEntry({ hubspotFormId: String(id) || null }),
1341
1344
  children: [
1342
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "", children: t("editor.hubspot-form-none", "None — CRM upsert only") }),
1345
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "", children: t(
1346
+ "editor.hubspot-form-none",
1347
+ "Default marketing form (Settings) — contact only if none is set"
1348
+ ) }),
1343
1349
  entry.hubspotFormId && !hsForms.some((f) => f.id === entry.hubspotFormId) && /* @__PURE__ */ jsx(SingleSelectOption, { value: entry.hubspotFormId, children: entry.hubspotFormId }),
1344
1350
  hsForms.map((f) => /* @__PURE__ */ jsx(SingleSelectOption, { value: f.id, children: f.name }, f.id))
1345
1351
  ]
@@ -1355,6 +1361,10 @@ const FormEditor = () => {
1355
1361
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
1356
1362
  "editor.hubspot-form-hint",
1357
1363
  "Submissions go through HubSpot’s Forms API so they count as conversions. After you change the form in HubSpot, use Resync from HubSpot — mapped fields update in place, new ones are appended, your steps stay."
1364
+ ) }),
1365
+ published && (entry.hubspotFormId || "") !== (publishedHubspotFormId || "") && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: t(
1366
+ "editor.hubspot-form-unpublished",
1367
+ "The live site still uses the previously published HubSpot form. Publish to apply this link."
1358
1368
  ) })
1359
1369
  ] }),
1360
1370
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
@@ -1764,7 +1774,7 @@ const Submissions = () => {
1764
1774
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: row.formTitle || row.form }) }),
1765
1775
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: row.email || "—" }) }),
1766
1776
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", ellipsis: true, children: preview(row.values) }) }),
1767
- /* @__PURE__ */ jsx(Td, { children: row.hubspotSynced ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.synced", "Synced") }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
1777
+ /* @__PURE__ */ jsx(Td, { children: row.meta?.hubspotConversion === true || row.meta?.hubspotConversion === "true" ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.converted", "Conversion") }) : row.hubspotSynced ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.contact-only", "Contact only") }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
1768
1778
  ]
1769
1779
  },
1770
1780
  row.documentId
@@ -1800,9 +1810,13 @@ const Submissions = () => {
1800
1810
  /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", tag: "p", children: key }),
1801
1811
  /* @__PURE__ */ jsx(Typography, { tag: "p", children: String(value) })
1802
1812
  ] }, key)),
1803
- selected.meta?.pagePath && /* @__PURE__ */ jsxs(Box, { children: [
1813
+ typeof selected.meta?.pagePath === "string" && selected.meta.pagePath && /* @__PURE__ */ jsxs(Box, { children: [
1804
1814
  /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", tag: "p", children: t("submissions.meta-page", "Submitted from") }),
1805
1815
  /* @__PURE__ */ jsx(Typography, { tag: "p", children: selected.meta.pagePath })
1816
+ ] }),
1817
+ typeof selected.meta?.hubspotError === "string" && selected.meta.hubspotError && /* @__PURE__ */ jsxs(Box, { children: [
1818
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "danger600", tag: "p", children: t("submissions.conversion-error", "Conversion failed") }),
1819
+ /* @__PURE__ */ jsx(Typography, { tag: "p", children: selected.meta.hubspotError })
1806
1820
  ] })
1807
1821
  ] }) })
1808
1822
  ] }) })
@@ -7,12 +7,12 @@ const index = require("./index-DkTxsEqL.js");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
9
  const admin = require("@strapi/strapi/admin");
10
- const index$1 = require("./index-XEuSPuUj.js");
10
+ const index$1 = require("./index-CG_WQzSa.js");
11
11
  const core = require("@dnd-kit/core");
12
12
  const sortable = require("@dnd-kit/sortable");
13
13
  const utilities = require("@dnd-kit/utilities");
14
- const objectLabels = require("./objectLabels-wBwfLdZt.js");
15
- const useHubspotSchema = require("./useHubspotSchema-WvgW3od8.js");
14
+ const objectLabels = require("./objectLabels-BGq-Ypgn.js");
15
+ const useHubspotSchema = require("./useHubspotSchema-BZP2qkAu.js");
16
16
  function _interopNamespace(e) {
17
17
  if (e && e.__esModule) return e;
18
18
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -725,6 +725,7 @@ const FormEditor = () => {
725
725
  const locale = searchParams.get("locale") ?? void 0;
726
726
  const [entry, setEntry] = React__namespace.useState(null);
727
727
  const [published, setPublished] = React__namespace.useState(false);
728
+ const [publishedHubspotFormId, setPublishedHubspotFormId] = React__namespace.useState(null);
728
729
  const [locales, setLocales] = React__namespace.useState([]);
729
730
  const [selection, setSelection] = React__namespace.useState({ kind: "form" });
730
731
  const [dirty, setDirty] = React__namespace.useState(false);
@@ -761,6 +762,7 @@ const FormEditor = () => {
761
762
  if (cancelled) return;
762
763
  setEntry(data.form);
763
764
  setPublished(data.published);
765
+ setPublishedHubspotFormId(data.publishedHubspotFormId ?? null);
764
766
  setDirty(false);
765
767
  }).catch(async () => {
766
768
  if (cancelled || !locale) return;
@@ -871,6 +873,7 @@ const FormEditor = () => {
871
873
  try {
872
874
  const { data } = await post(`/${index$1.PLUGIN_ID}/builder/forms/${documentId}/publish${query}`);
873
875
  setPublished(true);
876
+ setPublishedHubspotFormId(entry.hubspotFormId ?? null);
874
877
  setErrors([]);
875
878
  setProblems([]);
876
879
  const added = Array.isArray(data?.addedFields) ? data.addedFields.length : 0;
@@ -1359,7 +1362,10 @@ const FormEditor = () => {
1359
1362
  ),
1360
1363
  onChange: (id) => patchEntry({ hubspotFormId: String(id) || null }),
1361
1364
  children: [
1362
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "", children: t("editor.hubspot-form-none", "None — CRM upsert only") }),
1365
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "", children: t(
1366
+ "editor.hubspot-form-none",
1367
+ "Default marketing form (Settings) — contact only if none is set"
1368
+ ) }),
1363
1369
  entry.hubspotFormId && !hsForms.some((f) => f.id === entry.hubspotFormId) && /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: entry.hubspotFormId, children: entry.hubspotFormId }),
1364
1370
  hsForms.map((f) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: f.id, children: f.name }, f.id))
1365
1371
  ]
@@ -1375,6 +1381,10 @@ const FormEditor = () => {
1375
1381
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: t(
1376
1382
  "editor.hubspot-form-hint",
1377
1383
  "Submissions go through HubSpot’s Forms API so they count as conversions. After you change the form in HubSpot, use Resync from HubSpot — mapped fields update in place, new ones are appended, your steps stay."
1384
+ ) }),
1385
+ published && (entry.hubspotFormId || "") !== (publishedHubspotFormId || "") && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: t(
1386
+ "editor.hubspot-form-unpublished",
1387
+ "The live site still uses the previously published HubSpot form. Publish to apply this link."
1378
1388
  ) })
1379
1389
  ] }),
1380
1390
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: t(
@@ -1784,7 +1794,7 @@ const Submissions = () => {
1784
1794
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { children: row.formTitle || row.form }) }),
1785
1795
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "semiBold", children: row.email || "—" }) }),
1786
1796
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", ellipsis: true, children: preview(row.values) }) }),
1787
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: row.hubspotSynced ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.synced", "Synced") }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
1797
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: row.meta?.hubspotConversion === true || row.meta?.hubspotConversion === "true" ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.converted", "Conversion") }) : row.hubspotSynced ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.contact-only", "Contact only") }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
1788
1798
  ]
1789
1799
  },
1790
1800
  row.documentId
@@ -1820,9 +1830,13 @@ const Submissions = () => {
1820
1830
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", tag: "p", children: key }),
1821
1831
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { tag: "p", children: String(value) })
1822
1832
  ] }, key)),
1823
- selected.meta?.pagePath && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
1833
+ typeof selected.meta?.pagePath === "string" && selected.meta.pagePath && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
1824
1834
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", tag: "p", children: t("submissions.meta-page", "Submitted from") }),
1825
1835
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { tag: "p", children: selected.meta.pagePath })
1836
+ ] }),
1837
+ typeof selected.meta?.hubspotError === "string" && selected.meta.hubspotError && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
1838
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "danger600", tag: "p", children: t("submissions.conversion-error", "Conversion failed") }),
1839
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { tag: "p", children: selected.meta.hubspotError })
1826
1840
  ] })
1827
1841
  ] }) })
1828
1842
  ] }) })
@@ -5,7 +5,7 @@ const React = require("react");
5
5
  const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const admin = require("@strapi/strapi/admin");
8
- const index$1 = require("./index-XEuSPuUj.js");
8
+ const index$1 = require("./index-CG_WQzSa.js");
9
9
  function _interopNamespace(e) {
10
10
  if (e && e.__esModule) return e;
11
11
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -3,7 +3,7 @@ import * as React from "react";
3
3
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Field, Flex, Loader, Combobox, ComboboxOption } from "@strapi/design-system";
5
5
  import { useFetchClient } from "@strapi/strapi/admin";
6
- import { P as PLUGIN_ID, g as getTranslation } from "./index-B8GYi6sw.mjs";
6
+ import { P as PLUGIN_ID, g as getTranslation } from "./index-BeBn7bJa.mjs";
7
7
  const HubspotFormInput = React.forwardRef(
8
8
  ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
9
9
  const { formatMessage } = useIntl();
@@ -4,9 +4,9 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const React = require("react");
5
5
  const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
- const index$1 = require("./index-XEuSPuUj.js");
8
- const objectLabels = require("./objectLabels-wBwfLdZt.js");
9
- const useHubspotSchema = require("./useHubspotSchema-WvgW3od8.js");
7
+ const index$1 = require("./index-CG_WQzSa.js");
8
+ const objectLabels = require("./objectLabels-BGq-Ypgn.js");
9
+ const useHubspotSchema = require("./useHubspotSchema-BZP2qkAu.js");
10
10
  function _interopNamespace(e) {
11
11
  if (e && e.__esModule) return e;
12
12
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -2,9 +2,9 @@ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Field, Flex, Loader, SingleSelect, SingleSelectOption } from "@strapi/design-system";
5
- import { g as getTranslation } from "./index-B8GYi6sw.mjs";
6
- import { o as objectLabel } from "./objectLabels-DybqhBdT.mjs";
7
- import { u as useHubspotSchema } from "./useHubspotSchema-DtcHHUOb.mjs";
5
+ import { g as getTranslation } from "./index-BeBn7bJa.mjs";
6
+ import { o as objectLabel } from "./objectLabels-CS_0QCNz.mjs";
7
+ import { u as useHubspotSchema } from "./useHubspotSchema-CP7LRdzO.mjs";
8
8
  const HubspotObjectInput = React.forwardRef(
9
9
  ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
10
10
  const intl = useIntl();
@@ -6,9 +6,9 @@ const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const icons = require("@strapi/icons");
8
8
  const admin = require("@strapi/strapi/admin");
9
- const index$1 = require("./index-XEuSPuUj.js");
10
- const objectLabels = require("./objectLabels-wBwfLdZt.js");
11
- const useHubspotSchema = require("./useHubspotSchema-WvgW3od8.js");
9
+ const index$1 = require("./index-CG_WQzSa.js");
10
+ const objectLabels = require("./objectLabels-BGq-Ypgn.js");
11
+ const useHubspotSchema = require("./useHubspotSchema-BZP2qkAu.js");
12
12
  function _interopNamespace(e) {
13
13
  if (e && e.__esModule) return e;
14
14
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -4,9 +4,9 @@ import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Field, Flex, Loader, Combobox, ComboboxOption, IconButton, Button, Typography, Link } from "@strapi/design-system";
5
5
  import { ArrowClockwise } from "@strapi/icons";
6
6
  import { useField } from "@strapi/strapi/admin";
7
- import { g as getTranslation } from "./index-B8GYi6sw.mjs";
8
- import { o as objectLabel } from "./objectLabels-DybqhBdT.mjs";
9
- import { u as useHubspotSchema } from "./useHubspotSchema-DtcHHUOb.mjs";
7
+ import { g as getTranslation } from "./index-BeBn7bJa.mjs";
8
+ import { o as objectLabel } from "./objectLabels-CS_0QCNz.mjs";
9
+ import { u as useHubspotSchema } from "./useHubspotSchema-CP7LRdzO.mjs";
10
10
  const oneLine = {
11
11
  display: "block",
12
12
  whiteSpace: "nowrap",
@@ -5,9 +5,9 @@ const React = require("react");
5
5
  const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const admin = require("@strapi/strapi/admin");
8
- const index$1 = require("./index-XEuSPuUj.js");
8
+ const index$1 = require("./index-CG_WQzSa.js");
9
9
  const reactRouterDom = require("react-router-dom");
10
- const objectLabels = require("./objectLabels-wBwfLdZt.js");
10
+ const objectLabels = require("./objectLabels-BGq-Ypgn.js");
11
11
  function _interopNamespace(e) {
12
12
  if (e && e.__esModule) return e;
13
13
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -3,9 +3,9 @@ import * as React from "react";
3
3
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Flex, Divider, Typography, Button, Box, Link, Badge, Loader, Field, SingleSelect, SingleSelectOption, Checkbox } from "@strapi/design-system";
5
5
  import { useFetchClient } from "@strapi/strapi/admin";
6
- import { g as getTranslation, P as PLUGIN_ID } from "./index-B8GYi6sw.mjs";
6
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-BeBn7bJa.mjs";
7
7
  import { Link as Link$1 } from "react-router-dom";
8
- import { o as objectLabel } from "./objectLabels-DybqhBdT.mjs";
8
+ import { o as objectLabel } from "./objectLabels-CS_0QCNz.mjs";
9
9
  const AuditSection = ({ configured }) => {
10
10
  const intl = useIntl();
11
11
  const { formatMessage } = intl;
@@ -163,8 +163,9 @@ const en = {
163
163
  "editor.form-success": "Success message",
164
164
  "editor.hubspot-form": "HubSpot marketing form",
165
165
  "editor.hubspot-form-placeholder": "Pick a form of the connected portal",
166
- "editor.hubspot-form-none": "None — CRM upsert only",
166
+ "editor.hubspot-form-none": "Default marketing form (Settings) — contact only if none is set",
167
167
  "editor.hubspot-form-hint": "Submissions go through HubSpot’s Forms API so they count as conversions. After you change the form in HubSpot, use Resync from HubSpot — mapped fields update in place, new ones are appended, your steps stay.",
168
+ "editor.hubspot-form-unpublished": "The live site still uses the previously published HubSpot form. Publish to apply this link.",
168
169
  "editor.resync": "Resync from HubSpot",
169
170
  "editor.resynced": "Resynced from HubSpot — {updated, plural, one {# field updated} other {# fields updated}}, {added, plural, one {# added} other {# added}}.",
170
171
  "editor.resynced-gaps": "Resynced from HubSpot — {updated, plural, one {# updated} other {# updated}}, {added, plural, one {# added} other {# added}}, {skipped, plural, one {# skipped} other {# skipped}}.",
@@ -269,6 +270,9 @@ const en = {
269
270
  "submissions.col-answers": "Answers",
270
271
  "submissions.col-crm": "CRM",
271
272
  "submissions.synced": "Synced",
273
+ "submissions.converted": "Conversion",
274
+ "submissions.contact-only": "Contact only",
275
+ "submissions.conversion-error": "Conversion failed",
272
276
  "submissions.not-synced": "Not synced",
273
277
  "submissions.count": "{total, plural, one {# submission} other {# submissions}}",
274
278
  "submissions.prev": "Previous",
@@ -165,8 +165,9 @@ const en = {
165
165
  "editor.form-success": "Success message",
166
166
  "editor.hubspot-form": "HubSpot marketing form",
167
167
  "editor.hubspot-form-placeholder": "Pick a form of the connected portal",
168
- "editor.hubspot-form-none": "None — CRM upsert only",
168
+ "editor.hubspot-form-none": "Default marketing form (Settings) — contact only if none is set",
169
169
  "editor.hubspot-form-hint": "Submissions go through HubSpot’s Forms API so they count as conversions. After you change the form in HubSpot, use Resync from HubSpot — mapped fields update in place, new ones are appended, your steps stay.",
170
+ "editor.hubspot-form-unpublished": "The live site still uses the previously published HubSpot form. Publish to apply this link.",
170
171
  "editor.resync": "Resync from HubSpot",
171
172
  "editor.resynced": "Resynced from HubSpot — {updated, plural, one {# field updated} other {# fields updated}}, {added, plural, one {# added} other {# added}}.",
172
173
  "editor.resynced-gaps": "Resynced from HubSpot — {updated, plural, one {# updated} other {# updated}}, {added, plural, one {# added} other {# added}}, {skipped, plural, one {# skipped} other {# skipped}}.",
@@ -271,6 +272,9 @@ const en = {
271
272
  "submissions.col-answers": "Answers",
272
273
  "submissions.col-crm": "CRM",
273
274
  "submissions.synced": "Synced",
275
+ "submissions.converted": "Conversion",
276
+ "submissions.contact-only": "Contact only",
277
+ "submissions.conversion-error": "Conversion failed",
274
278
  "submissions.not-synced": "Not synced",
275
279
  "submissions.count": "{total, plural, one {# submission} other {# submissions}}",
276
280
  "submissions.prev": "Previous",
@@ -163,8 +163,9 @@ const fr = {
163
163
  "editor.form-success": "Message de succès",
164
164
  "editor.hubspot-form": "Formulaire marketing HubSpot",
165
165
  "editor.hubspot-form-placeholder": "Choisir un formulaire du portail connecté",
166
- "editor.hubspot-form-none": "Aucun — upsert CRM uniquement",
166
+ "editor.hubspot-form-none": "Formulaire marketing par défaut (Réglages) — contact seul si aucun n'est défini",
167
167
  "editor.hubspot-form-hint": "Les envois passent par l’API Forms de HubSpot pour compter comme des conversions. Après une modification dans HubSpot, utilisez Resynchroniser — les champs mappés se mettent à jour sur place, les nouveaux s’ajoutent, vos étapes restent.",
168
+ "editor.hubspot-form-unpublished": "Le site live utilise encore le formulaire HubSpot précédemment publié. Publiez pour appliquer ce lien.",
168
169
  "editor.resync": "Resynchroniser depuis HubSpot",
169
170
  "editor.resynced": "Resynchronisé depuis HubSpot — {updated, plural, one {# champ mis à jour} other {# champs mis à jour}}, {added, plural, one {# ajouté} other {# ajoutés}}.",
170
171
  "editor.resynced-gaps": "Resynchronisé depuis HubSpot — {updated, plural, one {# mis à jour} other {# mis à jour}}, {added, plural, one {# ajouté} other {# ajoutés}}, {skipped, plural, one {# écarté} other {# écartés}}.",
@@ -269,6 +270,9 @@ const fr = {
269
270
  "submissions.col-answers": "Réponses",
270
271
  "submissions.col-crm": "CRM",
271
272
  "submissions.synced": "Synchronisée",
273
+ "submissions.converted": "Conversion",
274
+ "submissions.contact-only": "Contact seul",
275
+ "submissions.conversion-error": "Conversion échouée",
272
276
  "submissions.not-synced": "Non synchronisée",
273
277
  "submissions.count": "{total, plural, one {# soumission} other {# soumissions}}",
274
278
  "submissions.prev": "Précédent",
@@ -165,8 +165,9 @@ const fr = {
165
165
  "editor.form-success": "Message de succès",
166
166
  "editor.hubspot-form": "Formulaire marketing HubSpot",
167
167
  "editor.hubspot-form-placeholder": "Choisir un formulaire du portail connecté",
168
- "editor.hubspot-form-none": "Aucun — upsert CRM uniquement",
168
+ "editor.hubspot-form-none": "Formulaire marketing par défaut (Réglages) — contact seul si aucun n'est défini",
169
169
  "editor.hubspot-form-hint": "Les envois passent par l’API Forms de HubSpot pour compter comme des conversions. Après une modification dans HubSpot, utilisez Resynchroniser — les champs mappés se mettent à jour sur place, les nouveaux s’ajoutent, vos étapes restent.",
170
+ "editor.hubspot-form-unpublished": "Le site live utilise encore le formulaire HubSpot précédemment publié. Publiez pour appliquer ce lien.",
170
171
  "editor.resync": "Resynchroniser depuis HubSpot",
171
172
  "editor.resynced": "Resynchronisé depuis HubSpot — {updated, plural, one {# champ mis à jour} other {# champs mis à jour}}, {added, plural, one {# ajouté} other {# ajoutés}}.",
172
173
  "editor.resynced-gaps": "Resynchronisé depuis HubSpot — {updated, plural, one {# mis à jour} other {# mis à jour}}, {added, plural, one {# ajouté} other {# ajoutés}}, {skipped, plural, one {# écarté} other {# écartés}}.",
@@ -271,6 +272,9 @@ const fr = {
271
272
  "submissions.col-answers": "Réponses",
272
273
  "submissions.col-crm": "CRM",
273
274
  "submissions.synced": "Synchronisée",
275
+ "submissions.converted": "Conversion",
276
+ "submissions.contact-only": "Contact seul",
277
+ "submissions.conversion-error": "Conversion échouée",
274
278
  "submissions.not-synced": "Non synchronisée",
275
279
  "submissions.count": "{total, plural, one {# soumission} other {# soumissions}}",
276
280
  "submissions.prev": "Précédent",
@@ -25,7 +25,7 @@ const index = {
25
25
  icon: PaperPlane,
26
26
  intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "HubSpot Forms" },
27
27
  permissions: [{ action: `plugin::${PLUGIN_ID}.forms`, subject: null }],
28
- Component: async () => (await import("./Forms-BBaw53Tz.mjs")).default
28
+ Component: async () => (await import("./Forms-DyV6KyiO.mjs")).default
29
29
  });
30
30
  app.customFields.register({
31
31
  name: "property",
@@ -40,7 +40,7 @@ const index = {
40
40
  defaultMessage: "Picked from the portal's real properties"
41
41
  },
42
42
  components: {
43
- Input: async () => import("./HubspotPropertyInput-Dsn7ikop.mjs")
43
+ Input: async () => import("./HubspotPropertyInput-WU5FMJj3.mjs")
44
44
  },
45
45
  // Exposed in the Content-Type Builder. Without this, `objectField` could
46
46
  // only be set by hand-editing schema.json — which is not how anyone
@@ -103,7 +103,7 @@ const index = {
103
103
  defaultMessage: "Picked from the portal's configured objects"
104
104
  },
105
105
  components: {
106
- Input: async () => import("./HubspotObjectInput-BA16dswz.mjs")
106
+ Input: async () => import("./HubspotObjectInput-DL8fYK2x.mjs")
107
107
  }
108
108
  });
109
109
  app.customFields.register({
@@ -119,7 +119,7 @@ const index = {
119
119
  defaultMessage: "A form built in the HubSpot form builder, stored by slug"
120
120
  },
121
121
  components: {
122
- Input: async () => import("./HubspotFormInput-DIo6hCNz.mjs")
122
+ Input: async () => import("./HubspotFormInput-DEXPX-6j.mjs")
123
123
  }
124
124
  });
125
125
  app.createSettingSection(
@@ -135,7 +135,7 @@ const index = {
135
135
  // Mirrors the RBAC action registered server-side: a role without it
136
136
  // doesn't see the link at all — the routes refuse it anyway.
137
137
  permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }],
138
- Component: async () => (await import("./Settings-DxX9Uh_2.mjs")).default
138
+ Component: async () => (await import("./Settings-CiXEps_C.mjs")).default
139
139
  }
140
140
  ]
141
141
  );
@@ -145,7 +145,7 @@ const index = {
145
145
  return Promise.all(
146
146
  locales.map(async (locale) => {
147
147
  try {
148
- const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-Djd3TID0.mjs"), "./translations/fr.json": () => import("./fr-C7t1elmR.mjs") }), `./translations/${locale}.json`, 3);
148
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-BIUBYzHv.mjs"), "./translations/fr.json": () => import("./fr-CMYcnFH0.mjs") }), `./translations/${locale}.json`, 3);
149
149
  return { data: prefixPluginTranslations(data), locale };
150
150
  } catch {
151
151
  return { data: {}, locale };
@@ -26,7 +26,7 @@ const index = {
26
26
  icon: icons.PaperPlane,
27
27
  intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "HubSpot Forms" },
28
28
  permissions: [{ action: `plugin::${PLUGIN_ID}.forms`, subject: null }],
29
- Component: async () => (await Promise.resolve().then(() => require("./Forms-CIJrUB_n.js"))).default
29
+ Component: async () => (await Promise.resolve().then(() => require("./Forms-uUN4XjhW.js"))).default
30
30
  });
31
31
  app.customFields.register({
32
32
  name: "property",
@@ -41,7 +41,7 @@ const index = {
41
41
  defaultMessage: "Picked from the portal's real properties"
42
42
  },
43
43
  components: {
44
- Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-C77Op1Wm.js"))
44
+ Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-CFMkbQeT.js"))
45
45
  },
46
46
  // Exposed in the Content-Type Builder. Without this, `objectField` could
47
47
  // only be set by hand-editing schema.json — which is not how anyone
@@ -104,7 +104,7 @@ const index = {
104
104
  defaultMessage: "Picked from the portal's configured objects"
105
105
  },
106
106
  components: {
107
- Input: async () => Promise.resolve().then(() => require("./HubspotObjectInput-BIqC8tl2.js"))
107
+ Input: async () => Promise.resolve().then(() => require("./HubspotObjectInput-CFDYW-TM.js"))
108
108
  }
109
109
  });
110
110
  app.customFields.register({
@@ -120,7 +120,7 @@ const index = {
120
120
  defaultMessage: "A form built in the HubSpot form builder, stored by slug"
121
121
  },
122
122
  components: {
123
- Input: async () => Promise.resolve().then(() => require("./HubspotFormInput-DSTuQ_bx.js"))
123
+ Input: async () => Promise.resolve().then(() => require("./HubspotFormInput-BkWat_wu.js"))
124
124
  }
125
125
  });
126
126
  app.createSettingSection(
@@ -136,7 +136,7 @@ const index = {
136
136
  // Mirrors the RBAC action registered server-side: a role without it
137
137
  // doesn't see the link at all — the routes refuse it anyway.
138
138
  permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }],
139
- Component: async () => (await Promise.resolve().then(() => require("./Settings-PVhJVPqq.js"))).default
139
+ Component: async () => (await Promise.resolve().then(() => require("./Settings-B_hhdpHa.js"))).default
140
140
  }
141
141
  ]
142
142
  );
@@ -146,7 +146,7 @@ const index = {
146
146
  return Promise.all(
147
147
  locales.map(async (locale) => {
148
148
  try {
149
- const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-sfzopB5K.js")), "./translations/fr.json": () => Promise.resolve().then(() => require("./fr-sF5Josfh.js")) }), `./translations/${locale}.json`, 3);
149
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-CFhEtgZJ.js")), "./translations/fr.json": () => Promise.resolve().then(() => require("./fr-zdUuuV0M.js")) }), `./translations/${locale}.json`, 3);
150
150
  return { data: prefixPluginTranslations(data), locale };
151
151
  } catch {
152
152
  return { data: {}, locale };
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const index = require("./index-XEuSPuUj.js");
2
+ const index = require("./index-CG_WQzSa.js");
3
3
  const STANDARD = /* @__PURE__ */ new Set([
4
4
  "contact",
5
5
  "company",
@@ -1,4 +1,4 @@
1
- import { g as getTranslation } from "./index-B8GYi6sw.mjs";
1
+ import { g as getTranslation } from "./index-BeBn7bJa.mjs";
2
2
  const STANDARD = /* @__PURE__ */ new Set([
3
3
  "contact",
4
4
  "company",
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  const React = require("react");
3
3
  const admin = require("@strapi/strapi/admin");
4
- const index = require("./index-XEuSPuUj.js");
4
+ const index = require("./index-CG_WQzSa.js");
5
5
  function _interopNamespace(e) {
6
6
  if (e && e.__esModule) return e;
7
7
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { useFetchClient } from "@strapi/strapi/admin";
3
- import { P as PLUGIN_ID } from "./index-B8GYi6sw.mjs";
3
+ import { P as PLUGIN_ID } from "./index-BeBn7bJa.mjs";
4
4
  let cached = null;
5
5
  let inFlight = null;
6
6
  const listeners = /* @__PURE__ */ new Set();
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-XEuSPuUj.js");
2
+ const index = require("../_chunks/index-CG_WQzSa.js");
3
3
  require("@strapi/icons");
4
4
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-B8GYi6sw.mjs";
1
+ import { i } from "../_chunks/index-BeBn7bJa.mjs";
2
2
  import "@strapi/icons";
3
3
  export {
4
4
  i as default
@@ -103,7 +103,7 @@ export interface SubmissionRowDto {
103
103
  formTitle?: string | null;
104
104
  email?: string | null;
105
105
  values: Record<string, string | number | boolean>;
106
- meta?: Record<string, string> | null;
106
+ meta?: Record<string, unknown> | null;
107
107
  locale?: string | null;
108
108
  hubspotSynced?: boolean;
109
109
  createdAt?: string;
@@ -1936,6 +1936,59 @@ function formFieldNames(raw) {
1936
1936
  }
1937
1937
  return names;
1938
1938
  }
1939
+ function formRequiredNames(raw) {
1940
+ const names = [];
1941
+ for (const group of raw.fieldGroups ?? []) {
1942
+ for (const field of group.fields ?? []) {
1943
+ if (field.name && field.required) names.push(field.name);
1944
+ }
1945
+ }
1946
+ return names;
1947
+ }
1948
+ function parseFormShape(raw) {
1949
+ const legal = raw.legalConsentOptions;
1950
+ const hasLegalConsent = Boolean(
1951
+ legal && typeof legal === "object" && legal.type && legal.type !== "none"
1952
+ );
1953
+ const communications = [];
1954
+ for (const box of legal?.communicationsCheckboxes ?? []) {
1955
+ const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
1956
+ if (!Number.isFinite(id) || id <= 0) continue;
1957
+ communications.push({
1958
+ subscriptionTypeId: id,
1959
+ text: String(box.label || box.text || "").trim() || "I agree to receive communications."
1960
+ });
1961
+ }
1962
+ const consentToProcessText = legal?.consentToProcessText?.trim();
1963
+ return {
1964
+ names: new Set(formFieldNames(raw)),
1965
+ required: new Set(formRequiredNames(raw)),
1966
+ hasLegalConsent,
1967
+ captcha: raw.configuration?.captchaEnabled === true,
1968
+ ...consentToProcessText ? { consentToProcessText } : {},
1969
+ communications
1970
+ };
1971
+ }
1972
+ function canRetryEmailOnly(required) {
1973
+ const names = [...required ?? []];
1974
+ return names.length === 0 || names.every((name) => name === "email");
1975
+ }
1976
+ function buildLegalConsent(shape, given, fallbackText) {
1977
+ if (!given) return void 0;
1978
+ if (shape && !shape.hasLegalConsent) return void 0;
1979
+ const consent = {
1980
+ consentToProcess: true,
1981
+ text: shape?.consentToProcessText?.trim() || fallbackText?.trim() || DEFAULT_CONSENT_TEXT
1982
+ };
1983
+ if (shape?.communications.length) {
1984
+ consent.communications = shape.communications.map((item) => ({
1985
+ value: true,
1986
+ subscriptionTypeId: item.subscriptionTypeId,
1987
+ text: item.text
1988
+ }));
1989
+ }
1990
+ return { consent };
1991
+ }
1939
1992
  const shapeCache = /* @__PURE__ */ new Map();
1940
1993
  function clearFormShapeCache(formGuid) {
1941
1994
  if (formGuid) shapeCache.delete(formGuid);
@@ -1944,16 +1997,13 @@ function clearFormShapeCache(formGuid) {
1944
1997
  async function loadFormShape(apiKey, formGuid) {
1945
1998
  const cached = shapeCache.get(formGuid);
1946
1999
  if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
1947
- return { names: cached.names, hasLegalConsent: cached.hasLegalConsent };
2000
+ return cached.shape;
1948
2001
  }
1949
2002
  try {
1950
2003
  const raw = await fetchHubspotForm(apiKey, formGuid);
1951
- const names = new Set(formFieldNames(raw));
1952
- const hasLegalConsent = Boolean(
1953
- raw.legalConsentOptions && typeof raw.legalConsentOptions === "object" && raw.legalConsentOptions.type && raw.legalConsentOptions.type !== "none"
1954
- );
1955
- shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
1956
- return { names, hasLegalConsent };
2004
+ const shape = parseFormShape(raw);
2005
+ shapeCache.set(formGuid, { at: Date.now(), shape });
2006
+ return shape;
1957
2007
  } catch {
1958
2008
  return null;
1959
2009
  }
@@ -2011,28 +2061,45 @@ async function submitMarketingForm(input) {
2011
2061
  if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
2012
2062
  return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
2013
2063
  }
2064
+ if (input.shape?.captcha) {
2065
+ return {
2066
+ ok: false,
2067
+ status: 0,
2068
+ error: "HubSpot form has CAPTCHA enabled — the Forms API cannot submit it. Turn CAPTCHA off on that marketing form."
2069
+ };
2070
+ }
2014
2071
  const url = formsSubmitUrl(portalId, formGuid, input.region);
2015
2072
  const context = contextPayload(input.context);
2073
+ const fields = input.fields.map((field) => ({
2074
+ objectTypeId: field.objectTypeId || "0-1",
2075
+ name: field.name,
2076
+ value: field.value
2077
+ }));
2016
2078
  const base = {
2017
- fields: input.fields,
2079
+ fields,
2018
2080
  ...Object.keys(context).length ? { context } : {}
2019
2081
  };
2020
- const withConsent = input.consent?.given && input.formHasLegalConsent !== false ? {
2021
- ...base,
2022
- legalConsentOptions: {
2023
- consent: {
2024
- consentToProcess: true,
2025
- text: input.consent.text?.trim() || DEFAULT_CONSENT_TEXT
2026
- }
2027
- }
2028
- } : base;
2082
+ const legal = buildLegalConsent(
2083
+ input.shape,
2084
+ input.consent?.given === true,
2085
+ input.consent?.text
2086
+ );
2087
+ const withConsent = legal ? { ...base, legalConsentOptions: legal } : base;
2029
2088
  let result = await postForm(url, input.apiKey, withConsent);
2030
- if (!result.ok && result.status === 400 && withConsent !== base) {
2089
+ const mustKeepConsent = Boolean(input.shape?.hasLegalConsent && legal);
2090
+ if (!result.ok && result.status === 400 && withConsent !== base && !mustKeepConsent) {
2031
2091
  result = await postForm(url, input.apiKey, base);
2032
2092
  }
2033
- if (!result.ok && result.status === 400 && input.fields.length > 1) {
2034
- const email = input.fields.find((f) => f.name === "email");
2035
- if (email) result = await postForm(url, input.apiKey, { ...base, fields: [email] });
2093
+ if (!result.ok && result.status === 400 && input.fields.length > 1 && canRetryEmailOnly(input.shape?.required)) {
2094
+ const email = fields.find((f) => f.name === "email");
2095
+ if (email) {
2096
+ const slim = { ...base, fields: [email] };
2097
+ result = await postForm(
2098
+ url,
2099
+ input.apiKey,
2100
+ legal && mustKeepConsent ? { ...slim, legalConsentOptions: legal } : slim
2101
+ );
2102
+ }
2036
2103
  }
2037
2104
  return result;
2038
2105
  }
@@ -2414,6 +2481,7 @@ function createFormsService(strapi, _opts = {}) {
2414
2481
  }
2415
2482
  let usedFormsApi = false;
2416
2483
  let sentFieldNames = [];
2484
+ let conversionError;
2417
2485
  if (email && apiKey) {
2418
2486
  const groups = groupByObject(form.definition, resolution.values);
2419
2487
  for (const [object, props] of Object.entries(companyGroups)) {
@@ -2453,7 +2521,7 @@ function createFormsService(strapi, _opts = {}) {
2453
2521
  ipAddress: sanitizeIp(meta.ipAddress)
2454
2522
  },
2455
2523
  consent: { given: meta.consent === true },
2456
- formHasLegalConsent: shape?.hasLegalConsent
2524
+ shape
2457
2525
  });
2458
2526
  if (submitted.ok) {
2459
2527
  hubspotSynced = true;
@@ -2466,10 +2534,12 @@ function createFormsService(strapi, _opts = {}) {
2466
2534
  );
2467
2535
  }
2468
2536
  } else {
2537
+ conversionError = submitted.error;
2469
2538
  strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
2470
2539
  }
2471
2540
  } catch (err) {
2472
- strapi.log.warn(`[hubspot] Forms API submit failed — ${err.message}`);
2541
+ conversionError = err.message;
2542
+ strapi.log.warn(`[hubspot] Forms API submit failed — ${conversionError}`);
2473
2543
  }
2474
2544
  }
2475
2545
  if (!usedFormsApi) {
@@ -2551,7 +2621,11 @@ function createFormsService(strapi, _opts = {}) {
2551
2621
  formTitle: form.title || form.name,
2552
2622
  email,
2553
2623
  values: resolution.values,
2554
- meta,
2624
+ meta: {
2625
+ ...meta,
2626
+ hubspotConversion: usedFormsApi,
2627
+ ...conversionError ? { hubspotError: conversionError } : {}
2628
+ },
2555
2629
  locale: form.locale ?? null,
2556
2630
  hubspotSynced,
2557
2631
  contactId,
@@ -2738,7 +2812,7 @@ function createFormsAdminController(strapi) {
2738
2812
  locale: ctx.query.locale,
2739
2813
  status: "published"
2740
2814
  });
2741
- ctx.body = { form: entry, published: Boolean(published) };
2815
+ ctx.body = { form: entry, published: Boolean(published), publishedHubspotFormId: published?.hubspotFormId ?? null };
2742
2816
  },
2743
2817
  async create(ctx) {
2744
2818
  const body = ctx.request.body ?? {};
@@ -1935,6 +1935,59 @@ function formFieldNames(raw) {
1935
1935
  }
1936
1936
  return names;
1937
1937
  }
1938
+ function formRequiredNames(raw) {
1939
+ const names = [];
1940
+ for (const group of raw.fieldGroups ?? []) {
1941
+ for (const field of group.fields ?? []) {
1942
+ if (field.name && field.required) names.push(field.name);
1943
+ }
1944
+ }
1945
+ return names;
1946
+ }
1947
+ function parseFormShape(raw) {
1948
+ const legal = raw.legalConsentOptions;
1949
+ const hasLegalConsent = Boolean(
1950
+ legal && typeof legal === "object" && legal.type && legal.type !== "none"
1951
+ );
1952
+ const communications = [];
1953
+ for (const box of legal?.communicationsCheckboxes ?? []) {
1954
+ const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
1955
+ if (!Number.isFinite(id) || id <= 0) continue;
1956
+ communications.push({
1957
+ subscriptionTypeId: id,
1958
+ text: String(box.label || box.text || "").trim() || "I agree to receive communications."
1959
+ });
1960
+ }
1961
+ const consentToProcessText = legal?.consentToProcessText?.trim();
1962
+ return {
1963
+ names: new Set(formFieldNames(raw)),
1964
+ required: new Set(formRequiredNames(raw)),
1965
+ hasLegalConsent,
1966
+ captcha: raw.configuration?.captchaEnabled === true,
1967
+ ...consentToProcessText ? { consentToProcessText } : {},
1968
+ communications
1969
+ };
1970
+ }
1971
+ function canRetryEmailOnly(required) {
1972
+ const names = [...required ?? []];
1973
+ return names.length === 0 || names.every((name) => name === "email");
1974
+ }
1975
+ function buildLegalConsent(shape, given, fallbackText) {
1976
+ if (!given) return void 0;
1977
+ if (shape && !shape.hasLegalConsent) return void 0;
1978
+ const consent = {
1979
+ consentToProcess: true,
1980
+ text: shape?.consentToProcessText?.trim() || fallbackText?.trim() || DEFAULT_CONSENT_TEXT
1981
+ };
1982
+ if (shape?.communications.length) {
1983
+ consent.communications = shape.communications.map((item) => ({
1984
+ value: true,
1985
+ subscriptionTypeId: item.subscriptionTypeId,
1986
+ text: item.text
1987
+ }));
1988
+ }
1989
+ return { consent };
1990
+ }
1938
1991
  const shapeCache = /* @__PURE__ */ new Map();
1939
1992
  function clearFormShapeCache(formGuid) {
1940
1993
  if (formGuid) shapeCache.delete(formGuid);
@@ -1943,16 +1996,13 @@ function clearFormShapeCache(formGuid) {
1943
1996
  async function loadFormShape(apiKey, formGuid) {
1944
1997
  const cached = shapeCache.get(formGuid);
1945
1998
  if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
1946
- return { names: cached.names, hasLegalConsent: cached.hasLegalConsent };
1999
+ return cached.shape;
1947
2000
  }
1948
2001
  try {
1949
2002
  const raw = await fetchHubspotForm(apiKey, formGuid);
1950
- const names = new Set(formFieldNames(raw));
1951
- const hasLegalConsent = Boolean(
1952
- raw.legalConsentOptions && typeof raw.legalConsentOptions === "object" && raw.legalConsentOptions.type && raw.legalConsentOptions.type !== "none"
1953
- );
1954
- shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
1955
- return { names, hasLegalConsent };
2003
+ const shape = parseFormShape(raw);
2004
+ shapeCache.set(formGuid, { at: Date.now(), shape });
2005
+ return shape;
1956
2006
  } catch {
1957
2007
  return null;
1958
2008
  }
@@ -2010,28 +2060,45 @@ async function submitMarketingForm(input) {
2010
2060
  if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
2011
2061
  return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
2012
2062
  }
2063
+ if (input.shape?.captcha) {
2064
+ return {
2065
+ ok: false,
2066
+ status: 0,
2067
+ error: "HubSpot form has CAPTCHA enabled — the Forms API cannot submit it. Turn CAPTCHA off on that marketing form."
2068
+ };
2069
+ }
2013
2070
  const url = formsSubmitUrl(portalId, formGuid, input.region);
2014
2071
  const context = contextPayload(input.context);
2072
+ const fields = input.fields.map((field) => ({
2073
+ objectTypeId: field.objectTypeId || "0-1",
2074
+ name: field.name,
2075
+ value: field.value
2076
+ }));
2015
2077
  const base = {
2016
- fields: input.fields,
2078
+ fields,
2017
2079
  ...Object.keys(context).length ? { context } : {}
2018
2080
  };
2019
- const withConsent = input.consent?.given && input.formHasLegalConsent !== false ? {
2020
- ...base,
2021
- legalConsentOptions: {
2022
- consent: {
2023
- consentToProcess: true,
2024
- text: input.consent.text?.trim() || DEFAULT_CONSENT_TEXT
2025
- }
2026
- }
2027
- } : base;
2081
+ const legal = buildLegalConsent(
2082
+ input.shape,
2083
+ input.consent?.given === true,
2084
+ input.consent?.text
2085
+ );
2086
+ const withConsent = legal ? { ...base, legalConsentOptions: legal } : base;
2028
2087
  let result = await postForm(url, input.apiKey, withConsent);
2029
- if (!result.ok && result.status === 400 && withConsent !== base) {
2088
+ const mustKeepConsent = Boolean(input.shape?.hasLegalConsent && legal);
2089
+ if (!result.ok && result.status === 400 && withConsent !== base && !mustKeepConsent) {
2030
2090
  result = await postForm(url, input.apiKey, base);
2031
2091
  }
2032
- if (!result.ok && result.status === 400 && input.fields.length > 1) {
2033
- const email = input.fields.find((f) => f.name === "email");
2034
- if (email) result = await postForm(url, input.apiKey, { ...base, fields: [email] });
2092
+ if (!result.ok && result.status === 400 && input.fields.length > 1 && canRetryEmailOnly(input.shape?.required)) {
2093
+ const email = fields.find((f) => f.name === "email");
2094
+ if (email) {
2095
+ const slim = { ...base, fields: [email] };
2096
+ result = await postForm(
2097
+ url,
2098
+ input.apiKey,
2099
+ legal && mustKeepConsent ? { ...slim, legalConsentOptions: legal } : slim
2100
+ );
2101
+ }
2035
2102
  }
2036
2103
  return result;
2037
2104
  }
@@ -2413,6 +2480,7 @@ function createFormsService(strapi, _opts = {}) {
2413
2480
  }
2414
2481
  let usedFormsApi = false;
2415
2482
  let sentFieldNames = [];
2483
+ let conversionError;
2416
2484
  if (email && apiKey) {
2417
2485
  const groups = groupByObject(form.definition, resolution.values);
2418
2486
  for (const [object, props] of Object.entries(companyGroups)) {
@@ -2452,7 +2520,7 @@ function createFormsService(strapi, _opts = {}) {
2452
2520
  ipAddress: sanitizeIp(meta.ipAddress)
2453
2521
  },
2454
2522
  consent: { given: meta.consent === true },
2455
- formHasLegalConsent: shape?.hasLegalConsent
2523
+ shape
2456
2524
  });
2457
2525
  if (submitted.ok) {
2458
2526
  hubspotSynced = true;
@@ -2465,10 +2533,12 @@ function createFormsService(strapi, _opts = {}) {
2465
2533
  );
2466
2534
  }
2467
2535
  } else {
2536
+ conversionError = submitted.error;
2468
2537
  strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
2469
2538
  }
2470
2539
  } catch (err) {
2471
- strapi.log.warn(`[hubspot] Forms API submit failed — ${err.message}`);
2540
+ conversionError = err.message;
2541
+ strapi.log.warn(`[hubspot] Forms API submit failed — ${conversionError}`);
2472
2542
  }
2473
2543
  }
2474
2544
  if (!usedFormsApi) {
@@ -2550,7 +2620,11 @@ function createFormsService(strapi, _opts = {}) {
2550
2620
  formTitle: form.title || form.name,
2551
2621
  email,
2552
2622
  values: resolution.values,
2553
- meta,
2623
+ meta: {
2624
+ ...meta,
2625
+ hubspotConversion: usedFormsApi,
2626
+ ...conversionError ? { hubspotError: conversionError } : {}
2627
+ },
2554
2628
  locale: form.locale ?? null,
2555
2629
  hubspotSynced,
2556
2630
  contactId,
@@ -2737,7 +2811,7 @@ function createFormsAdminController(strapi) {
2737
2811
  locale: ctx.query.locale,
2738
2812
  status: "published"
2739
2813
  });
2740
- ctx.body = { form: entry, published: Boolean(published) };
2814
+ ctx.body = { form: entry, published: Boolean(published), publishedHubspotFormId: published?.hubspotFormId ?? null };
2741
2815
  },
2742
2816
  async create(ctx) {
2743
2817
  const body = ctx.request.body ?? {};
@@ -34,9 +34,54 @@ export declare function formFieldNames(raw: {
34
34
  fieldGroups?: {
35
35
  fields?: {
36
36
  name?: string;
37
+ required?: boolean;
37
38
  }[];
38
39
  }[];
39
40
  }): string[];
41
+ export declare function formRequiredNames(raw: {
42
+ fieldGroups?: {
43
+ fields?: {
44
+ name?: string;
45
+ required?: boolean;
46
+ }[];
47
+ }[];
48
+ }): string[];
49
+ export interface CommunicationConsent {
50
+ subscriptionTypeId: number;
51
+ text: string;
52
+ }
53
+ export interface FormShape {
54
+ names: Set<string>;
55
+ required: Set<string>;
56
+ hasLegalConsent: boolean;
57
+ captcha: boolean;
58
+ consentToProcessText?: string;
59
+ communications: CommunicationConsent[];
60
+ }
61
+ export declare function parseFormShape(raw: {
62
+ fieldGroups?: {
63
+ fields?: {
64
+ name?: string;
65
+ required?: boolean;
66
+ }[];
67
+ }[];
68
+ configuration?: {
69
+ captchaEnabled?: boolean;
70
+ };
71
+ legalConsentOptions?: {
72
+ type?: string;
73
+ consentToProcessText?: string;
74
+ communicationsCheckboxes?: {
75
+ label?: string;
76
+ text?: string;
77
+ subscriptionTypeId?: number | string;
78
+ communicationTypeId?: number | string;
79
+ }[];
80
+ } | null;
81
+ }): FormShape;
82
+ /** Email-only retry is only safe when HubSpot doesn't require other fields. */
83
+ export declare function canRetryEmailOnly(required: Iterable<string> | undefined): boolean;
84
+ export declare function buildLegalConsent(shape: Pick<FormShape, "hasLegalConsent" | "communications" | "consentToProcessText"> | null | undefined, given: boolean, fallbackText?: string): Record<string, unknown> | undefined;
40
85
  export interface FormSubmitContext {
41
86
  hutk?: string;
42
87
  pageUri?: string;
@@ -51,24 +96,25 @@ export interface FormSubmitInput {
51
96
  fields: {
52
97
  name: string;
53
98
  value: string;
99
+ objectTypeId?: string;
54
100
  }[];
55
101
  context?: FormSubmitContext;
56
102
  consent?: {
57
103
  given: boolean;
58
104
  text?: string;
59
105
  };
60
- formHasLegalConsent?: boolean;
106
+ shape?: FormShape | null;
61
107
  }
62
108
  export declare function clearFormShapeCache(formGuid?: string): void;
63
- export declare function loadFormShape(apiKey: string, formGuid: string): Promise<{
64
- names: Set<string>;
65
- hasLegalConsent: boolean;
66
- } | null>;
109
+ export declare function loadFormShape(apiKey: string, formGuid: string): Promise<FormShape | null>;
67
110
  export declare function findContactIdByEmail(apiKey: string, email: string): Promise<string | undefined>;
68
111
  /**
69
112
  * Submit a marketing form. Extra fields HubSpot doesn't know on that form
70
- * 400 the whole request, so callers should filter first; we still retry
71
- * without legal-consent and then email-only if HubSpot refuses the payload.
113
+ * 400 the whole request, so callers should filter first. A GDPR block on the
114
+ * HubSpot form needs the matching legalConsentOptions (including
115
+ * communications); stripping consent to "retry" would 400 forever. Email-only
116
+ * retry is only used when HubSpot doesn't require other fields. CAPTCHA
117
+ * cannot be solved server-side.
72
118
  */
73
119
  export declare function submitMarketingForm(input: FormSubmitInput): Promise<{
74
120
  ok: boolean;
@@ -73,9 +73,18 @@ export interface RawHubspotForm {
73
73
  type?: string;
74
74
  value?: string;
75
75
  };
76
+ captchaEnabled?: boolean;
76
77
  };
77
78
  legalConsentOptions?: {
78
79
  type?: string;
80
+ consentToProcessText?: string;
81
+ communicationsCheckboxes?: {
82
+ required?: boolean;
83
+ label?: string;
84
+ text?: string;
85
+ subscriptionTypeId?: number | string;
86
+ communicationTypeId?: number | string;
87
+ }[];
79
88
  } | null;
80
89
  }
81
90
  export declare function convertHubspotForm(raw: RawHubspotForm): ConvertedHubspotForm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-hubspot",
3
- "version": "0.16.3",
3
+ "version": "0.16.5",
4
4
  "description": "The form builder for HubSpot in Strapi — multi-step forms with conditional fields mapped to real CRM properties, a public submit pipeline with retries, and save-time mapping validation.",
5
5
  "keywords": [
6
6
  "strapi",