strapi-plugin-hubspot 0.13.1 → 0.15.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/CHANGELOG.md +33 -0
- package/README.md +58 -32
- package/dist/_chunks/{Forms-CHB-sKem.mjs → Forms-B7eIm0Fi.mjs} +38 -3
- package/dist/_chunks/{Forms-lrzuWzoB.js → Forms-ClyI4MJ1.js} +38 -3
- package/dist/_chunks/{HubspotFormInput-Bb6it0nP.js → HubspotFormInput-Do7F83V6.js} +1 -1
- package/dist/_chunks/{HubspotFormInput-YI09keTC.mjs → HubspotFormInput-cZAiaAaq.mjs} +1 -1
- package/dist/_chunks/{HubspotObjectInput-D3A6xzb3.js → HubspotObjectInput-DWqk2C26.js} +3 -3
- package/dist/_chunks/{HubspotObjectInput-CzLKT9oj.mjs → HubspotObjectInput-ZZk_7hWq.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-DicskLcN.mjs → HubspotPropertyInput-5bkDGmGo.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-Dss3eUlL.js → HubspotPropertyInput-v9B3wb1a.js} +3 -3
- package/dist/_chunks/{Settings-4CbX3SPK.mjs → Settings-QcFbm3s5.mjs} +132 -36
- package/dist/_chunks/{Settings-Ch8tPZAV.js → Settings-X3EHnTaO.js} +131 -35
- package/dist/_chunks/{en-D8DJiSxd.mjs → en-DMJeF7EN.mjs} +19 -6
- package/dist/_chunks/{en-PYFibnDc.js → en-ZHvaZLi7.js} +19 -6
- package/dist/_chunks/{fr-BMjixfXn.mjs → fr-CVAntLmc.mjs} +18 -5
- package/dist/_chunks/{fr-yHk4sSEl.js → fr-Ds_2EAzZ.js} +18 -5
- package/dist/_chunks/{index-BE2ribUL.mjs → index-BSJAhj8r.mjs} +6 -6
- package/dist/_chunks/{index-BAvedd2m.js → index-BpcilDy1.js} +6 -6
- package/dist/_chunks/{objectLabels-CycoU0zY.js → objectLabels-CYXXFpNB.js} +1 -1
- package/dist/_chunks/{objectLabels-DGJxYjKz.mjs → objectLabels-CrYRzsqe.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-DRfaI6mF.js → useHubspotSchema-Ba6xZUCL.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-e31UsYb2.mjs → useHubspotSchema-CBF4GkmK.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/builder/types.d.ts +2 -0
- package/dist/server/index.js +502 -197
- package/dist/server/index.mjs +502 -197
- package/dist/server/src/__tests__/hsforms.test.d.ts +1 -0
- package/dist/server/src/__tests__/settings.test.d.ts +1 -0
- package/dist/server/src/content-types.d.ts +8 -0
- package/dist/server/src/forms.d.ts +10 -6
- package/dist/server/src/hsforms.d.ts +75 -0
- package/dist/server/src/importHubspot.d.ts +1 -0
- package/dist/server/src/index.d.ts +17 -2
- package/dist/server/src/settings.d.ts +36 -5
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -158,6 +158,9 @@ function resolveObjects(configured) {
|
|
|
158
158
|
return out;
|
|
159
159
|
}
|
|
160
160
|
const ENV_VAR = "HUBSPOT_API_KEY";
|
|
161
|
+
const ENV_PORTAL = "HUBSPOT_PORTAL_ID";
|
|
162
|
+
const ENV_REGION = "HUBSPOT_REGION";
|
|
163
|
+
const ENV_DEFAULT_FORM = "HUBSPOT_DEFAULT_FORM_ID";
|
|
161
164
|
const store = (strapi) => strapi.store({ type: "plugin", name: "hubspot" });
|
|
162
165
|
async function getStoredSettings(strapi) {
|
|
163
166
|
return await store(strapi).get({ key: "settings" }) ?? {};
|
|
@@ -166,6 +169,13 @@ async function setStoredSettings(strapi, value) {
|
|
|
166
169
|
await store(strapi).set({ key: "settings", value });
|
|
167
170
|
clearCache();
|
|
168
171
|
}
|
|
172
|
+
const first = (values, fallback = "") => {
|
|
173
|
+
for (const item of values) {
|
|
174
|
+
const value = (item.value ?? "").trim();
|
|
175
|
+
if (value) return { value, source: item.source };
|
|
176
|
+
}
|
|
177
|
+
return { value: fallback, source: null };
|
|
178
|
+
};
|
|
169
179
|
async function resolveApiKey(strapi) {
|
|
170
180
|
const stored = await getStoredSettings(strapi);
|
|
171
181
|
if (stored.apiKey) return { apiKey: stored.apiKey, source: "settings" };
|
|
@@ -175,14 +185,71 @@ async function resolveApiKey(strapi) {
|
|
|
175
185
|
if (fromEnv) return { apiKey: fromEnv, source: "env" };
|
|
176
186
|
return { apiKey: "", source: null };
|
|
177
187
|
}
|
|
178
|
-
async function
|
|
179
|
-
const
|
|
188
|
+
async function resolveAccount(strapi) {
|
|
189
|
+
const resolved = await resolveAccountSources(strapi);
|
|
190
|
+
return {
|
|
191
|
+
portalId: resolved.portalId,
|
|
192
|
+
region: resolved.region || "eu1",
|
|
193
|
+
defaultFormId: resolved.defaultFormId
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function resolveAccountSources(strapi) {
|
|
197
|
+
const stored = await getStoredSettings(strapi);
|
|
198
|
+
const formsCfg = strapi.plugin("hubspot").config("forms", {}) ?? {};
|
|
199
|
+
const portal = first([
|
|
200
|
+
{ value: stored.portalId, source: "settings" },
|
|
201
|
+
{ value: strapi.plugin("hubspot").config("portalId", ""), source: "config" },
|
|
202
|
+
{ value: process.env[ENV_PORTAL], source: "env" }
|
|
203
|
+
]);
|
|
204
|
+
const region = first(
|
|
205
|
+
[
|
|
206
|
+
{ value: stored.region, source: "settings" },
|
|
207
|
+
{ value: strapi.plugin("hubspot").config("region", ""), source: "config" },
|
|
208
|
+
{ value: process.env[ENV_REGION], source: "env" }
|
|
209
|
+
],
|
|
210
|
+
"eu1"
|
|
211
|
+
);
|
|
212
|
+
const form = first([
|
|
213
|
+
{ value: stored.defaultFormId, source: "settings" },
|
|
214
|
+
{ value: formsCfg.defaultFormId, source: "config" },
|
|
215
|
+
{ value: process.env[ENV_DEFAULT_FORM], source: "env" }
|
|
216
|
+
]);
|
|
217
|
+
const { apiKey, source: keySource } = await resolveApiKey(strapi);
|
|
180
218
|
return {
|
|
181
219
|
configured: Boolean(apiKey),
|
|
182
|
-
keySource
|
|
183
|
-
hint: apiKey ? `…${apiKey.slice(-4)}` : ""
|
|
220
|
+
keySource,
|
|
221
|
+
hint: apiKey ? `…${apiKey.slice(-4)}` : "",
|
|
222
|
+
portalId: portal.value,
|
|
223
|
+
region: region.value || "eu1",
|
|
224
|
+
defaultFormId: form.value,
|
|
225
|
+
portalSource: portal.source,
|
|
226
|
+
regionSource: region.source,
|
|
227
|
+
formSource: form.source
|
|
184
228
|
};
|
|
185
229
|
}
|
|
230
|
+
async function publicSettings(strapi) {
|
|
231
|
+
return resolveAccountSources(strapi);
|
|
232
|
+
}
|
|
233
|
+
async function patchStoredSettings(strapi, body) {
|
|
234
|
+
const current = await getStoredSettings(strapi);
|
|
235
|
+
const next = { ...current };
|
|
236
|
+
if (typeof body.apiKey === "string" && body.apiKey.trim()) {
|
|
237
|
+
next.apiKey = body.apiKey.trim();
|
|
238
|
+
}
|
|
239
|
+
if ("portalId" in body) next.portalId = String(body.portalId ?? "").trim();
|
|
240
|
+
if ("region" in body) {
|
|
241
|
+
const region = String(body.region ?? "").trim().toLowerCase();
|
|
242
|
+
next.region = region || "eu1";
|
|
243
|
+
}
|
|
244
|
+
if ("defaultFormId" in body) next.defaultFormId = String(body.defaultFormId ?? "").trim();
|
|
245
|
+
await setStoredSettings(strapi, next);
|
|
246
|
+
}
|
|
247
|
+
async function clearStoredApiKey(strapi) {
|
|
248
|
+
const current = await getStoredSettings(strapi);
|
|
249
|
+
const { apiKey: _apiKey, ...rest } = current;
|
|
250
|
+
const empty = !rest.portalId && !rest.region && !rest.defaultFormId;
|
|
251
|
+
await setStoredSettings(strapi, empty ? null : rest);
|
|
252
|
+
}
|
|
186
253
|
function collectMappings(node, target, found = []) {
|
|
187
254
|
if (Array.isArray(node)) {
|
|
188
255
|
for (const item of node) collectMappings(item, target, found);
|
|
@@ -1246,6 +1313,13 @@ const contentTypes = {
|
|
|
1246
1313
|
submitLabel: localizedString(),
|
|
1247
1314
|
successMessage: localizedString("text"),
|
|
1248
1315
|
class: { type: "string" },
|
|
1316
|
+
// Marketing form GUID of the connected HubSpot portal. Test portal
|
|
1317
|
+
// today, production later — swap the private app + this GUID, no
|
|
1318
|
+
// code change. Empty = CRM upsert fallback (no native conversion).
|
|
1319
|
+
hubspotFormId: {
|
|
1320
|
+
type: "string",
|
|
1321
|
+
pluginOptions: { i18n: { localized: false } }
|
|
1322
|
+
},
|
|
1249
1323
|
definition: {
|
|
1250
1324
|
type: "json",
|
|
1251
1325
|
required: true,
|
|
@@ -1449,6 +1523,312 @@ function resolveSubmission(definition, rawValues) {
|
|
|
1449
1523
|
const unknown = Object.keys(rawValues).filter((key) => !declared.has(key));
|
|
1450
1524
|
return { values, hidden, missingRequired, unknown };
|
|
1451
1525
|
}
|
|
1526
|
+
const HS_BASE$2 = "https://api.hubapi.com";
|
|
1527
|
+
const OBJECT_BY_TYPE_ID = {
|
|
1528
|
+
"0-1": "contact",
|
|
1529
|
+
"0-2": "company"
|
|
1530
|
+
};
|
|
1531
|
+
const TYPE_MAP = {
|
|
1532
|
+
single_line_text: "text",
|
|
1533
|
+
multi_line_text: "textarea",
|
|
1534
|
+
email: "email",
|
|
1535
|
+
phone: "tel",
|
|
1536
|
+
mobile_phone: "tel",
|
|
1537
|
+
number: "number",
|
|
1538
|
+
dropdown: "select",
|
|
1539
|
+
select: "select",
|
|
1540
|
+
radio: "radio",
|
|
1541
|
+
multiple_checkboxes: "checkbox",
|
|
1542
|
+
single_checkbox: "checkbox",
|
|
1543
|
+
booleancheckbox: "checkbox"
|
|
1544
|
+
};
|
|
1545
|
+
let counter$1 = 0;
|
|
1546
|
+
const makeId$1 = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter$1 += 1).toString(36)}`;
|
|
1547
|
+
function convertCondition(parentFieldId, raw) {
|
|
1548
|
+
const operator = (raw.operator ?? "").toUpperCase();
|
|
1549
|
+
const values = (raw.values ?? []).map((v) => String(v)).filter((v) => v !== "");
|
|
1550
|
+
const spread = (op, logic) => values.length ? { logic, rules: values.map((value) => ({ field: parentFieldId, operator: op, value })) } : null;
|
|
1551
|
+
switch (operator) {
|
|
1552
|
+
case "EQ":
|
|
1553
|
+
case "SET_ANY":
|
|
1554
|
+
return spread("eq", "or");
|
|
1555
|
+
case "NEQ":
|
|
1556
|
+
case "NOT_SET_ANY":
|
|
1557
|
+
return spread("neq", "and");
|
|
1558
|
+
case "CONTAINS":
|
|
1559
|
+
return spread("contains", "or");
|
|
1560
|
+
case "GT":
|
|
1561
|
+
return spread("gt", "or");
|
|
1562
|
+
case "LT":
|
|
1563
|
+
return spread("lt", "or");
|
|
1564
|
+
case "SET":
|
|
1565
|
+
case "NOT_EMPTY":
|
|
1566
|
+
return { logic: "and", rules: [{ field: parentFieldId, operator: "notEmpty" }] };
|
|
1567
|
+
case "NOT_SET":
|
|
1568
|
+
case "EMPTY":
|
|
1569
|
+
return { logic: "and", rules: [{ field: parentFieldId, operator: "empty" }] };
|
|
1570
|
+
default:
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function convertField(raw, ctx) {
|
|
1575
|
+
const label = raw.label || raw.name || "";
|
|
1576
|
+
const name = raw.name ?? "";
|
|
1577
|
+
if (raw.hidden) {
|
|
1578
|
+
ctx.skipped.push({ code: "hidden-field", label });
|
|
1579
|
+
return [];
|
|
1580
|
+
}
|
|
1581
|
+
const object = OBJECT_BY_TYPE_ID[raw.objectTypeId ?? "0-1"];
|
|
1582
|
+
if (!object) {
|
|
1583
|
+
ctx.skipped.push({ code: "object", label, detail: raw.objectTypeId });
|
|
1584
|
+
return [];
|
|
1585
|
+
}
|
|
1586
|
+
const type = TYPE_MAP[raw.fieldType ?? ""];
|
|
1587
|
+
if (!type || !name) {
|
|
1588
|
+
ctx.skipped.push({ code: "field-type", label, detail: raw.fieldType });
|
|
1589
|
+
return [];
|
|
1590
|
+
}
|
|
1591
|
+
if (ctx.usedNames.has(name)) {
|
|
1592
|
+
ctx.skipped.push({ code: "duplicate", label, detail: name });
|
|
1593
|
+
return [];
|
|
1594
|
+
}
|
|
1595
|
+
ctx.usedNames.add(name);
|
|
1596
|
+
const options = (raw.options ?? []).map((o) => ({ value: String(o.value ?? ""), label: o.label || void 0 })).filter((o) => o.value !== "");
|
|
1597
|
+
const field = {
|
|
1598
|
+
id: makeId$1("fld"),
|
|
1599
|
+
name,
|
|
1600
|
+
label,
|
|
1601
|
+
type,
|
|
1602
|
+
required: Boolean(raw.required),
|
|
1603
|
+
placeholder: raw.placeholder || void 0,
|
|
1604
|
+
helpText: raw.description || void 0,
|
|
1605
|
+
...options.length ? { options } : {},
|
|
1606
|
+
hubspot: { object, property: name },
|
|
1607
|
+
...ctx.parentCondition ? { visibleIf: ctx.parentCondition } : {}
|
|
1608
|
+
};
|
|
1609
|
+
const out = [field];
|
|
1610
|
+
for (const dependent of raw.dependentFields ?? []) {
|
|
1611
|
+
if (!dependent.field) continue;
|
|
1612
|
+
const childLabel = dependent.field.label || dependent.field.name || "";
|
|
1613
|
+
const condition = dependent.dependentCondition ? convertCondition(field.id, dependent.dependentCondition) : null;
|
|
1614
|
+
if (!condition) {
|
|
1615
|
+
ctx.skipped.push({
|
|
1616
|
+
code: "condition",
|
|
1617
|
+
label: childLabel,
|
|
1618
|
+
detail: dependent.dependentCondition?.operator
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
out.push(...convertField(dependent.field, { ...ctx, parentCondition: condition }));
|
|
1622
|
+
}
|
|
1623
|
+
return out;
|
|
1624
|
+
}
|
|
1625
|
+
function convertHubspotForm(raw) {
|
|
1626
|
+
const skipped = [];
|
|
1627
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
1628
|
+
const fields = [];
|
|
1629
|
+
for (const group of raw.fieldGroups ?? []) {
|
|
1630
|
+
if (group.richText && !(group.fields ?? []).length) {
|
|
1631
|
+
skipped.push({ code: "rich-text", detail: group.richText.slice(0, 80) });
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
const groupFields = (group.fields ?? []).flatMap(
|
|
1635
|
+
(f) => convertField(f, { usedNames, skipped })
|
|
1636
|
+
);
|
|
1637
|
+
const topLevel = (group.fields ?? []).length;
|
|
1638
|
+
if (topLevel === 2 && groupFields.length >= 2) {
|
|
1639
|
+
groupFields[0].width = "half";
|
|
1640
|
+
groupFields[1].width = "half";
|
|
1641
|
+
}
|
|
1642
|
+
fields.push(...groupFields);
|
|
1643
|
+
}
|
|
1644
|
+
if (raw.legalConsentOptions && Object.keys(raw.legalConsentOptions).length) {
|
|
1645
|
+
skipped.push({ code: "legal-consent" });
|
|
1646
|
+
}
|
|
1647
|
+
const postSubmit = raw.configuration?.postSubmitAction;
|
|
1648
|
+
return {
|
|
1649
|
+
name: raw.name ?? "",
|
|
1650
|
+
submitLabel: raw.displayOptions?.submitButtonText || null,
|
|
1651
|
+
successMessage: postSubmit?.type === "thank_you" && postSubmit.value ? postSubmit.value : null,
|
|
1652
|
+
hubspotFormId: raw.id ?? null,
|
|
1653
|
+
// HubSpot forms are single-page: one step, reorganizable in the builder.
|
|
1654
|
+
definition: { version: 1, steps: [{ id: makeId$1("stp"), fields }] },
|
|
1655
|
+
skipped
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
async function hsGet(apiKey, path) {
|
|
1659
|
+
const res = await fetch(`${HS_BASE$2}${path}`, {
|
|
1660
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1661
|
+
});
|
|
1662
|
+
if (!res.ok) {
|
|
1663
|
+
const body = await res.json().catch(() => ({}));
|
|
1664
|
+
throw Object.assign(new Error(body.message || `HubSpot ${res.status}`), {
|
|
1665
|
+
status: res.status
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
return await res.json();
|
|
1669
|
+
}
|
|
1670
|
+
async function listHubspotForms(apiKey) {
|
|
1671
|
+
const out = [];
|
|
1672
|
+
let after;
|
|
1673
|
+
for (let page = 0; page < 10; page += 1) {
|
|
1674
|
+
const query2 = after ? `&after=${encodeURIComponent(after)}` : "";
|
|
1675
|
+
const res = await hsGet(apiKey, `/marketing/v3/forms/?limit=100&formTypes=hubspot${query2}`);
|
|
1676
|
+
for (const form of res.results ?? []) {
|
|
1677
|
+
if (form.id && !form.archived) {
|
|
1678
|
+
out.push({ id: form.id, name: form.name || form.id, updatedAt: form.updatedAt });
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
after = res.paging?.next?.after;
|
|
1682
|
+
if (!after) break;
|
|
1683
|
+
}
|
|
1684
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1685
|
+
}
|
|
1686
|
+
async function fetchHubspotForm(apiKey, formId) {
|
|
1687
|
+
return hsGet(apiKey, `/marketing/v3/forms/${encodeURIComponent(formId)}`);
|
|
1688
|
+
}
|
|
1689
|
+
const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1690
|
+
const PORTAL = /^\d{1,12}$/;
|
|
1691
|
+
const HUTK = /^[a-f0-9]{8,128}$/i;
|
|
1692
|
+
const SHAPE_TTL_MS = 5 * 60 * 1e3;
|
|
1693
|
+
const DEFAULT_CONSENT_TEXT = "J'accepte que mes données soient utilisées pour me recontacter.";
|
|
1694
|
+
function isFormGuid(value) {
|
|
1695
|
+
return GUID.test(value.trim());
|
|
1696
|
+
}
|
|
1697
|
+
function sanitizeHutk(value) {
|
|
1698
|
+
if (typeof value !== "string") return void 0;
|
|
1699
|
+
const trimmed = value.trim();
|
|
1700
|
+
return HUTK.test(trimmed) ? trimmed : void 0;
|
|
1701
|
+
}
|
|
1702
|
+
function sanitizePortalId(value) {
|
|
1703
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
1704
|
+
const trimmed = String(value).trim();
|
|
1705
|
+
return PORTAL.test(trimmed) ? trimmed : void 0;
|
|
1706
|
+
}
|
|
1707
|
+
function sanitizeIp(value) {
|
|
1708
|
+
if (typeof value !== "string") return void 0;
|
|
1709
|
+
const trimmed = value.trim();
|
|
1710
|
+
return trimmed && trimmed.length <= 45 ? trimmed : void 0;
|
|
1711
|
+
}
|
|
1712
|
+
function formsHost(region) {
|
|
1713
|
+
const r = (region || "eu1").trim().toLowerCase();
|
|
1714
|
+
if (!r || r === "na1" || r === "na") return "api.hsforms.com";
|
|
1715
|
+
return `api-${r}.hsforms.com`;
|
|
1716
|
+
}
|
|
1717
|
+
function formsSubmitUrl(portalId, formGuid, region = "eu1") {
|
|
1718
|
+
return `https://${formsHost(region)}/submissions/v3/integration/secure/submit/${encodeURIComponent(portalId)}/${encodeURIComponent(formGuid)}`;
|
|
1719
|
+
}
|
|
1720
|
+
function toHsFields(props) {
|
|
1721
|
+
return Object.entries(props).filter(([, value]) => value !== "").map(([name, value]) => ({ name, value }));
|
|
1722
|
+
}
|
|
1723
|
+
function filterToFormFields(fields, allowed) {
|
|
1724
|
+
if (!allowed) return fields;
|
|
1725
|
+
return fields.filter((field) => allowed.has(field.name));
|
|
1726
|
+
}
|
|
1727
|
+
function formFieldNames(raw) {
|
|
1728
|
+
const names = [];
|
|
1729
|
+
for (const group of raw.fieldGroups ?? []) {
|
|
1730
|
+
for (const field of group.fields ?? []) {
|
|
1731
|
+
if (field.name) names.push(field.name);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return names;
|
|
1735
|
+
}
|
|
1736
|
+
const shapeCache = /* @__PURE__ */ new Map();
|
|
1737
|
+
async function loadFormShape(apiKey, formGuid) {
|
|
1738
|
+
const cached = shapeCache.get(formGuid);
|
|
1739
|
+
if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
|
|
1740
|
+
return { names: cached.names, hasLegalConsent: cached.hasLegalConsent };
|
|
1741
|
+
}
|
|
1742
|
+
try {
|
|
1743
|
+
const raw = await fetchHubspotForm(apiKey, formGuid);
|
|
1744
|
+
const names = new Set(formFieldNames(raw));
|
|
1745
|
+
const hasLegalConsent = Boolean(
|
|
1746
|
+
raw.legalConsentOptions && typeof raw.legalConsentOptions === "object" && raw.legalConsentOptions.type && raw.legalConsentOptions.type !== "none"
|
|
1747
|
+
);
|
|
1748
|
+
shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
|
|
1749
|
+
return { names, hasLegalConsent };
|
|
1750
|
+
} catch {
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
async function findContactIdByEmail(apiKey, email) {
|
|
1755
|
+
const res = await fetch("https://api.hubapi.com/crm/v3/objects/contacts/search", {
|
|
1756
|
+
method: "POST",
|
|
1757
|
+
headers: {
|
|
1758
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1759
|
+
"Content-Type": "application/json"
|
|
1760
|
+
},
|
|
1761
|
+
body: JSON.stringify({
|
|
1762
|
+
filterGroups: [
|
|
1763
|
+
{ filters: [{ propertyName: "email", operator: "EQ", value: email }] }
|
|
1764
|
+
],
|
|
1765
|
+
properties: ["email"],
|
|
1766
|
+
limit: 1
|
|
1767
|
+
})
|
|
1768
|
+
});
|
|
1769
|
+
const body = await res.json().catch(() => ({}));
|
|
1770
|
+
if (!res.ok) return void 0;
|
|
1771
|
+
return body.results?.[0]?.id;
|
|
1772
|
+
}
|
|
1773
|
+
function contextPayload(ctx) {
|
|
1774
|
+
const out = {};
|
|
1775
|
+
const hutk = sanitizeHutk(ctx?.hutk);
|
|
1776
|
+
if (hutk) out.hutk = hutk;
|
|
1777
|
+
if (typeof ctx?.pageUri === "string" && ctx.pageUri.trim()) {
|
|
1778
|
+
out.pageUri = ctx.pageUri.trim().slice(0, 1e3);
|
|
1779
|
+
}
|
|
1780
|
+
if (typeof ctx?.pageName === "string" && ctx.pageName.trim()) {
|
|
1781
|
+
out.pageName = ctx.pageName.trim().slice(0, 200);
|
|
1782
|
+
}
|
|
1783
|
+
const ip = sanitizeIp(ctx?.ipAddress);
|
|
1784
|
+
if (ip) out.ipAddress = ip;
|
|
1785
|
+
return out;
|
|
1786
|
+
}
|
|
1787
|
+
async function postForm(url, apiKey, payload) {
|
|
1788
|
+
const res = await fetch(url, {
|
|
1789
|
+
method: "POST",
|
|
1790
|
+
headers: {
|
|
1791
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1792
|
+
"Content-Type": "application/json"
|
|
1793
|
+
},
|
|
1794
|
+
body: JSON.stringify(payload)
|
|
1795
|
+
});
|
|
1796
|
+
if (res.ok) return { ok: true, status: res.status };
|
|
1797
|
+
const body = await res.json().catch(() => ({}));
|
|
1798
|
+
const error = body.errors?.[0]?.message || body.message || `HubSpot ${res.status}`;
|
|
1799
|
+
return { ok: false, status: res.status, error };
|
|
1800
|
+
}
|
|
1801
|
+
async function submitMarketingForm(input) {
|
|
1802
|
+
const portalId = sanitizePortalId(input.portalId);
|
|
1803
|
+
const formGuid = input.formGuid.trim();
|
|
1804
|
+
if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
|
|
1805
|
+
return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
|
|
1806
|
+
}
|
|
1807
|
+
const url = formsSubmitUrl(portalId, formGuid, input.region);
|
|
1808
|
+
const context = contextPayload(input.context);
|
|
1809
|
+
const base = {
|
|
1810
|
+
fields: input.fields,
|
|
1811
|
+
...Object.keys(context).length ? { context } : {}
|
|
1812
|
+
};
|
|
1813
|
+
const withConsent = input.consent?.given && input.formHasLegalConsent !== false ? {
|
|
1814
|
+
...base,
|
|
1815
|
+
legalConsentOptions: {
|
|
1816
|
+
consent: {
|
|
1817
|
+
consentToProcess: true,
|
|
1818
|
+
text: input.consent.text?.trim() || DEFAULT_CONSENT_TEXT
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
} : base;
|
|
1822
|
+
let result = await postForm(url, input.apiKey, withConsent);
|
|
1823
|
+
if (!result.ok && result.status === 400 && withConsent !== base) {
|
|
1824
|
+
result = await postForm(url, input.apiKey, base);
|
|
1825
|
+
}
|
|
1826
|
+
if (!result.ok && result.status === 400 && input.fields.length > 1) {
|
|
1827
|
+
const email = input.fields.find((f) => f.name === "email");
|
|
1828
|
+
if (email) result = await postForm(url, input.apiKey, { ...base, fields: [email] });
|
|
1829
|
+
}
|
|
1830
|
+
return result;
|
|
1831
|
+
}
|
|
1452
1832
|
function publicForm(entry) {
|
|
1453
1833
|
return {
|
|
1454
1834
|
slug: entry.slug,
|
|
@@ -1557,9 +1937,9 @@ function sanitizeRawValues(raw) {
|
|
|
1557
1937
|
}
|
|
1558
1938
|
return values;
|
|
1559
1939
|
}
|
|
1560
|
-
const HS_BASE$
|
|
1561
|
-
const HS_COMPANIES = `${HS_BASE$
|
|
1562
|
-
const HS_NOTES = `${HS_BASE$
|
|
1940
|
+
const HS_BASE$1 = "https://api.hubapi.com";
|
|
1941
|
+
const HS_COMPANIES = `${HS_BASE$1}/crm/v3/objects/companies`;
|
|
1942
|
+
const HS_NOTES = `${HS_BASE$1}/crm/v3/objects/notes`;
|
|
1563
1943
|
const ASSOC_NOTE_TO_CONTACT = 202;
|
|
1564
1944
|
const ASSOC_NOTE_TO_COMPANY = 190;
|
|
1565
1945
|
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1617,8 +1997,17 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1617
1997
|
const formsConfig = () => ({
|
|
1618
1998
|
companyFromDomain: true,
|
|
1619
1999
|
timelineNote: true,
|
|
2000
|
+
defaultFormId: "",
|
|
1620
2001
|
...strapi.plugin("hubspot").config("forms", {})
|
|
1621
2002
|
});
|
|
2003
|
+
const stringProps = (props) => {
|
|
2004
|
+
const out = {};
|
|
2005
|
+
for (const [key, value] of Object.entries(props)) {
|
|
2006
|
+
if (value === "" || value == null) continue;
|
|
2007
|
+
out[key] = typeof value === "boolean" ? value ? "true" : "false" : String(value);
|
|
2008
|
+
}
|
|
2009
|
+
return out;
|
|
2010
|
+
};
|
|
1622
2011
|
async function partitionBySchema(apiKey, groups) {
|
|
1623
2012
|
let schema;
|
|
1624
2013
|
try {
|
|
@@ -1658,20 +2047,20 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1658
2047
|
});
|
|
1659
2048
|
const existingId = found.results?.[0]?.id;
|
|
1660
2049
|
if (!existingId) return void 0;
|
|
1661
|
-
const
|
|
1662
|
-
for (const [key, value] of Object.entries(props))
|
|
2050
|
+
const stringProps2 = {};
|
|
2051
|
+
for (const [key, value] of Object.entries(props)) stringProps2[key] = String(value);
|
|
1663
2052
|
await hsJson(apiKey, `${HS_COMPANIES}/${existingId}`, {
|
|
1664
2053
|
method: "PATCH",
|
|
1665
|
-
body: { properties:
|
|
2054
|
+
body: { properties: stringProps2 }
|
|
1666
2055
|
});
|
|
1667
2056
|
return existingId;
|
|
1668
2057
|
}
|
|
1669
2058
|
async function createCompany(apiKey, props) {
|
|
1670
|
-
const
|
|
1671
|
-
for (const [key, value] of Object.entries(props))
|
|
2059
|
+
const stringProps2 = {};
|
|
2060
|
+
for (const [key, value] of Object.entries(props)) stringProps2[key] = String(value);
|
|
1672
2061
|
const created = await hsJson(apiKey, HS_COMPANIES, {
|
|
1673
2062
|
method: "POST",
|
|
1674
|
-
body: { properties:
|
|
2063
|
+
body: { properties: stringProps2 }
|
|
1675
2064
|
});
|
|
1676
2065
|
return created.id;
|
|
1677
2066
|
}
|
|
@@ -1684,19 +2073,19 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1684
2073
|
limit: 1
|
|
1685
2074
|
}
|
|
1686
2075
|
});
|
|
1687
|
-
const
|
|
1688
|
-
for (const [key, value] of Object.entries(props))
|
|
2076
|
+
const stringProps2 = { domain };
|
|
2077
|
+
for (const [key, value] of Object.entries(props)) stringProps2[key] = String(value);
|
|
1689
2078
|
const existingId = found.results?.[0]?.id;
|
|
1690
2079
|
if (existingId) {
|
|
1691
2080
|
await hsJson(apiKey, `${HS_COMPANIES}/${existingId}`, {
|
|
1692
2081
|
method: "PATCH",
|
|
1693
|
-
body: { properties:
|
|
2082
|
+
body: { properties: stringProps2 }
|
|
1694
2083
|
});
|
|
1695
2084
|
return existingId;
|
|
1696
2085
|
}
|
|
1697
2086
|
const created = await hsJson(apiKey, HS_COMPANIES, {
|
|
1698
2087
|
method: "POST",
|
|
1699
|
-
body: { properties:
|
|
2088
|
+
body: { properties: stringProps2 }
|
|
1700
2089
|
});
|
|
1701
2090
|
return created.id;
|
|
1702
2091
|
}
|
|
@@ -1806,6 +2195,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1806
2195
|
});
|
|
1807
2196
|
}
|
|
1808
2197
|
}
|
|
2198
|
+
let usedFormsApi = false;
|
|
1809
2199
|
if (email && apiKey) {
|
|
1810
2200
|
const groups = groupByObject(form.definition, resolution.values);
|
|
1811
2201
|
for (const [object, props] of Object.entries(companyGroups)) {
|
|
@@ -1813,14 +2203,63 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1813
2203
|
}
|
|
1814
2204
|
const partition = await partitionBySchema(apiKey, groups);
|
|
1815
2205
|
rejected = partition.rejected;
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
if (
|
|
1822
|
-
|
|
1823
|
-
|
|
2206
|
+
const account = await resolveAccount(strapi);
|
|
2207
|
+
const portalId = sanitizePortalId(account.portalId);
|
|
2208
|
+
const region = account.region || "eu1";
|
|
2209
|
+
const ownGuid = typeof form.hubspotFormId === "string" ? form.hubspotFormId.trim() : "";
|
|
2210
|
+
const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
|
|
2211
|
+
if (portalId && formGuid) {
|
|
2212
|
+
try {
|
|
2213
|
+
const shape = await loadFormShape(apiKey, formGuid);
|
|
2214
|
+
const fields = filterToFormFields(
|
|
2215
|
+
toHsFields({ ...stringProps(partition.accepted.contact ?? {}), email }),
|
|
2216
|
+
shape?.names ?? null
|
|
2217
|
+
);
|
|
2218
|
+
if (fields.length && !fields.some((f) => f.name === "email")) {
|
|
2219
|
+
fields.unshift({ name: "email", value: email });
|
|
2220
|
+
}
|
|
2221
|
+
const submitted = await submitMarketingForm({
|
|
2222
|
+
apiKey,
|
|
2223
|
+
portalId,
|
|
2224
|
+
formGuid,
|
|
2225
|
+
region,
|
|
2226
|
+
fields: fields.length ? fields : [{ name: "email", value: email }],
|
|
2227
|
+
context: {
|
|
2228
|
+
hutk: sanitizeHutk(meta.hutk),
|
|
2229
|
+
pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
|
|
2230
|
+
pageName: typeof meta.pageName === "string" && meta.pageName || (form.title || form.name) || void 0,
|
|
2231
|
+
ipAddress: sanitizeIp(meta.ipAddress)
|
|
2232
|
+
},
|
|
2233
|
+
consent: { given: meta.consent === true },
|
|
2234
|
+
formHasLegalConsent: shape?.hasLegalConsent
|
|
2235
|
+
});
|
|
2236
|
+
if (submitted.ok) {
|
|
2237
|
+
hubspotSynced = true;
|
|
2238
|
+
usedFormsApi = true;
|
|
2239
|
+
try {
|
|
2240
|
+
contactId = await findContactIdByEmail(apiKey, email);
|
|
2241
|
+
} catch (err) {
|
|
2242
|
+
strapi.log.warn(
|
|
2243
|
+
`[hubspot] contact lookup after form submit failed — ${err.message}`
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
} else {
|
|
2247
|
+
strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
|
|
2248
|
+
}
|
|
2249
|
+
} catch (err) {
|
|
2250
|
+
strapi.log.warn(`[hubspot] Forms API submit failed — ${err.message}`);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if (!usedFormsApi) {
|
|
2254
|
+
const result = await strapi.plugin("hubspot").service("submit").upsert({
|
|
2255
|
+
object: "contact",
|
|
2256
|
+
idProperty: "email",
|
|
2257
|
+
properties: { ...partition.accepted.contact ?? {}, email }
|
|
2258
|
+
});
|
|
2259
|
+
if (result.ok) {
|
|
2260
|
+
hubspotSynced = true;
|
|
2261
|
+
contactId = result.id;
|
|
2262
|
+
}
|
|
1824
2263
|
}
|
|
1825
2264
|
const domain = corporateDomain(email);
|
|
1826
2265
|
const companyProps = partition.accepted.company ?? {};
|
|
@@ -1844,7 +2283,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1844
2283
|
if (companyId) {
|
|
1845
2284
|
await hsJson(
|
|
1846
2285
|
apiKey,
|
|
1847
|
-
`${HS_BASE$
|
|
2286
|
+
`${HS_BASE$1}/crm/v4/objects/contacts/${contactId}/associations/default/companies/${companyId}`,
|
|
1848
2287
|
{ method: "PUT" }
|
|
1849
2288
|
);
|
|
1850
2289
|
}
|
|
@@ -1852,7 +2291,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1852
2291
|
strapi.log.warn(`[hubspot] company sync failed (contact OK) — ${err.message}`);
|
|
1853
2292
|
}
|
|
1854
2293
|
}
|
|
1855
|
-
if (contactId && config2.timelineNote) {
|
|
2294
|
+
if (contactId && config2.timelineNote && !usedFormsApi) {
|
|
1856
2295
|
try {
|
|
1857
2296
|
await createLeadNote(apiKey, {
|
|
1858
2297
|
contactId,
|
|
@@ -1887,168 +2326,6 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1887
2326
|
}
|
|
1888
2327
|
return { submit };
|
|
1889
2328
|
}
|
|
1890
|
-
const HS_BASE$1 = "https://api.hubapi.com";
|
|
1891
|
-
const OBJECT_BY_TYPE_ID = {
|
|
1892
|
-
"0-1": "contact",
|
|
1893
|
-
"0-2": "company"
|
|
1894
|
-
};
|
|
1895
|
-
const TYPE_MAP = {
|
|
1896
|
-
single_line_text: "text",
|
|
1897
|
-
multi_line_text: "textarea",
|
|
1898
|
-
email: "email",
|
|
1899
|
-
phone: "tel",
|
|
1900
|
-
mobile_phone: "tel",
|
|
1901
|
-
number: "number",
|
|
1902
|
-
dropdown: "select",
|
|
1903
|
-
select: "select",
|
|
1904
|
-
radio: "radio",
|
|
1905
|
-
multiple_checkboxes: "checkbox",
|
|
1906
|
-
single_checkbox: "checkbox",
|
|
1907
|
-
booleancheckbox: "checkbox"
|
|
1908
|
-
};
|
|
1909
|
-
let counter$1 = 0;
|
|
1910
|
-
const makeId$1 = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter$1 += 1).toString(36)}`;
|
|
1911
|
-
function convertCondition(parentFieldId, raw) {
|
|
1912
|
-
const operator = (raw.operator ?? "").toUpperCase();
|
|
1913
|
-
const values = (raw.values ?? []).map((v) => String(v)).filter((v) => v !== "");
|
|
1914
|
-
const spread = (op, logic) => values.length ? { logic, rules: values.map((value) => ({ field: parentFieldId, operator: op, value })) } : null;
|
|
1915
|
-
switch (operator) {
|
|
1916
|
-
case "EQ":
|
|
1917
|
-
case "SET_ANY":
|
|
1918
|
-
return spread("eq", "or");
|
|
1919
|
-
case "NEQ":
|
|
1920
|
-
case "NOT_SET_ANY":
|
|
1921
|
-
return spread("neq", "and");
|
|
1922
|
-
case "CONTAINS":
|
|
1923
|
-
return spread("contains", "or");
|
|
1924
|
-
case "GT":
|
|
1925
|
-
return spread("gt", "or");
|
|
1926
|
-
case "LT":
|
|
1927
|
-
return spread("lt", "or");
|
|
1928
|
-
case "SET":
|
|
1929
|
-
case "NOT_EMPTY":
|
|
1930
|
-
return { logic: "and", rules: [{ field: parentFieldId, operator: "notEmpty" }] };
|
|
1931
|
-
case "NOT_SET":
|
|
1932
|
-
case "EMPTY":
|
|
1933
|
-
return { logic: "and", rules: [{ field: parentFieldId, operator: "empty" }] };
|
|
1934
|
-
default:
|
|
1935
|
-
return null;
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
function convertField(raw, ctx) {
|
|
1939
|
-
const label = raw.label || raw.name || "";
|
|
1940
|
-
const name = raw.name ?? "";
|
|
1941
|
-
if (raw.hidden) {
|
|
1942
|
-
ctx.skipped.push({ code: "hidden-field", label });
|
|
1943
|
-
return [];
|
|
1944
|
-
}
|
|
1945
|
-
const object = OBJECT_BY_TYPE_ID[raw.objectTypeId ?? "0-1"];
|
|
1946
|
-
if (!object) {
|
|
1947
|
-
ctx.skipped.push({ code: "object", label, detail: raw.objectTypeId });
|
|
1948
|
-
return [];
|
|
1949
|
-
}
|
|
1950
|
-
const type = TYPE_MAP[raw.fieldType ?? ""];
|
|
1951
|
-
if (!type || !name) {
|
|
1952
|
-
ctx.skipped.push({ code: "field-type", label, detail: raw.fieldType });
|
|
1953
|
-
return [];
|
|
1954
|
-
}
|
|
1955
|
-
if (ctx.usedNames.has(name)) {
|
|
1956
|
-
ctx.skipped.push({ code: "duplicate", label, detail: name });
|
|
1957
|
-
return [];
|
|
1958
|
-
}
|
|
1959
|
-
ctx.usedNames.add(name);
|
|
1960
|
-
const options = (raw.options ?? []).map((o) => ({ value: String(o.value ?? ""), label: o.label || void 0 })).filter((o) => o.value !== "");
|
|
1961
|
-
const field = {
|
|
1962
|
-
id: makeId$1("fld"),
|
|
1963
|
-
name,
|
|
1964
|
-
label,
|
|
1965
|
-
type,
|
|
1966
|
-
required: Boolean(raw.required),
|
|
1967
|
-
placeholder: raw.placeholder || void 0,
|
|
1968
|
-
helpText: raw.description || void 0,
|
|
1969
|
-
...options.length ? { options } : {},
|
|
1970
|
-
hubspot: { object, property: name },
|
|
1971
|
-
...ctx.parentCondition ? { visibleIf: ctx.parentCondition } : {}
|
|
1972
|
-
};
|
|
1973
|
-
const out = [field];
|
|
1974
|
-
for (const dependent of raw.dependentFields ?? []) {
|
|
1975
|
-
if (!dependent.field) continue;
|
|
1976
|
-
const childLabel = dependent.field.label || dependent.field.name || "";
|
|
1977
|
-
const condition = dependent.dependentCondition ? convertCondition(field.id, dependent.dependentCondition) : null;
|
|
1978
|
-
if (!condition) {
|
|
1979
|
-
ctx.skipped.push({
|
|
1980
|
-
code: "condition",
|
|
1981
|
-
label: childLabel,
|
|
1982
|
-
detail: dependent.dependentCondition?.operator
|
|
1983
|
-
});
|
|
1984
|
-
}
|
|
1985
|
-
out.push(...convertField(dependent.field, { ...ctx, parentCondition: condition }));
|
|
1986
|
-
}
|
|
1987
|
-
return out;
|
|
1988
|
-
}
|
|
1989
|
-
function convertHubspotForm(raw) {
|
|
1990
|
-
const skipped = [];
|
|
1991
|
-
const usedNames = /* @__PURE__ */ new Set();
|
|
1992
|
-
const fields = [];
|
|
1993
|
-
for (const group of raw.fieldGroups ?? []) {
|
|
1994
|
-
if (group.richText && !(group.fields ?? []).length) {
|
|
1995
|
-
skipped.push({ code: "rich-text", detail: group.richText.slice(0, 80) });
|
|
1996
|
-
continue;
|
|
1997
|
-
}
|
|
1998
|
-
const groupFields = (group.fields ?? []).flatMap(
|
|
1999
|
-
(f) => convertField(f, { usedNames, skipped })
|
|
2000
|
-
);
|
|
2001
|
-
const topLevel = (group.fields ?? []).length;
|
|
2002
|
-
if (topLevel === 2 && groupFields.length >= 2) {
|
|
2003
|
-
groupFields[0].width = "half";
|
|
2004
|
-
groupFields[1].width = "half";
|
|
2005
|
-
}
|
|
2006
|
-
fields.push(...groupFields);
|
|
2007
|
-
}
|
|
2008
|
-
if (raw.legalConsentOptions && Object.keys(raw.legalConsentOptions).length) {
|
|
2009
|
-
skipped.push({ code: "legal-consent" });
|
|
2010
|
-
}
|
|
2011
|
-
const postSubmit = raw.configuration?.postSubmitAction;
|
|
2012
|
-
return {
|
|
2013
|
-
name: raw.name ?? "",
|
|
2014
|
-
submitLabel: raw.displayOptions?.submitButtonText || null,
|
|
2015
|
-
successMessage: postSubmit?.type === "thank_you" && postSubmit.value ? postSubmit.value : null,
|
|
2016
|
-
// HubSpot forms are single-page: one step, reorganizable in the builder.
|
|
2017
|
-
definition: { version: 1, steps: [{ id: makeId$1("stp"), fields }] },
|
|
2018
|
-
skipped
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
|
-
async function hsGet(apiKey, path) {
|
|
2022
|
-
const res = await fetch(`${HS_BASE$1}${path}`, {
|
|
2023
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
2024
|
-
});
|
|
2025
|
-
if (!res.ok) {
|
|
2026
|
-
const body = await res.json().catch(() => ({}));
|
|
2027
|
-
throw Object.assign(new Error(body.message || `HubSpot ${res.status}`), {
|
|
2028
|
-
status: res.status
|
|
2029
|
-
});
|
|
2030
|
-
}
|
|
2031
|
-
return await res.json();
|
|
2032
|
-
}
|
|
2033
|
-
async function listHubspotForms(apiKey) {
|
|
2034
|
-
const out = [];
|
|
2035
|
-
let after;
|
|
2036
|
-
for (let page = 0; page < 10; page += 1) {
|
|
2037
|
-
const query2 = after ? `&after=${encodeURIComponent(after)}` : "";
|
|
2038
|
-
const res = await hsGet(apiKey, `/marketing/v3/forms/?limit=100&formTypes=hubspot${query2}`);
|
|
2039
|
-
for (const form of res.results ?? []) {
|
|
2040
|
-
if (form.id && !form.archived) {
|
|
2041
|
-
out.push({ id: form.id, name: form.name || form.id, updatedAt: form.updatedAt });
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
after = res.paging?.next?.after;
|
|
2045
|
-
if (!after) break;
|
|
2046
|
-
}
|
|
2047
|
-
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
2048
|
-
}
|
|
2049
|
-
async function fetchHubspotForm(apiKey, formId) {
|
|
2050
|
-
return hsGet(apiKey, `/marketing/v3/forms/${encodeURIComponent(formId)}`);
|
|
2051
|
-
}
|
|
2052
2329
|
let counter = 0;
|
|
2053
2330
|
const makeId = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter += 1).toString(36)}`;
|
|
2054
2331
|
const slugifyName = (label) => label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
@@ -2127,7 +2404,8 @@ const WRITABLE = [
|
|
|
2127
2404
|
"nextLabel",
|
|
2128
2405
|
"submitLabel",
|
|
2129
2406
|
"successMessage",
|
|
2130
|
-
"class"
|
|
2407
|
+
"class",
|
|
2408
|
+
"hubspotFormId"
|
|
2131
2409
|
];
|
|
2132
2410
|
const slugify = (value) => value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "form";
|
|
2133
2411
|
function createFormsAdminController(strapi) {
|
|
@@ -2492,6 +2770,7 @@ function createFormsAdminController(strapi) {
|
|
|
2492
2770
|
submitLabel: source.submitLabel,
|
|
2493
2771
|
successMessage: source.successMessage,
|
|
2494
2772
|
class: source.class,
|
|
2773
|
+
hubspotFormId: source.hubspotFormId ?? null,
|
|
2495
2774
|
definition: source.definition
|
|
2496
2775
|
};
|
|
2497
2776
|
if (targetId) {
|
|
@@ -2726,17 +3005,27 @@ const FORMS_ACTION = "plugin::hubspot.forms";
|
|
|
2726
3005
|
const config = {
|
|
2727
3006
|
default: {
|
|
2728
3007
|
apiKey: "",
|
|
3008
|
+
// HubSpot account that receives form submissions. A test portal id is
|
|
3009
|
+
// fine in development; production swaps this env var, not the code.
|
|
3010
|
+
portalId: "",
|
|
3011
|
+
// Hosting region of that portal (`eu1`, `na1`, …). Drives the Forms API
|
|
3012
|
+
// host (`api-eu1.hsforms.com`) — CRM stays on api.hubapi.com.
|
|
3013
|
+
region: "eu1",
|
|
2729
3014
|
// Objects whose properties are offered. Names from the standard set, or
|
|
2730
3015
|
// `{ name, path }` for a custom object type.
|
|
2731
3016
|
objects: ["contact", "company"],
|
|
2732
3017
|
validate: [],
|
|
2733
|
-
// Submission pipeline of the built forms
|
|
3018
|
+
// Submission pipeline of the built forms:
|
|
2734
3019
|
// - companyFromDomain: a corporate email upserts the Company (deduped by
|
|
2735
3020
|
// domain) and associates it to the contact;
|
|
2736
|
-
// - timelineNote:
|
|
3021
|
+
// - timelineNote: recap note on the CRM-upsert fallback only (off when
|
|
3022
|
+
// submissions go through the marketing Forms API);
|
|
3023
|
+
// - defaultFormId: portal-wide marketing form GUID when a builder form
|
|
3024
|
+
// doesn't set its own — swap with the portal at production cutover.
|
|
2737
3025
|
forms: {
|
|
2738
3026
|
companyFromDomain: true,
|
|
2739
3027
|
timelineNote: true,
|
|
3028
|
+
defaultFormId: "",
|
|
2740
3029
|
// Per-IP brakes on the public routes; 0 disables one.
|
|
2741
3030
|
rateLimit: { submitPerMinute: 6, searchPerMinute: 30 }
|
|
2742
3031
|
}
|
|
@@ -2892,7 +3181,7 @@ const controllers = {
|
|
|
2892
3181
|
if (!values || !Object.keys(values).length) ctx.throw(422, "Invalid submission");
|
|
2893
3182
|
const rawMeta = ctx.request.body?.meta ?? {};
|
|
2894
3183
|
const meta = {};
|
|
2895
|
-
for (const key of ["pagePath", "pageUrl", "originPath", "originLabel", "source"]) {
|
|
3184
|
+
for (const key of ["pagePath", "pageUrl", "pageName", "originPath", "originLabel", "source", "ipAddress"]) {
|
|
2896
3185
|
const v = rawMeta[key];
|
|
2897
3186
|
if (typeof v === "string" && v) meta[key] = v.slice(0, 500);
|
|
2898
3187
|
}
|
|
@@ -2900,6 +3189,13 @@ const controllers = {
|
|
|
2900
3189
|
if (typeof rawMeta.consentedAt === "string" && rawMeta.consentedAt.trim()) {
|
|
2901
3190
|
meta.consentedAt = rawMeta.consentedAt.trim().slice(0, 40);
|
|
2902
3191
|
}
|
|
3192
|
+
if (typeof rawMeta.hutk === "string" && rawMeta.hutk.trim()) {
|
|
3193
|
+
meta.hutk = rawMeta.hutk.trim().slice(0, 128);
|
|
3194
|
+
}
|
|
3195
|
+
if (!meta.ipAddress) {
|
|
3196
|
+
const ip = ctx.request.ip;
|
|
3197
|
+
if (ip) meta.ipAddress = ip.slice(0, 45);
|
|
3198
|
+
}
|
|
2903
3199
|
const outcome = await strapi.plugin("hubspot").service("forms").submit(entry, values, meta);
|
|
2904
3200
|
if (!outcome.ok) {
|
|
2905
3201
|
ctx.status = 422;
|
|
@@ -2912,15 +3208,24 @@ const controllers = {
|
|
|
2912
3208
|
},
|
|
2913
3209
|
settings: ({ strapi }) => ({
|
|
2914
3210
|
async get(ctx) {
|
|
2915
|
-
|
|
3211
|
+
const settings = await publicSettings(strapi);
|
|
3212
|
+
let forms = [];
|
|
3213
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
3214
|
+
if (apiKey) {
|
|
3215
|
+
try {
|
|
3216
|
+
forms = await listHubspotForms(apiKey);
|
|
3217
|
+
} catch {
|
|
3218
|
+
forms = [];
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
ctx.body = { ...settings, forms };
|
|
2916
3222
|
},
|
|
2917
3223
|
async update(ctx) {
|
|
2918
|
-
|
|
2919
|
-
await setStoredSettings(strapi, apiKey ? { apiKey } : null);
|
|
3224
|
+
await patchStoredSettings(strapi, ctx.request.body ?? {});
|
|
2920
3225
|
ctx.body = await publicSettings(strapi);
|
|
2921
3226
|
},
|
|
2922
3227
|
async reset(ctx) {
|
|
2923
|
-
await
|
|
3228
|
+
await clearStoredApiKey(strapi);
|
|
2924
3229
|
ctx.body = await publicSettings(strapi);
|
|
2925
3230
|
}
|
|
2926
3231
|
})
|