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