Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/domains/users-sign-up/feature/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -1,42 +1,34 @@
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 CargoSignupPayload, useSignUpCargo } from '../use-signup-cargo/use-signup-cargo'

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'),
}
}
import { type HubspotSignupPayload, useSignUpHubspot } from '../use-signup-hubspot/use-signup-hubspot'

export function useCreateUserSignUp() {
const queryClient = useQueryClient()
const { mutate: signUpCargo } = useSignUpCargo()
const { mutate: signUpHubspot } = useSignUpHubspot()

return useMutation(mutations.createUserSignup, {
onSuccess(_, variables) {
queryClient.invalidateQueries({
queryKey: queries.usersSignUp.get.queryKey,
})

const utmParams = getUtmParams()
const trackingParams = getStoredTrackingParams()

const cargoPayload: CargoSignupPayload = {
const signupPayload: HubspotSignupPayload = {
email: variables.user_email,
first_name: variables.first_name,
last_name: variables.last_name,
company: variables.company_name || '',
job_title: variables.user_role || '',
phone: variables.phone ?? '',
signup_source: 'Console',
...utmParams,
qovery_interest: variables.qovery_usage ?? 'Not specified',
which_cloud_service_provider_do_you_use_: variables.infrastructure_hosting ?? 'Not specified',
tracking_params: trackingParams,
}
signUpCargo(cargoPayload)
signUpHubspot(signupPayload)
},
meta: {
notifyOnError: true,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useMutation } from '@tanstack/react-query'

const HUBSPOT_PORTAL_ID = '25346960'
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 {
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
/** All URL/tracking params captured at landing (utm_*, gclid, etc.) — sent as HubSpot fields */
tracking_params?: Record<string, string>
}

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',
},
]
const tracking = payload.tracking_params ?? {}
for (const [name, value] of Object.entries(tracking)) {
if (name && value != null && value !== '') {
fields.push({ name, value })
}
}
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
8 changes: 0 additions & 8 deletions libs/pages/onboarding/src/lib/feature/container/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,6 @@ export function Container(props: PropsWithChildren<ContainerProps>) {
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
Expand Down
66 changes: 38 additions & 28 deletions libs/shared/util-hooks/src/lib/use-utm-params/use-utm-params.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,56 @@
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
export const TRACKING_PARAMS_STORAGE_KEY = 'tracking_params'

/** Returns all URL search params as a record (dynamic keys only). */
export function getTrackingParams(searchParams: URLSearchParams): Record<string, string> {
const result: Record<string, string> = {}
searchParams.forEach((value, key) => {
if (value) result[key] = value
})
return result
}

const UTM_KEYS: (keyof UtmParams)[] = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid']

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
/** Returns all URL params stored at landing. */
export function getStoredTrackingParams(): Record<string, string> {
try {
const raw = localStorage.getItem(TRACKING_PARAMS_STORAGE_KEY)
if (raw) {
const parsed = JSON.parse(raw) as Record<string, unknown>
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
}

return utmParams
return {}
}

export function useUtmParams(): UtmParams {
export function useTrackingParams(): Record<string, string> {
const [searchParams] = useSearchParams()

const utmParams = useMemo(() => getUtmParams(searchParams), [searchParams])

return utmParams
return useMemo(() => getTrackingParams(searchParams), [searchParams])
}

// 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)
UTM_KEYS.forEach((key) => {
const value = params.get(key)
if (value) {
localStorage.setItem(key, value)
}
const fromUrl: Record<string, string> = {}
params.forEach((value, key) => {
if (value && !OAUTH_PARAMS.includes(key)) fromUrl[key] = value
})
const existing = getStoredTrackingParams()
const merged = { ...existing, ...fromUrl }
if (Object.keys(merged).length > 0) {
localStorage.setItem(TRACKING_PARAMS_STORAGE_KEY, JSON.stringify(merged))
}
}, [])
}