From 919603d946504209a2cf14a8e8394e69e0a3c8fe Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Wed, 18 Feb 2026 15:35:32 +0100 Subject: [PATCH 1/5] feat(sign-up): integrate HubSpot signup functionality and remove Cargo integration --- .../users-sign-up/feature/src/index.ts | 1 + .../use-create-user-sign-up.ts | 10 ++- .../use-signup-cargo/use-signup-cargo.ts | 53 ------------ .../use-signup-hubspot/use-signup-hubspot.ts | 83 +++++++++++++++++++ .../src/lib/feature/container/container.tsx | 8 -- 5 files changed, 90 insertions(+), 65 deletions(-) delete mode 100644 libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-cargo/use-signup-cargo.ts create mode 100644 libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts diff --git a/libs/domains/users-sign-up/feature/src/index.ts b/libs/domains/users-sign-up/feature/src/index.ts index 74051acaeda..106f8c6f461 100644 --- a/libs/domains/users-sign-up/feature/src/index.ts +++ b/libs/domains/users-sign-up/feature/src/index.ts @@ -1,2 +1,3 @@ export * from './lib/hooks/use-user-sign-up/use-user-sign-up' export * from './lib/hooks/use-create-user-sign-up/use-create-user-sign-up' +export * from './lib/hooks/use-signup-hubspot/use-signup-hubspot' diff --git a/libs/domains/users-sign-up/feature/src/lib/hooks/use-create-user-sign-up/use-create-user-sign-up.ts b/libs/domains/users-sign-up/feature/src/lib/hooks/use-create-user-sign-up/use-create-user-sign-up.ts index 2a2e6d677c8..28eecde5d07 100644 --- a/libs/domains/users-sign-up/feature/src/lib/hooks/use-create-user-sign-up/use-create-user-sign-up.ts +++ b/libs/domains/users-sign-up/feature/src/lib/hooks/use-create-user-sign-up/use-create-user-sign-up.ts @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { mutations } from '@qovery/domains/users-sign-up/data-access' import { queries } from '@qovery/state/util-queries' -import { type CargoSignupPayload, useSignUpCargo } from '../use-signup-cargo/use-signup-cargo' +import { type HubspotSignupPayload, useSignUpHubspot } from '../use-signup-hubspot/use-signup-hubspot' function getUtmParams() { return { @@ -16,7 +16,7 @@ function getUtmParams() { export function useCreateUserSignUp() { const queryClient = useQueryClient() - const { mutate: signUpCargo } = useSignUpCargo() + const { mutate: signUpHubspot } = useSignUpHubspot() return useMutation(mutations.createUserSignup, { onSuccess(_, variables) { @@ -26,7 +26,7 @@ export function useCreateUserSignUp() { const utmParams = getUtmParams() - const cargoPayload: CargoSignupPayload = { + const signupPayload: HubspotSignupPayload = { email: variables.user_email, first_name: variables.first_name, last_name: variables.last_name, @@ -34,9 +34,11 @@ export function useCreateUserSignUp() { job_title: variables.user_role || '', phone: variables.phone ?? '', signup_source: 'Console', + qovery_interest: variables.qovery_usage ?? 'Not specified', + which_cloud_service_provider_do_you_use_: variables.infrastructure_hosting ?? 'Not specified', ...utmParams, } - signUpCargo(cargoPayload) + signUpHubspot(signupPayload) }, meta: { notifyOnError: true, diff --git a/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-cargo/use-signup-cargo.ts b/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-cargo/use-signup-cargo.ts deleted file mode 100644 index c5b35b6a50a..00000000000 --- a/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-cargo/use-signup-cargo.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useMutation } from '@tanstack/react-query' -import { CARGO_API_TOKEN } from '@qovery/shared/util-node-env' - -export interface CargoSignupPayload { - email: string - first_name: string - last_name: string - company: string - job_title: string - signup_source: string - phone: string - utm_source?: string | null - utm_medium?: string | null - utm_campaign?: string | null - utm_term?: string | null - utm_content?: string | null - gclid?: string | null -} - -const CARGO_API_URL = 'https://api.getcargo.io/v1/models/7e42e545-bdee-438b-b2dc-3799e95bf046/records/ingest' - -// https://qovery.slack.com/archives/C08M2RT8T29/p1746543979992499 -export function useSignUpCargo() { - const postToCargo = async (payload: CargoSignupPayload) => { - try { - const response = await fetch(`${CARGO_API_URL}?token=${CARGO_API_TOKEN}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }) - - if (!response.ok) { - throw new Error(`Error posting signup data to Cargo: ${response.status} ${response.statusText}`) - } - - return await response.json() - } catch (error) { - console.error('Error posting signup data to Cargo:', error) - throw error - } - } - - return useMutation({ - mutationFn: postToCargo, - meta: { - notifyOnError: true, - }, - }) -} - -export default useSignUpCargo diff --git a/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts b/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts new file mode 100644 index 00000000000..f2bd9b70c60 --- /dev/null +++ b/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts @@ -0,0 +1,83 @@ +import { useMutation } from '@tanstack/react-query' + +const HUBSPOT_PORTAL_ID = '' // TODO Add HubSpot portal ID +const HUBSPOT_FORM_ID = '' // TODO Add HubSpot form ID +const HUBSPOT_SUBMIT_URL = `https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_ID}` + +export interface HubspotSignupPayload { + email: string + first_name: string + last_name: string + company: string + job_title: string + signup_source: string + phone: string + /** Maps to HubSpot required field qovery_interest (e.g. from qovery_usage) */ + qovery_interest: string + /** Maps to HubSpot required field which_cloud_service_provider_do_you_use_ */ + which_cloud_service_provider_do_you_use_: string + utm_source?: string | null + utm_medium?: string | null + utm_campaign?: string | null + utm_term?: string | null + utm_content?: string | null + gclid?: string | null +} + +function toHubspotFields(payload: HubspotSignupPayload): { name: string; value: string }[] { + const fields: { name: string; value: string }[] = [ + { name: 'email', value: payload.email }, + { name: 'firstname', value: payload.first_name }, + { name: 'lastname', value: payload.last_name }, + { name: 'company', value: payload.company }, + { name: 'jobtitle', value: payload.job_title }, + { name: 'phone', value: payload.phone || '—' }, + { name: 'signup_source', value: payload.signup_source }, + { name: 'qovery_interest', value: payload.qovery_interest || 'Not specified' }, + { + name: 'which_cloud_service_provider_do_you_use_', + value: payload.which_cloud_service_provider_do_you_use_ || 'Not specified', + }, + ] + if (payload.utm_source != null && payload.utm_source !== '') + fields.push({ name: 'utm_source', value: payload.utm_source }) + if (payload.utm_medium != null && payload.utm_medium !== '') + fields.push({ name: 'utm_medium', value: payload.utm_medium }) + if (payload.utm_campaign != null && payload.utm_campaign !== '') + fields.push({ name: 'utm_campaign', value: payload.utm_campaign }) + if (payload.utm_term != null && payload.utm_term !== '') fields.push({ name: 'utm_term', value: payload.utm_term }) + if (payload.utm_content != null && payload.utm_content !== '') + fields.push({ name: 'utm_content', value: payload.utm_content }) + if (payload.gclid != null && payload.gclid !== '') fields.push({ name: 'gclid', value: payload.gclid }) + return fields +} + +export function useSignUpHubspot() { + const postToHubspot = async (payload: HubspotSignupPayload) => { + const response = await fetch(HUBSPOT_SUBMIT_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fields: toHubspotFields(payload), + }), + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`Error posting signup data to HubSpot: ${response.status} ${response.statusText} - ${text}`) + } + + return response.json() + } + + return useMutation({ + mutationFn: postToHubspot, + meta: { + notifyOnError: true, + }, + }) +} + +export default useSignUpHubspot diff --git a/libs/pages/onboarding/src/lib/feature/container/container.tsx b/libs/pages/onboarding/src/lib/feature/container/container.tsx index 68bdd0fb484..aa135101421 100644 --- a/libs/pages/onboarding/src/lib/feature/container/container.tsx +++ b/libs/pages/onboarding/src/lib/feature/container/container.tsx @@ -66,14 +66,6 @@ export function Container(props: PropsWithChildren) { const hasExistingOrganization = organizations.length > 0 const totalSteps = hasExistingOrganization || hasDxAuth ? 2 : ROUTER_ONBOARDING.length - useEffect(() => { - if (hasDxAuth) { - navigate(`${ONBOARDING_URL}${ONBOARDING_PROJECT_URL}`, { - state: location.state, - }) - } - }, [currentPath, hasDxAuth, navigate, location.state]) - const currentStep = hasExistingOrganization ? currentPath === ONBOARDING_PROJECT_URL ? 2 From 33c7eebf24a28c34c97e30cf34ef779a5a9e8150 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Wed, 18 Feb 2026 16:32:29 +0100 Subject: [PATCH 2/5] feat(sign-up): update HubSpot integration with portal and form IDs --- .../src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts b/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts index f2bd9b70c60..d16b9ae8b43 100644 --- a/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts +++ b/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts @@ -1,7 +1,7 @@ import { useMutation } from '@tanstack/react-query' -const HUBSPOT_PORTAL_ID = '' // TODO Add HubSpot portal ID -const HUBSPOT_FORM_ID = '' // TODO Add HubSpot form ID +const HUBSPOT_PORTAL_ID = '25346960' +const HUBSPOT_FORM_ID = '159ac368-09f9-4596-8d1e-9634bc0a4a8b' const HUBSPOT_SUBMIT_URL = `https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_ID}` export interface HubspotSignupPayload { From e9c7416058fe0ec68e6210d0d82a3bb323a88b86 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Wed, 18 Feb 2026 16:36:26 +0100 Subject: [PATCH 3/5] feat(sign-up): refactor tracking parameters handling in HubSpot signup integration --- .../use-create-user-sign-up.ts | 16 ++------ .../use-signup-hubspot/use-signup-hubspot.ts | 24 ++++-------- .../src/lib/use-utm-params/use-utm-params.ts | 38 +++++++++++++++++-- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/libs/domains/users-sign-up/feature/src/lib/hooks/use-create-user-sign-up/use-create-user-sign-up.ts b/libs/domains/users-sign-up/feature/src/lib/hooks/use-create-user-sign-up/use-create-user-sign-up.ts index 28eecde5d07..88883a8c9b1 100644 --- a/libs/domains/users-sign-up/feature/src/lib/hooks/use-create-user-sign-up/use-create-user-sign-up.ts +++ b/libs/domains/users-sign-up/feature/src/lib/hooks/use-create-user-sign-up/use-create-user-sign-up.ts @@ -1,19 +1,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { mutations } from '@qovery/domains/users-sign-up/data-access' +import { getStoredTrackingParams } from '@qovery/shared/util-hooks' import { queries } from '@qovery/state/util-queries' import { type HubspotSignupPayload, useSignUpHubspot } from '../use-signup-hubspot/use-signup-hubspot' -function getUtmParams() { - return { - utm_source: localStorage.getItem('utm_source'), - utm_medium: localStorage.getItem('utm_medium'), - utm_campaign: localStorage.getItem('utm_campaign'), - utm_term: localStorage.getItem('utm_term'), - utm_content: localStorage.getItem('utm_content'), - gclid: localStorage.getItem('gclid'), - } -} - export function useCreateUserSignUp() { const queryClient = useQueryClient() const { mutate: signUpHubspot } = useSignUpHubspot() @@ -24,7 +14,7 @@ export function useCreateUserSignUp() { queryKey: queries.usersSignUp.get.queryKey, }) - const utmParams = getUtmParams() + const trackingParams = getStoredTrackingParams() const signupPayload: HubspotSignupPayload = { email: variables.user_email, @@ -36,7 +26,7 @@ export function useCreateUserSignUp() { signup_source: 'Console', qovery_interest: variables.qovery_usage ?? 'Not specified', which_cloud_service_provider_do_you_use_: variables.infrastructure_hosting ?? 'Not specified', - ...utmParams, + tracking_params: trackingParams, } signUpHubspot(signupPayload) }, diff --git a/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts b/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts index d16b9ae8b43..380e2c561ca 100644 --- a/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts +++ b/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts @@ -16,12 +16,8 @@ export interface HubspotSignupPayload { qovery_interest: string /** Maps to HubSpot required field which_cloud_service_provider_do_you_use_ */ which_cloud_service_provider_do_you_use_: string - utm_source?: string | null - utm_medium?: string | null - utm_campaign?: string | null - utm_term?: string | null - utm_content?: string | null - gclid?: string | null + /** All URL/tracking params captured at landing (utm_*, gclid, etc.) — sent as HubSpot fields */ + tracking_params?: Record } function toHubspotFields(payload: HubspotSignupPayload): { name: string; value: string }[] { @@ -39,16 +35,12 @@ function toHubspotFields(payload: HubspotSignupPayload): { name: string; value: value: payload.which_cloud_service_provider_do_you_use_ || 'Not specified', }, ] - if (payload.utm_source != null && payload.utm_source !== '') - fields.push({ name: 'utm_source', value: payload.utm_source }) - if (payload.utm_medium != null && payload.utm_medium !== '') - fields.push({ name: 'utm_medium', value: payload.utm_medium }) - if (payload.utm_campaign != null && payload.utm_campaign !== '') - fields.push({ name: 'utm_campaign', value: payload.utm_campaign }) - if (payload.utm_term != null && payload.utm_term !== '') fields.push({ name: 'utm_term', value: payload.utm_term }) - if (payload.utm_content != null && payload.utm_content !== '') - fields.push({ name: 'utm_content', value: payload.utm_content }) - if (payload.gclid != null && payload.gclid !== '') fields.push({ name: 'gclid', value: payload.gclid }) + const tracking = payload.tracking_params ?? {} + for (const [name, value] of Object.entries(tracking)) { + if (name && value != null && value !== '') { + fields.push({ name, value }) + } + } return fields } diff --git a/libs/shared/util-hooks/src/lib/use-utm-params/use-utm-params.ts b/libs/shared/util-hooks/src/lib/use-utm-params/use-utm-params.ts index 79b88993f82..ca79efa8627 100644 --- a/libs/shared/util-hooks/src/lib/use-utm-params/use-utm-params.ts +++ b/libs/shared/util-hooks/src/lib/use-utm-params/use-utm-params.ts @@ -12,6 +12,8 @@ export interface UtmParams { const UTM_KEYS: (keyof UtmParams)[] = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid'] +export const TRACKING_PARAMS_STORAGE_KEY = 'tracking_params' + export function getUtmParams(searchParams: URLSearchParams): UtmParams { const utmParams: UtmParams = {} @@ -25,6 +27,30 @@ export function getUtmParams(searchParams: URLSearchParams): UtmParams { return utmParams } +/** Returns all URL params stored at landing. Fallback: legacy UTM/gclid keys from localStorage. */ +export function getStoredTrackingParams(): Record { + try { + const raw = localStorage.getItem(TRACKING_PARAMS_STORAGE_KEY) + if (raw) { + const parsed = JSON.parse(raw) as Record + return Object.fromEntries( + Object.entries(parsed).filter( + (entry): entry is [string, string] => + typeof entry[0] === 'string' && typeof entry[1] === 'string' && entry[1].length > 0 + ) + ) + } + } catch { + // ignore invalid JSON + } + const fallback: Record = {} + UTM_KEYS.forEach((key) => { + const value = localStorage.getItem(key) + if (value) fallback[key] = value + }) + return fallback +} + export function useUtmParams(): UtmParams { const [searchParams] = useSearchParams() @@ -33,14 +59,20 @@ export function useUtmParams(): UtmParams { return utmParams } +/** Persists all current URL search params to localStorage for later use (e.g. HubSpot signup). */ export function useCaptureUtmParams() { useEffect(() => { const params = new URLSearchParams(window.location.search) + const all: Record = {} + params.forEach((value, key) => { + if (value) all[key] = value + }) + if (Object.keys(all).length > 0) { + localStorage.setItem(TRACKING_PARAMS_STORAGE_KEY, JSON.stringify(all)) + } UTM_KEYS.forEach((key) => { const value = params.get(key) - if (value) { - localStorage.setItem(key, value) - } + if (value) localStorage.setItem(key, value) }) }, []) } From 34ef73ace42ca24e4d52bb353c52bd96e37f0d9e Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 19 Feb 2026 10:00:15 +0100 Subject: [PATCH 4/5] fix(sign-up): update HubSpot form ID for signup integration --- .../src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts b/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts index 380e2c561ca..c42ab35ffc6 100644 --- a/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts +++ b/libs/domains/users-sign-up/feature/src/lib/hooks/use-signup-hubspot/use-signup-hubspot.ts @@ -1,7 +1,7 @@ import { useMutation } from '@tanstack/react-query' const HUBSPOT_PORTAL_ID = '25346960' -const HUBSPOT_FORM_ID = '159ac368-09f9-4596-8d1e-9634bc0a4a8b' +const HUBSPOT_FORM_ID = '70df367c-130e-4bf8-866a-c972f565c67a' const HUBSPOT_SUBMIT_URL = `https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_ID}` export interface HubspotSignupPayload { From 3822f500310e18b651710fa5910050ac113793bd Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 19 Feb 2026 10:34:52 +0100 Subject: [PATCH 5/5] refactor(tracking-params): simplify UTM parameter handling and enhance tracking functionality - Replaced UTM-specific parameter handling with a more generic approach to capture all URL search parameters. - Updated function names and types for clarity, changing `getUtmParams` to `getTrackingParams` and `useUtmParams` to `useTrackingParams`. - Removed legacy UTM key handling from localStorage, streamlining the storage logic for tracking parameters. --- .../src/lib/use-utm-params/use-utm-params.ts | 64 ++++++------------- 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/libs/shared/util-hooks/src/lib/use-utm-params/use-utm-params.ts b/libs/shared/util-hooks/src/lib/use-utm-params/use-utm-params.ts index ca79efa8627..eb5ee0540cf 100644 --- a/libs/shared/util-hooks/src/lib/use-utm-params/use-utm-params.ts +++ b/libs/shared/util-hooks/src/lib/use-utm-params/use-utm-params.ts @@ -1,33 +1,18 @@ import { useEffect, useMemo } from 'react' import { useSearchParams } from 'react-router-dom' -export interface UtmParams { - utm_source?: string - utm_medium?: string - utm_campaign?: string - utm_term?: string - utm_content?: string - gclid?: string -} - -const UTM_KEYS: (keyof UtmParams)[] = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid'] - export const TRACKING_PARAMS_STORAGE_KEY = 'tracking_params' -export function getUtmParams(searchParams: URLSearchParams): UtmParams { - const utmParams: UtmParams = {} - - for (const key of UTM_KEYS) { - const value = searchParams.get(key) - if (value) { - utmParams[key] = value - } - } - - return utmParams +/** Returns all URL search params as a record (dynamic keys only). */ +export function getTrackingParams(searchParams: URLSearchParams): Record { + const result: Record = {} + searchParams.forEach((value, key) => { + if (value) result[key] = value + }) + return result } -/** Returns all URL params stored at landing. Fallback: legacy UTM/gclid keys from localStorage. */ +/** Returns all URL params stored at landing. */ export function getStoredTrackingParams(): Record { try { const raw = localStorage.getItem(TRACKING_PARAMS_STORAGE_KEY) @@ -43,36 +28,29 @@ export function getStoredTrackingParams(): Record { } catch { // ignore invalid JSON } - const fallback: Record = {} - UTM_KEYS.forEach((key) => { - const value = localStorage.getItem(key) - if (value) fallback[key] = value - }) - return fallback + return {} } -export function useUtmParams(): UtmParams { +export function useTrackingParams(): Record { const [searchParams] = useSearchParams() - - const utmParams = useMemo(() => getUtmParams(searchParams), [searchParams]) - - return utmParams + return useMemo(() => getTrackingParams(searchParams), [searchParams]) } -/** Persists all current URL search params to localStorage for later use (e.g. HubSpot signup). */ +// Auth0 OAuth callback params — excluded from tracking storage and from payloads sent to HubSpot +const OAUTH_PARAMS = ['code', 'state'] + +/** Persists URL tracking params to localStorage. Merges with existing so Auth0 redirect (?code=&state=) does not overwrite UTMs. Excludes Auth0 code/state from storage. */ export function useCaptureUtmParams() { useEffect(() => { const params = new URLSearchParams(window.location.search) - const all: Record = {} + const fromUrl: Record = {} params.forEach((value, key) => { - if (value) all[key] = value + if (value && !OAUTH_PARAMS.includes(key)) fromUrl[key] = value }) - if (Object.keys(all).length > 0) { - localStorage.setItem(TRACKING_PARAMS_STORAGE_KEY, JSON.stringify(all)) + const existing = getStoredTrackingParams() + const merged = { ...existing, ...fromUrl } + if (Object.keys(merged).length > 0) { + localStorage.setItem(TRACKING_PARAMS_STORAGE_KEY, JSON.stringify(merged)) } - UTM_KEYS.forEach((key) => { - const value = params.get(key) - if (value) localStorage.setItem(key, value) - }) }, []) }