strapi-plugin-hubspot 0.16.3 → 0.16.7
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/dist/_chunks/{Forms-BBaw53Tz.mjs → Forms-DyV6KyiO.mjs} +20 -6
- package/dist/_chunks/{Forms-CIJrUB_n.js → Forms-uUN4XjhW.js} +20 -6
- package/dist/_chunks/{HubspotFormInput-DSTuQ_bx.js → HubspotFormInput-BkWat_wu.js} +1 -1
- package/dist/_chunks/{HubspotFormInput-DIo6hCNz.mjs → HubspotFormInput-DEXPX-6j.mjs} +1 -1
- package/dist/_chunks/{HubspotObjectInput-BIqC8tl2.js → HubspotObjectInput-CFDYW-TM.js} +3 -3
- package/dist/_chunks/{HubspotObjectInput-BA16dswz.mjs → HubspotObjectInput-DL8fYK2x.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-C77Op1Wm.js → HubspotPropertyInput-CFMkbQeT.js} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-Dsn7ikop.mjs → HubspotPropertyInput-WU5FMJj3.mjs} +3 -3
- package/dist/_chunks/{Settings-PVhJVPqq.js → Settings-B_hhdpHa.js} +2 -2
- package/dist/_chunks/{Settings-DxX9Uh_2.mjs → Settings-CiXEps_C.mjs} +2 -2
- package/dist/_chunks/{en-Djd3TID0.mjs → en-BIUBYzHv.mjs} +5 -1
- package/dist/_chunks/{en-sfzopB5K.js → en-CFhEtgZJ.js} +5 -1
- package/dist/_chunks/{fr-C7t1elmR.mjs → fr-CMYcnFH0.mjs} +5 -1
- package/dist/_chunks/{fr-sF5Josfh.js → fr-zdUuuV0M.js} +5 -1
- package/dist/_chunks/{index-B8GYi6sw.mjs → index-BeBn7bJa.mjs} +6 -6
- package/dist/_chunks/{index-XEuSPuUj.js → index-CG_WQzSa.js} +6 -6
- package/dist/_chunks/{objectLabels-wBwfLdZt.js → objectLabels-BGq-Ypgn.js} +1 -1
- package/dist/_chunks/{objectLabels-DybqhBdT.mjs → objectLabels-CS_0QCNz.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-WvgW3od8.js → useHubspotSchema-BZP2qkAu.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-DtcHHUOb.mjs → useHubspotSchema-CP7LRdzO.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 +1 -1
- package/dist/server/index.js +152 -34
- package/dist/server/index.mjs +152 -34
- package/dist/server/src/hsforms.d.ts +84 -12
- package/dist/server/src/importHubspot.d.ts +9 -0
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1923,10 +1923,6 @@ function formsSubmitUrl(portalId, formGuid, region = "eu1") {
|
|
|
1923
1923
|
function toHsFields(props) {
|
|
1924
1924
|
return Object.entries(props).filter(([, value]) => value !== "").map(([name, value]) => ({ name, value }));
|
|
1925
1925
|
}
|
|
1926
|
-
function filterToFormFields(fields, allowed) {
|
|
1927
|
-
if (!allowed) return fields;
|
|
1928
|
-
return fields.filter((field) => allowed.has(field.name));
|
|
1929
|
-
}
|
|
1930
1926
|
function formFieldNames(raw) {
|
|
1931
1927
|
const names = [];
|
|
1932
1928
|
for (const group of raw.fieldGroups ?? []) {
|
|
@@ -1936,6 +1932,103 @@ function formFieldNames(raw) {
|
|
|
1936
1932
|
}
|
|
1937
1933
|
return names;
|
|
1938
1934
|
}
|
|
1935
|
+
const OBJECT_BAG = {
|
|
1936
|
+
"0-1": "contact",
|
|
1937
|
+
"0-2": "company"
|
|
1938
|
+
};
|
|
1939
|
+
function formShapeFields(raw) {
|
|
1940
|
+
const fields = [];
|
|
1941
|
+
for (const group of raw.fieldGroups ?? []) {
|
|
1942
|
+
for (const field of group.fields ?? []) {
|
|
1943
|
+
if (!field.name) continue;
|
|
1944
|
+
fields.push({
|
|
1945
|
+
name: field.name,
|
|
1946
|
+
objectTypeId: field.objectTypeId?.trim() || "0-1",
|
|
1947
|
+
required: Boolean(field.required)
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
return fields;
|
|
1952
|
+
}
|
|
1953
|
+
function fieldsForHubspotForm(bags, shape, extra = {}) {
|
|
1954
|
+
const contact = { ...bags.contact ?? {}, ...extra };
|
|
1955
|
+
if (!shape?.fields.length) {
|
|
1956
|
+
return toHsFields(contact).map((field) => ({ ...field, objectTypeId: "0-1" }));
|
|
1957
|
+
}
|
|
1958
|
+
const out = [];
|
|
1959
|
+
for (const field of shape.fields) {
|
|
1960
|
+
const bagName = OBJECT_BAG[field.objectTypeId] || "contact";
|
|
1961
|
+
const value = bags[bagName]?.[field.name] ?? contact[field.name];
|
|
1962
|
+
if (!value) continue;
|
|
1963
|
+
out.push({ name: field.name, value, objectTypeId: field.objectTypeId || "0-1" });
|
|
1964
|
+
}
|
|
1965
|
+
return out;
|
|
1966
|
+
}
|
|
1967
|
+
function parseFormShape(raw) {
|
|
1968
|
+
const legal = raw.legalConsentOptions;
|
|
1969
|
+
const legalType = typeof legal?.type === "string" ? legal.type : void 0;
|
|
1970
|
+
const hasLegalConsent = Boolean(legalType && legalType !== "none");
|
|
1971
|
+
const communications = [];
|
|
1972
|
+
for (const box of legal?.communicationsCheckboxes ?? []) {
|
|
1973
|
+
const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
|
|
1974
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
1975
|
+
communications.push({
|
|
1976
|
+
subscriptionTypeId: id,
|
|
1977
|
+
text: String(box.label || box.text || "").trim() || "I agree to receive communications."
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
const subscriptionTypeIds = (legal?.subscriptionTypeIds ?? []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0);
|
|
1981
|
+
const consentToProcessText = legal?.consentToProcessText?.trim();
|
|
1982
|
+
const privacyText = legal?.privacyText?.trim();
|
|
1983
|
+
const lawfulRaw = String(legal?.lawfulBasis || "").toUpperCase();
|
|
1984
|
+
const fields = formShapeFields(raw);
|
|
1985
|
+
return {
|
|
1986
|
+
names: new Set(fields.map((field) => field.name)),
|
|
1987
|
+
required: new Set(fields.filter((field) => field.required).map((field) => field.name)),
|
|
1988
|
+
fields,
|
|
1989
|
+
hasLegalConsent,
|
|
1990
|
+
...legalType ? { legalType } : {},
|
|
1991
|
+
lawfulBasis: lawfulRaw.includes("CUSTOMER") ? "CUSTOMER" : "LEAD",
|
|
1992
|
+
subscriptionTypeIds,
|
|
1993
|
+
...privacyText ? { privacyText } : {},
|
|
1994
|
+
captcha: raw.configuration?.recaptchaEnabled === true || raw.configuration?.captchaEnabled === true,
|
|
1995
|
+
...consentToProcessText ? { consentToProcessText } : {},
|
|
1996
|
+
communications
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
function canRetryEmailOnly(required) {
|
|
2000
|
+
const names = [...required ?? []];
|
|
2001
|
+
return names.length === 0 || names.every((name) => name === "email");
|
|
2002
|
+
}
|
|
2003
|
+
function buildLegalConsent(shape, given, fallbackText) {
|
|
2004
|
+
if (!given) return void 0;
|
|
2005
|
+
if (shape && !shape.hasLegalConsent) return void 0;
|
|
2006
|
+
const fallback = fallbackText?.trim() || DEFAULT_CONSENT_TEXT;
|
|
2007
|
+
if (shape?.legalType === "legitimate_interest") {
|
|
2008
|
+
const subscriptionTypeId = shape.subscriptionTypeIds[0];
|
|
2009
|
+
if (!subscriptionTypeId) return void 0;
|
|
2010
|
+
return {
|
|
2011
|
+
legitimateInterest: {
|
|
2012
|
+
value: true,
|
|
2013
|
+
subscriptionTypeId,
|
|
2014
|
+
legalBasis: shape.lawfulBasis || "LEAD",
|
|
2015
|
+
text: shape.privacyText?.trim() || fallback
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
const consent = {
|
|
2020
|
+
consentToProcess: true,
|
|
2021
|
+
text: shape?.consentToProcessText?.trim() || fallback
|
|
2022
|
+
};
|
|
2023
|
+
if (shape?.communications.length) {
|
|
2024
|
+
consent.communications = shape.communications.map((item) => ({
|
|
2025
|
+
value: true,
|
|
2026
|
+
subscriptionTypeId: item.subscriptionTypeId,
|
|
2027
|
+
text: item.text
|
|
2028
|
+
}));
|
|
2029
|
+
}
|
|
2030
|
+
return { consent };
|
|
2031
|
+
}
|
|
1939
2032
|
const shapeCache = /* @__PURE__ */ new Map();
|
|
1940
2033
|
function clearFormShapeCache(formGuid) {
|
|
1941
2034
|
if (formGuid) shapeCache.delete(formGuid);
|
|
@@ -1944,16 +2037,13 @@ function clearFormShapeCache(formGuid) {
|
|
|
1944
2037
|
async function loadFormShape(apiKey, formGuid) {
|
|
1945
2038
|
const cached = shapeCache.get(formGuid);
|
|
1946
2039
|
if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
|
|
1947
|
-
return
|
|
2040
|
+
return cached.shape;
|
|
1948
2041
|
}
|
|
1949
2042
|
try {
|
|
1950
2043
|
const raw = await fetchHubspotForm(apiKey, formGuid);
|
|
1951
|
-
const
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
);
|
|
1955
|
-
shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
|
|
1956
|
-
return { names, hasLegalConsent };
|
|
2044
|
+
const shape = parseFormShape(raw);
|
|
2045
|
+
shapeCache.set(formGuid, { at: Date.now(), shape });
|
|
2046
|
+
return shape;
|
|
1957
2047
|
} catch {
|
|
1958
2048
|
return null;
|
|
1959
2049
|
}
|
|
@@ -2011,28 +2101,45 @@ async function submitMarketingForm(input) {
|
|
|
2011
2101
|
if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
|
|
2012
2102
|
return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
|
|
2013
2103
|
}
|
|
2104
|
+
if (input.shape?.captcha) {
|
|
2105
|
+
return {
|
|
2106
|
+
ok: false,
|
|
2107
|
+
status: 0,
|
|
2108
|
+
error: "HubSpot form has CAPTCHA enabled — the Forms API cannot submit it. Turn CAPTCHA off on that marketing form."
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2014
2111
|
const url = formsSubmitUrl(portalId, formGuid, input.region);
|
|
2015
2112
|
const context = contextPayload(input.context);
|
|
2113
|
+
const fields = input.fields.map((field) => ({
|
|
2114
|
+
objectTypeId: field.objectTypeId || "0-1",
|
|
2115
|
+
name: field.name,
|
|
2116
|
+
value: field.value
|
|
2117
|
+
}));
|
|
2016
2118
|
const base = {
|
|
2017
|
-
fields
|
|
2119
|
+
fields,
|
|
2018
2120
|
...Object.keys(context).length ? { context } : {}
|
|
2019
2121
|
};
|
|
2020
|
-
const
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
} : base;
|
|
2122
|
+
const legal = buildLegalConsent(
|
|
2123
|
+
input.shape,
|
|
2124
|
+
input.consent?.given === true,
|
|
2125
|
+
input.consent?.text
|
|
2126
|
+
);
|
|
2127
|
+
const withConsent = legal ? { ...base, legalConsentOptions: legal } : base;
|
|
2029
2128
|
let result = await postForm(url, input.apiKey, withConsent);
|
|
2030
|
-
|
|
2129
|
+
const mustKeepConsent = Boolean(input.shape?.hasLegalConsent && legal);
|
|
2130
|
+
if (!result.ok && result.status === 400 && withConsent !== base && !mustKeepConsent) {
|
|
2031
2131
|
result = await postForm(url, input.apiKey, base);
|
|
2032
2132
|
}
|
|
2033
|
-
if (!result.ok && result.status === 400 && input.fields.length > 1) {
|
|
2034
|
-
const email =
|
|
2035
|
-
if (email)
|
|
2133
|
+
if (!result.ok && result.status === 400 && input.fields.length > 1 && canRetryEmailOnly(input.shape?.required)) {
|
|
2134
|
+
const email = fields.find((f) => f.name === "email");
|
|
2135
|
+
if (email) {
|
|
2136
|
+
const slim = { ...base, fields: [email] };
|
|
2137
|
+
result = await postForm(
|
|
2138
|
+
url,
|
|
2139
|
+
input.apiKey,
|
|
2140
|
+
legal && mustKeepConsent ? { ...slim, legalConsentOptions: legal } : slim
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2036
2143
|
}
|
|
2037
2144
|
return result;
|
|
2038
2145
|
}
|
|
@@ -2414,6 +2521,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2414
2521
|
}
|
|
2415
2522
|
let usedFormsApi = false;
|
|
2416
2523
|
let sentFieldNames = [];
|
|
2524
|
+
let conversionError;
|
|
2417
2525
|
if (email && apiKey) {
|
|
2418
2526
|
const groups = groupByObject(form.definition, resolution.values);
|
|
2419
2527
|
for (const [object, props] of Object.entries(companyGroups)) {
|
|
@@ -2432,12 +2540,16 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2432
2540
|
if (tryFormsApi && portalId && formGuid) {
|
|
2433
2541
|
try {
|
|
2434
2542
|
const shape = await loadFormShape(apiKey, formGuid);
|
|
2435
|
-
const fields =
|
|
2436
|
-
|
|
2437
|
-
|
|
2543
|
+
const fields = fieldsForHubspotForm(
|
|
2544
|
+
{
|
|
2545
|
+
contact: contactProps,
|
|
2546
|
+
company: stringProps(partition.accepted.company ?? {})
|
|
2547
|
+
},
|
|
2548
|
+
shape,
|
|
2549
|
+
{ email }
|
|
2438
2550
|
);
|
|
2439
2551
|
if (fields.length && !fields.some((f) => f.name === "email")) {
|
|
2440
|
-
fields.unshift({ name: "email", value: email });
|
|
2552
|
+
fields.unshift({ name: "email", value: email, objectTypeId: "0-1" });
|
|
2441
2553
|
}
|
|
2442
2554
|
sentFieldNames = fields.map((f) => f.name);
|
|
2443
2555
|
const submitted = await submitMarketingForm({
|
|
@@ -2445,7 +2557,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2445
2557
|
portalId,
|
|
2446
2558
|
formGuid,
|
|
2447
2559
|
region,
|
|
2448
|
-
fields: fields.length ? fields : [{ name: "email", value: email }],
|
|
2560
|
+
fields: fields.length ? fields : [{ name: "email", value: email, objectTypeId: "0-1" }],
|
|
2449
2561
|
context: {
|
|
2450
2562
|
hutk: sanitizeHutk(meta.hutk),
|
|
2451
2563
|
pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
|
|
@@ -2453,7 +2565,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2453
2565
|
ipAddress: sanitizeIp(meta.ipAddress)
|
|
2454
2566
|
},
|
|
2455
2567
|
consent: { given: meta.consent === true },
|
|
2456
|
-
|
|
2568
|
+
shape
|
|
2457
2569
|
});
|
|
2458
2570
|
if (submitted.ok) {
|
|
2459
2571
|
hubspotSynced = true;
|
|
@@ -2466,10 +2578,12 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2466
2578
|
);
|
|
2467
2579
|
}
|
|
2468
2580
|
} else {
|
|
2581
|
+
conversionError = submitted.error;
|
|
2469
2582
|
strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
|
|
2470
2583
|
}
|
|
2471
2584
|
} catch (err) {
|
|
2472
|
-
|
|
2585
|
+
conversionError = err.message;
|
|
2586
|
+
strapi.log.warn(`[hubspot] Forms API submit failed — ${conversionError}`);
|
|
2473
2587
|
}
|
|
2474
2588
|
}
|
|
2475
2589
|
if (!usedFormsApi) {
|
|
@@ -2551,7 +2665,11 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2551
2665
|
formTitle: form.title || form.name,
|
|
2552
2666
|
email,
|
|
2553
2667
|
values: resolution.values,
|
|
2554
|
-
meta
|
|
2668
|
+
meta: {
|
|
2669
|
+
...meta,
|
|
2670
|
+
hubspotConversion: usedFormsApi,
|
|
2671
|
+
...conversionError ? { hubspotError: conversionError } : {}
|
|
2672
|
+
},
|
|
2555
2673
|
locale: form.locale ?? null,
|
|
2556
2674
|
hubspotSynced,
|
|
2557
2675
|
contactId,
|
|
@@ -2738,7 +2856,7 @@ function createFormsAdminController(strapi) {
|
|
|
2738
2856
|
locale: ctx.query.locale,
|
|
2739
2857
|
status: "published"
|
|
2740
2858
|
});
|
|
2741
|
-
ctx.body = { form: entry, published: Boolean(published) };
|
|
2859
|
+
ctx.body = { form: entry, published: Boolean(published), publishedHubspotFormId: published?.hubspotFormId ?? null };
|
|
2742
2860
|
},
|
|
2743
2861
|
async create(ctx) {
|
|
2744
2862
|
const body = ctx.request.body ?? {};
|
package/dist/server/index.mjs
CHANGED
|
@@ -1922,10 +1922,6 @@ function formsSubmitUrl(portalId, formGuid, region = "eu1") {
|
|
|
1922
1922
|
function toHsFields(props) {
|
|
1923
1923
|
return Object.entries(props).filter(([, value]) => value !== "").map(([name, value]) => ({ name, value }));
|
|
1924
1924
|
}
|
|
1925
|
-
function filterToFormFields(fields, allowed) {
|
|
1926
|
-
if (!allowed) return fields;
|
|
1927
|
-
return fields.filter((field) => allowed.has(field.name));
|
|
1928
|
-
}
|
|
1929
1925
|
function formFieldNames(raw) {
|
|
1930
1926
|
const names = [];
|
|
1931
1927
|
for (const group of raw.fieldGroups ?? []) {
|
|
@@ -1935,6 +1931,103 @@ function formFieldNames(raw) {
|
|
|
1935
1931
|
}
|
|
1936
1932
|
return names;
|
|
1937
1933
|
}
|
|
1934
|
+
const OBJECT_BAG = {
|
|
1935
|
+
"0-1": "contact",
|
|
1936
|
+
"0-2": "company"
|
|
1937
|
+
};
|
|
1938
|
+
function formShapeFields(raw) {
|
|
1939
|
+
const fields = [];
|
|
1940
|
+
for (const group of raw.fieldGroups ?? []) {
|
|
1941
|
+
for (const field of group.fields ?? []) {
|
|
1942
|
+
if (!field.name) continue;
|
|
1943
|
+
fields.push({
|
|
1944
|
+
name: field.name,
|
|
1945
|
+
objectTypeId: field.objectTypeId?.trim() || "0-1",
|
|
1946
|
+
required: Boolean(field.required)
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return fields;
|
|
1951
|
+
}
|
|
1952
|
+
function fieldsForHubspotForm(bags, shape, extra = {}) {
|
|
1953
|
+
const contact = { ...bags.contact ?? {}, ...extra };
|
|
1954
|
+
if (!shape?.fields.length) {
|
|
1955
|
+
return toHsFields(contact).map((field) => ({ ...field, objectTypeId: "0-1" }));
|
|
1956
|
+
}
|
|
1957
|
+
const out = [];
|
|
1958
|
+
for (const field of shape.fields) {
|
|
1959
|
+
const bagName = OBJECT_BAG[field.objectTypeId] || "contact";
|
|
1960
|
+
const value = bags[bagName]?.[field.name] ?? contact[field.name];
|
|
1961
|
+
if (!value) continue;
|
|
1962
|
+
out.push({ name: field.name, value, objectTypeId: field.objectTypeId || "0-1" });
|
|
1963
|
+
}
|
|
1964
|
+
return out;
|
|
1965
|
+
}
|
|
1966
|
+
function parseFormShape(raw) {
|
|
1967
|
+
const legal = raw.legalConsentOptions;
|
|
1968
|
+
const legalType = typeof legal?.type === "string" ? legal.type : void 0;
|
|
1969
|
+
const hasLegalConsent = Boolean(legalType && legalType !== "none");
|
|
1970
|
+
const communications = [];
|
|
1971
|
+
for (const box of legal?.communicationsCheckboxes ?? []) {
|
|
1972
|
+
const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
|
|
1973
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
1974
|
+
communications.push({
|
|
1975
|
+
subscriptionTypeId: id,
|
|
1976
|
+
text: String(box.label || box.text || "").trim() || "I agree to receive communications."
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
const subscriptionTypeIds = (legal?.subscriptionTypeIds ?? []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0);
|
|
1980
|
+
const consentToProcessText = legal?.consentToProcessText?.trim();
|
|
1981
|
+
const privacyText = legal?.privacyText?.trim();
|
|
1982
|
+
const lawfulRaw = String(legal?.lawfulBasis || "").toUpperCase();
|
|
1983
|
+
const fields = formShapeFields(raw);
|
|
1984
|
+
return {
|
|
1985
|
+
names: new Set(fields.map((field) => field.name)),
|
|
1986
|
+
required: new Set(fields.filter((field) => field.required).map((field) => field.name)),
|
|
1987
|
+
fields,
|
|
1988
|
+
hasLegalConsent,
|
|
1989
|
+
...legalType ? { legalType } : {},
|
|
1990
|
+
lawfulBasis: lawfulRaw.includes("CUSTOMER") ? "CUSTOMER" : "LEAD",
|
|
1991
|
+
subscriptionTypeIds,
|
|
1992
|
+
...privacyText ? { privacyText } : {},
|
|
1993
|
+
captcha: raw.configuration?.recaptchaEnabled === true || raw.configuration?.captchaEnabled === true,
|
|
1994
|
+
...consentToProcessText ? { consentToProcessText } : {},
|
|
1995
|
+
communications
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function canRetryEmailOnly(required) {
|
|
1999
|
+
const names = [...required ?? []];
|
|
2000
|
+
return names.length === 0 || names.every((name) => name === "email");
|
|
2001
|
+
}
|
|
2002
|
+
function buildLegalConsent(shape, given, fallbackText) {
|
|
2003
|
+
if (!given) return void 0;
|
|
2004
|
+
if (shape && !shape.hasLegalConsent) return void 0;
|
|
2005
|
+
const fallback = fallbackText?.trim() || DEFAULT_CONSENT_TEXT;
|
|
2006
|
+
if (shape?.legalType === "legitimate_interest") {
|
|
2007
|
+
const subscriptionTypeId = shape.subscriptionTypeIds[0];
|
|
2008
|
+
if (!subscriptionTypeId) return void 0;
|
|
2009
|
+
return {
|
|
2010
|
+
legitimateInterest: {
|
|
2011
|
+
value: true,
|
|
2012
|
+
subscriptionTypeId,
|
|
2013
|
+
legalBasis: shape.lawfulBasis || "LEAD",
|
|
2014
|
+
text: shape.privacyText?.trim() || fallback
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
const consent = {
|
|
2019
|
+
consentToProcess: true,
|
|
2020
|
+
text: shape?.consentToProcessText?.trim() || fallback
|
|
2021
|
+
};
|
|
2022
|
+
if (shape?.communications.length) {
|
|
2023
|
+
consent.communications = shape.communications.map((item) => ({
|
|
2024
|
+
value: true,
|
|
2025
|
+
subscriptionTypeId: item.subscriptionTypeId,
|
|
2026
|
+
text: item.text
|
|
2027
|
+
}));
|
|
2028
|
+
}
|
|
2029
|
+
return { consent };
|
|
2030
|
+
}
|
|
1938
2031
|
const shapeCache = /* @__PURE__ */ new Map();
|
|
1939
2032
|
function clearFormShapeCache(formGuid) {
|
|
1940
2033
|
if (formGuid) shapeCache.delete(formGuid);
|
|
@@ -1943,16 +2036,13 @@ function clearFormShapeCache(formGuid) {
|
|
|
1943
2036
|
async function loadFormShape(apiKey, formGuid) {
|
|
1944
2037
|
const cached = shapeCache.get(formGuid);
|
|
1945
2038
|
if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
|
|
1946
|
-
return
|
|
2039
|
+
return cached.shape;
|
|
1947
2040
|
}
|
|
1948
2041
|
try {
|
|
1949
2042
|
const raw = await fetchHubspotForm(apiKey, formGuid);
|
|
1950
|
-
const
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
);
|
|
1954
|
-
shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
|
|
1955
|
-
return { names, hasLegalConsent };
|
|
2043
|
+
const shape = parseFormShape(raw);
|
|
2044
|
+
shapeCache.set(formGuid, { at: Date.now(), shape });
|
|
2045
|
+
return shape;
|
|
1956
2046
|
} catch {
|
|
1957
2047
|
return null;
|
|
1958
2048
|
}
|
|
@@ -2010,28 +2100,45 @@ async function submitMarketingForm(input) {
|
|
|
2010
2100
|
if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
|
|
2011
2101
|
return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
|
|
2012
2102
|
}
|
|
2103
|
+
if (input.shape?.captcha) {
|
|
2104
|
+
return {
|
|
2105
|
+
ok: false,
|
|
2106
|
+
status: 0,
|
|
2107
|
+
error: "HubSpot form has CAPTCHA enabled — the Forms API cannot submit it. Turn CAPTCHA off on that marketing form."
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2013
2110
|
const url = formsSubmitUrl(portalId, formGuid, input.region);
|
|
2014
2111
|
const context = contextPayload(input.context);
|
|
2112
|
+
const fields = input.fields.map((field) => ({
|
|
2113
|
+
objectTypeId: field.objectTypeId || "0-1",
|
|
2114
|
+
name: field.name,
|
|
2115
|
+
value: field.value
|
|
2116
|
+
}));
|
|
2015
2117
|
const base = {
|
|
2016
|
-
fields
|
|
2118
|
+
fields,
|
|
2017
2119
|
...Object.keys(context).length ? { context } : {}
|
|
2018
2120
|
};
|
|
2019
|
-
const
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
} : base;
|
|
2121
|
+
const legal = buildLegalConsent(
|
|
2122
|
+
input.shape,
|
|
2123
|
+
input.consent?.given === true,
|
|
2124
|
+
input.consent?.text
|
|
2125
|
+
);
|
|
2126
|
+
const withConsent = legal ? { ...base, legalConsentOptions: legal } : base;
|
|
2028
2127
|
let result = await postForm(url, input.apiKey, withConsent);
|
|
2029
|
-
|
|
2128
|
+
const mustKeepConsent = Boolean(input.shape?.hasLegalConsent && legal);
|
|
2129
|
+
if (!result.ok && result.status === 400 && withConsent !== base && !mustKeepConsent) {
|
|
2030
2130
|
result = await postForm(url, input.apiKey, base);
|
|
2031
2131
|
}
|
|
2032
|
-
if (!result.ok && result.status === 400 && input.fields.length > 1) {
|
|
2033
|
-
const email =
|
|
2034
|
-
if (email)
|
|
2132
|
+
if (!result.ok && result.status === 400 && input.fields.length > 1 && canRetryEmailOnly(input.shape?.required)) {
|
|
2133
|
+
const email = fields.find((f) => f.name === "email");
|
|
2134
|
+
if (email) {
|
|
2135
|
+
const slim = { ...base, fields: [email] };
|
|
2136
|
+
result = await postForm(
|
|
2137
|
+
url,
|
|
2138
|
+
input.apiKey,
|
|
2139
|
+
legal && mustKeepConsent ? { ...slim, legalConsentOptions: legal } : slim
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2035
2142
|
}
|
|
2036
2143
|
return result;
|
|
2037
2144
|
}
|
|
@@ -2413,6 +2520,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2413
2520
|
}
|
|
2414
2521
|
let usedFormsApi = false;
|
|
2415
2522
|
let sentFieldNames = [];
|
|
2523
|
+
let conversionError;
|
|
2416
2524
|
if (email && apiKey) {
|
|
2417
2525
|
const groups = groupByObject(form.definition, resolution.values);
|
|
2418
2526
|
for (const [object, props] of Object.entries(companyGroups)) {
|
|
@@ -2431,12 +2539,16 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2431
2539
|
if (tryFormsApi && portalId && formGuid) {
|
|
2432
2540
|
try {
|
|
2433
2541
|
const shape = await loadFormShape(apiKey, formGuid);
|
|
2434
|
-
const fields =
|
|
2435
|
-
|
|
2436
|
-
|
|
2542
|
+
const fields = fieldsForHubspotForm(
|
|
2543
|
+
{
|
|
2544
|
+
contact: contactProps,
|
|
2545
|
+
company: stringProps(partition.accepted.company ?? {})
|
|
2546
|
+
},
|
|
2547
|
+
shape,
|
|
2548
|
+
{ email }
|
|
2437
2549
|
);
|
|
2438
2550
|
if (fields.length && !fields.some((f) => f.name === "email")) {
|
|
2439
|
-
fields.unshift({ name: "email", value: email });
|
|
2551
|
+
fields.unshift({ name: "email", value: email, objectTypeId: "0-1" });
|
|
2440
2552
|
}
|
|
2441
2553
|
sentFieldNames = fields.map((f) => f.name);
|
|
2442
2554
|
const submitted = await submitMarketingForm({
|
|
@@ -2444,7 +2556,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2444
2556
|
portalId,
|
|
2445
2557
|
formGuid,
|
|
2446
2558
|
region,
|
|
2447
|
-
fields: fields.length ? fields : [{ name: "email", value: email }],
|
|
2559
|
+
fields: fields.length ? fields : [{ name: "email", value: email, objectTypeId: "0-1" }],
|
|
2448
2560
|
context: {
|
|
2449
2561
|
hutk: sanitizeHutk(meta.hutk),
|
|
2450
2562
|
pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
|
|
@@ -2452,7 +2564,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2452
2564
|
ipAddress: sanitizeIp(meta.ipAddress)
|
|
2453
2565
|
},
|
|
2454
2566
|
consent: { given: meta.consent === true },
|
|
2455
|
-
|
|
2567
|
+
shape
|
|
2456
2568
|
});
|
|
2457
2569
|
if (submitted.ok) {
|
|
2458
2570
|
hubspotSynced = true;
|
|
@@ -2465,10 +2577,12 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2465
2577
|
);
|
|
2466
2578
|
}
|
|
2467
2579
|
} else {
|
|
2580
|
+
conversionError = submitted.error;
|
|
2468
2581
|
strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
|
|
2469
2582
|
}
|
|
2470
2583
|
} catch (err) {
|
|
2471
|
-
|
|
2584
|
+
conversionError = err.message;
|
|
2585
|
+
strapi.log.warn(`[hubspot] Forms API submit failed — ${conversionError}`);
|
|
2472
2586
|
}
|
|
2473
2587
|
}
|
|
2474
2588
|
if (!usedFormsApi) {
|
|
@@ -2550,7 +2664,11 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2550
2664
|
formTitle: form.title || form.name,
|
|
2551
2665
|
email,
|
|
2552
2666
|
values: resolution.values,
|
|
2553
|
-
meta
|
|
2667
|
+
meta: {
|
|
2668
|
+
...meta,
|
|
2669
|
+
hubspotConversion: usedFormsApi,
|
|
2670
|
+
...conversionError ? { hubspotError: conversionError } : {}
|
|
2671
|
+
},
|
|
2554
2672
|
locale: form.locale ?? null,
|
|
2555
2673
|
hubspotSynced,
|
|
2556
2674
|
contactId,
|
|
@@ -2737,7 +2855,7 @@ function createFormsAdminController(strapi) {
|
|
|
2737
2855
|
locale: ctx.query.locale,
|
|
2738
2856
|
status: "published"
|
|
2739
2857
|
});
|
|
2740
|
-
ctx.body = { form: entry, published: Boolean(published) };
|
|
2858
|
+
ctx.body = { form: entry, published: Boolean(published), publishedHubspotFormId: published?.hubspotFormId ?? null };
|
|
2741
2859
|
},
|
|
2742
2860
|
async create(ctx) {
|
|
2743
2861
|
const body = ctx.request.body ?? {};
|
|
@@ -23,20 +23,91 @@ export declare function toHsFields(props: Record<string, string>): {
|
|
|
23
23
|
name: string;
|
|
24
24
|
value: string;
|
|
25
25
|
}[];
|
|
26
|
-
export declare function filterToFormFields
|
|
26
|
+
export declare function filterToFormFields<T extends {
|
|
27
27
|
name: string;
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
}>(fields: T[], allowed: Set<string> | null): T[];
|
|
29
|
+
export declare function formFieldNames(raw: {
|
|
30
|
+
fieldGroups?: {
|
|
31
|
+
fields?: {
|
|
32
|
+
name?: string;
|
|
33
|
+
required?: boolean;
|
|
34
|
+
}[];
|
|
35
|
+
}[];
|
|
36
|
+
}): string[];
|
|
37
|
+
export declare function formRequiredNames(raw: {
|
|
38
|
+
fieldGroups?: {
|
|
39
|
+
fields?: {
|
|
40
|
+
name?: string;
|
|
41
|
+
required?: boolean;
|
|
42
|
+
}[];
|
|
43
|
+
}[];
|
|
44
|
+
}): string[];
|
|
45
|
+
export interface CommunicationConsent {
|
|
46
|
+
subscriptionTypeId: number;
|
|
47
|
+
text: string;
|
|
48
|
+
}
|
|
49
|
+
export interface FormShapeField {
|
|
50
|
+
name: string;
|
|
51
|
+
objectTypeId: string;
|
|
52
|
+
required: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface FormShape {
|
|
55
|
+
names: Set<string>;
|
|
56
|
+
required: Set<string>;
|
|
57
|
+
fields: FormShapeField[];
|
|
58
|
+
hasLegalConsent: boolean;
|
|
59
|
+
legalType?: string;
|
|
60
|
+
lawfulBasis?: "LEAD" | "CUSTOMER";
|
|
61
|
+
subscriptionTypeIds: number[];
|
|
62
|
+
privacyText?: string;
|
|
63
|
+
captcha: boolean;
|
|
64
|
+
consentToProcessText?: string;
|
|
65
|
+
communications: CommunicationConsent[];
|
|
66
|
+
}
|
|
67
|
+
export declare function formShapeFields(raw: {
|
|
68
|
+
fieldGroups?: {
|
|
69
|
+
fields?: {
|
|
70
|
+
name?: string;
|
|
71
|
+
required?: boolean;
|
|
72
|
+
objectTypeId?: string;
|
|
73
|
+
}[];
|
|
74
|
+
}[];
|
|
75
|
+
}): FormShapeField[];
|
|
76
|
+
/** Values HubSpot declared on the marketing form, with the right objectTypeId. */
|
|
77
|
+
export declare function fieldsForHubspotForm(bags: Record<string, Record<string, string>>, shape: FormShape | null | undefined, extra?: Record<string, string>): {
|
|
30
78
|
name: string;
|
|
31
79
|
value: string;
|
|
80
|
+
objectTypeId: string;
|
|
32
81
|
}[];
|
|
33
|
-
export declare function
|
|
82
|
+
export declare function parseFormShape(raw: {
|
|
34
83
|
fieldGroups?: {
|
|
35
84
|
fields?: {
|
|
36
85
|
name?: string;
|
|
86
|
+
required?: boolean;
|
|
87
|
+
objectTypeId?: string;
|
|
37
88
|
}[];
|
|
38
89
|
}[];
|
|
39
|
-
|
|
90
|
+
configuration?: {
|
|
91
|
+
captchaEnabled?: boolean;
|
|
92
|
+
recaptchaEnabled?: boolean;
|
|
93
|
+
};
|
|
94
|
+
legalConsentOptions?: {
|
|
95
|
+
type?: string;
|
|
96
|
+
lawfulBasis?: string;
|
|
97
|
+
privacyText?: string;
|
|
98
|
+
subscriptionTypeIds?: (number | string)[];
|
|
99
|
+
consentToProcessText?: string;
|
|
100
|
+
communicationsCheckboxes?: {
|
|
101
|
+
label?: string;
|
|
102
|
+
text?: string;
|
|
103
|
+
subscriptionTypeId?: number | string;
|
|
104
|
+
communicationTypeId?: number | string;
|
|
105
|
+
}[];
|
|
106
|
+
} | null;
|
|
107
|
+
}): FormShape;
|
|
108
|
+
/** Email-only retry is only safe when HubSpot doesn't require other fields. */
|
|
109
|
+
export declare function canRetryEmailOnly(required: Iterable<string> | undefined): boolean;
|
|
110
|
+
export declare function buildLegalConsent(shape: Pick<FormShape, "hasLegalConsent" | "legalType" | "lawfulBasis" | "subscriptionTypeIds" | "privacyText" | "communications" | "consentToProcessText"> | null | undefined, given: boolean, fallbackText?: string): Record<string, unknown> | undefined;
|
|
40
111
|
export interface FormSubmitContext {
|
|
41
112
|
hutk?: string;
|
|
42
113
|
pageUri?: string;
|
|
@@ -51,24 +122,25 @@ export interface FormSubmitInput {
|
|
|
51
122
|
fields: {
|
|
52
123
|
name: string;
|
|
53
124
|
value: string;
|
|
125
|
+
objectTypeId?: string;
|
|
54
126
|
}[];
|
|
55
127
|
context?: FormSubmitContext;
|
|
56
128
|
consent?: {
|
|
57
129
|
given: boolean;
|
|
58
130
|
text?: string;
|
|
59
131
|
};
|
|
60
|
-
|
|
132
|
+
shape?: FormShape | null;
|
|
61
133
|
}
|
|
62
134
|
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>;
|
|
135
|
+
export declare function loadFormShape(apiKey: string, formGuid: string): Promise<FormShape | null>;
|
|
67
136
|
export declare function findContactIdByEmail(apiKey: string, email: string): Promise<string | undefined>;
|
|
68
137
|
/**
|
|
69
138
|
* Submit a marketing form. Extra fields HubSpot doesn't know on that form
|
|
70
|
-
* 400 the whole request, so callers should filter first
|
|
71
|
-
*
|
|
139
|
+
* 400 the whole request, so callers should filter first. A GDPR block on the
|
|
140
|
+
* HubSpot form needs the matching legalConsentOptions (including
|
|
141
|
+
* communications); stripping consent to "retry" would 400 forever. Email-only
|
|
142
|
+
* retry is only used when HubSpot doesn't require other fields. CAPTCHA
|
|
143
|
+
* cannot be solved server-side.
|
|
72
144
|
*/
|
|
73
145
|
export declare function submitMarketingForm(input: FormSubmitInput): Promise<{
|
|
74
146
|
ok: boolean;
|