From 4e9fba4978b8680cca00807f52ee9ccc2861ecef Mon Sep 17 00:00:00 2001 From: Jakob Holmgren Date: Fri, 9 Oct 2026 17:11:17 +0200 Subject: [PATCH] fix(Billing): Contact Sales opens the sales form instead of support chat The Enterprise "Contact Sales" button opened the Pylon support chat, so it did nothing for free-plan organisations and will stop working once Pylon chat is switched off. It now opens the website's "Contact Enterprise Sales" HubSpot form in a side panel, prefilled from the signed-in user, with a link to the website form if HubSpot is blocked. HubSpot's form embed resizes its iframe from a ResizeObserver, which the dev server overlay reported as a runtime error on close. The local dev config now ignores that benign error and otherwise keeps the overlay defaults. Co-Authored-By: Claude Opus 5.5 --- frontend/rspack/rspack.config.local.js | 12 +++ .../modals/payment/ContactSalesForm.tsx | 83 +++++++++++++++++++ .../modals/payment/PricingPanel.tsx | 10 ++- .../components/modals/payment/constants.ts | 11 +++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 frontend/web/components/modals/payment/ContactSalesForm.tsx diff --git a/frontend/rspack/rspack.config.local.js b/frontend/rspack/rspack.config.local.js index 0a9fa8fdf9b0..c5d5dd74008b 100644 --- a/frontend/rspack/rspack.config.local.js +++ b/frontend/rspack/rspack.config.local.js @@ -9,6 +9,18 @@ const base = require('../rspack.config') module.exports = { ...base, devServer: { + client: { + overlay: { + // Same as the dev server's defaults, which this object replaces. + errors: true, + // HubSpot's embedded forms resize their iframe from a ResizeObserver, + // which browsers report as this benign error (Sentry drops it too). + // Self-contained on purpose: the dev server rebuilds it in the browser. + runtimeErrors: (error) => + !/ResizeObserver loop/.test(String(error && error.message)), + warnings: false, + }, + }, historyApiFallback: true, hot: true, liveReload: false, diff --git a/frontend/web/components/modals/payment/ContactSalesForm.tsx b/frontend/web/components/modals/payment/ContactSalesForm.tsx new file mode 100644 index 000000000000..9f1c401236bb --- /dev/null +++ b/frontend/web/components/modals/payment/ContactSalesForm.tsx @@ -0,0 +1,83 @@ +import React, { FC, useEffect } from 'react' +import { useScript } from 'common/hooks/useScript' +import AccountStore from 'common/stores/account-store' +import { AccountModel } from 'common/types/responses' +import { + CONTACT_SALES_FORM, + CONTACT_SALES_URL, + HUBSPOT_FORMS_SCRIPT_URL, +} from './constants' + +// HubSpot passes a jQuery object when jQuery is on the page, as it is here. +type HubSpotFormArg = HTMLFormElement | { jquery: string; 0: HTMLFormElement } + +type HubSpotFormOptions = typeof CONTACT_SALES_FORM & { + target: string + onFormReady: (form: HubSpotFormArg) => void +} + +declare global { + interface Window { + hbspt?: { forms: { create: (options: HubSpotFormOptions) => void } } + } +} + +const TARGET_ID = 'contact-sales-form' + +// The form renders in an iframe, so set values the way HubSpot documents: +// assign, then fire an input event so the form registers them. +const prefill = (arg: HubSpotFormArg) => { + const form = 'jquery' in arg ? arg[0] : arg + const user = AccountStore.getUser() as AccountModel | undefined + if (!form || !user) return + const values: Record = { + email: user.email, + firstname: user.first_name, + lastname: user.last_name, + } + Object.entries(values).forEach(([name, value]) => { + const input = form.querySelector(`input[name="${name}"]`) + if (!input || !value) return + input.value = value + input.dispatchEvent(new Event('input', { bubbles: true })) + }) +} + +const ContactSalesForm: FC = () => { + const { error, ready } = useScript(HUBSPOT_FORMS_SCRIPT_URL) + + useEffect(() => { + if (ready && window.hbspt) { + window.hbspt.forms.create({ + ...CONTACT_SALES_FORM, + onFormReady: prefill, + target: `#${TARGET_ID}`, + }) + } + }, [ready]) + + // Ad blockers commonly block HubSpot, so always leave a way through. + if (error || (ready && !window.hbspt)) { + return ( +
+ We couldn't load the form here. Please{' '} + + contact us on our website + + . +
+ ) + } + + // HubSpot styles the form itself (dark labels), so it sits on a white + // card to stay readable in dark mode. + return ( +
+
+
+
+
+ ) +} + +export default ContactSalesForm diff --git a/frontend/web/components/modals/payment/PricingPanel.tsx b/frontend/web/components/modals/payment/PricingPanel.tsx index be96dd198275..ecf469637956 100644 --- a/frontend/web/components/modals/payment/PricingPanel.tsx +++ b/frontend/web/components/modals/payment/PricingPanel.tsx @@ -4,7 +4,7 @@ import Icon from 'components/icons/Icon' import Button from 'components/base/forms/Button' import { PricingFeaturesList } from './PricingFeaturesList' import { PaymentButton } from './PaymentButton' -import { openChat } from 'common/loadChat' +import ContactSalesForm from './ContactSalesForm' import { PricingFeature } from './types' export type PricingPanelProps = { @@ -110,7 +110,13 @@ export const PricingPanel = ({ {isEnterprise && (