diff --git a/.cspell.json b/.cspell.json
index 8262ddcf8e..6ad454ad71 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -605,6 +605,10 @@
"Pyroscope",
"PYROSCOPE",
"usecases",
+ "hbspt",
+ "prepopulating",
+ "Vonage",
+ "fieldtype",
"usecase",
"zulip",
"uuidv",
diff --git a/apps/api/src/app/auth/dtos/user-registration.dto.ts b/apps/api/src/app/auth/dtos/user-registration.dto.ts
index 01fdbe93f1..03e7199541 100644
--- a/apps/api/src/app/auth/dtos/user-registration.dto.ts
+++ b/apps/api/src/app/auth/dtos/user-registration.dto.ts
@@ -1,11 +1,5 @@
import { IsDefined, IsEmail, IsOptional, MinLength, Matches, MaxLength, IsString, IsEnum } from 'class-validator';
-import {
- JobTitleEnum,
- passwordConstraints,
- ProductUseCases,
- ProductUseCasesEnum,
- SignUpOriginEnum,
-} from '@novu/shared';
+import { JobTitleEnum, passwordConstraints, ProductUseCases, SignUpOriginEnum } from '@novu/shared';
export class UserRegistrationBodyDto {
@IsDefined()
@IsEmail()
diff --git a/apps/api/src/app/organization/dtos/create-organization.dto.ts b/apps/api/src/app/organization/dtos/create-organization.dto.ts
index 8a11687f97..ef7a6ab559 100644
--- a/apps/api/src/app/organization/dtos/create-organization.dto.ts
+++ b/apps/api/src/app/organization/dtos/create-organization.dto.ts
@@ -1,5 +1,5 @@
import { IsDefined, IsEnum, IsOptional, IsString } from 'class-validator';
-import { ICreateOrganizationDto, JobTitleEnum, ProductUseCases, ProductUseCasesEnum } from '@novu/shared';
+import { ICreateOrganizationDto, JobTitleEnum, ProductUseCases } from '@novu/shared';
export class CreateOrganizationDto implements ICreateOrganizationDto {
@IsString()
diff --git a/apps/web/src/constants/hubspotForms.ts b/apps/web/src/constants/hubspotForms.ts
new file mode 100644
index 0000000000..e1cfcecc84
--- /dev/null
+++ b/apps/web/src/constants/hubspotForms.ts
@@ -0,0 +1,3 @@
+export const HUBSPOT_FORM_IDS = {
+ SIGN_UP: 'c551a45f-a9fe-47eb-a2e5-4e8540d27695',
+};
diff --git a/apps/web/src/pages/auth/QuestionnairePage.tsx b/apps/web/src/pages/auth/QuestionnairePage.tsx
index acbbd4c242..2afcf1ab85 100644
--- a/apps/web/src/pages/auth/QuestionnairePage.tsx
+++ b/apps/web/src/pages/auth/QuestionnairePage.tsx
@@ -3,9 +3,12 @@ import AuthContainer from '../../components/layout/components/AuthContainer';
import { QuestionnaireForm } from './components/QuestionnaireForm';
import { useVercelIntegration } from '../../hooks';
import SetupLoader from './components/SetupLoader';
+import { ENV, IS_DOCKER_HOSTED } from '@novu/shared-web';
+import { HubspotSignupForm } from './components/HubspotSignupForm';
export default function QuestionnairePage() {
const { isLoading } = useVercelIntegration();
+ const isNovuProd = !IS_DOCKER_HOSTED && ENV === 'production';
return (
@@ -14,9 +17,9 @@ export default function QuestionnairePage() {
) : (
-
+ {!isNovuProd ? : }
)}
diff --git a/apps/web/src/pages/auth/components/HubspotSignupForm.tsx b/apps/web/src/pages/auth/components/HubspotSignupForm.tsx
new file mode 100644
index 0000000000..fb9278f4b6
--- /dev/null
+++ b/apps/web/src/pages/auth/components/HubspotSignupForm.tsx
@@ -0,0 +1,140 @@
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useMutation } from '@tanstack/react-query';
+import decode from 'jwt-decode';
+import { useMantineColorScheme } from '@mantine/core';
+
+import { JobTitleEnum } from '@novu/shared';
+import type { ProductUseCases, IResponseError, ICreateOrganizationDto, IJwtPayload } from '@novu/shared';
+import { HubspotForm, useSegment } from '@novu/shared-web';
+
+import { api } from '../../../api/api.client';
+import { useAuthContext } from '../../../components/providers/AuthProvider';
+import { useVercelIntegration, useVercelParams } from '../../../hooks';
+import { ROUTES } from '../../../constants/routes.enum';
+import { HUBSPOT_FORM_IDS } from '../../../constants/hubspotForms';
+import SetupLoader from './SetupLoader';
+
+export function HubspotSignupForm() {
+ const [loading, setLoading] = useState();
+ const navigate = useNavigate();
+ const { setToken, token, currentUser } = useAuthContext();
+ const { startVercelSetup } = useVercelIntegration();
+ const { isFromVercel } = useVercelParams();
+ const { colorScheme } = useMantineColorScheme();
+
+ const segment = useSegment();
+
+ const { mutateAsync: createOrganizationMutation } = useMutation<
+ { _id: string },
+ IResponseError,
+ ICreateOrganizationDto
+ >((data: ICreateOrganizationDto) => api.post(`/v1/organizations`, data));
+
+ useEffect(() => {
+ if (token) {
+ const userData = decode(token);
+
+ if (userData.environmentId) {
+ if (isFromVercel) {
+ startVercelSetup();
+
+ return;
+ }
+
+ navigate(ROUTES.HOME);
+ }
+ }
+ }, [token, navigate, isFromVercel, startVercelSetup]);
+
+ async function createOrganization(data: IOrganizationCreateForm) {
+ const { organizationName, jobTitle, ...rest } = data;
+ const createDto: ICreateOrganizationDto = { ...rest, name: organizationName, jobTitle };
+ const organization = await createOrganizationMutation(createDto);
+ const organizationResponseToken = await api.post(`/v1/auth/organizations/${organization._id}/switch`, {});
+
+ setToken(organizationResponseToken);
+ }
+
+ function jwtHasKey(key: string) {
+ if (!token) return false;
+ const jwt = decode(token);
+
+ return jwt && jwt[key];
+ }
+
+ const handleCreateOrganization = async (data: IOrganizationCreateForm) => {
+ if (!data?.organizationName) return;
+
+ segment.track('Button Clicked - [Signup]', { action: 'hubspot questionnaire form submit' });
+
+ setLoading(true);
+
+ if (!jwtHasKey('organizationId')) {
+ await createOrganization({ ...data });
+ }
+
+ setLoading(false);
+ if (isFromVercel) {
+ startVercelSetup();
+
+ return;
+ }
+
+ navigate(ROUTES.GET_STARTED);
+ };
+
+ if (!currentUser || loading) {
+ return ;
+ } else {
+ return (
+ {
+ const submissionValues = values?.submissionValues as unknown as {
+ company: string;
+ role___onboarding: string;
+ };
+
+ handleCreateOrganization({
+ organizationName: submissionValues?.company,
+ jobTitle: hubspotRoleToJobTitleMapping[submissionValues?.role___onboarding],
+ });
+ }}
+ colorScheme={colorScheme}
+ />
+ );
+ }
+}
+
+interface IOrganizationCreateForm {
+ organizationName: string;
+ jobTitle: JobTitleEnum;
+ domain?: string;
+ productUseCases?: ProductUseCases;
+}
+
+const hubspotRoleToJobTitleMapping: Record = {
+ 'Engineer/developer': JobTitleEnum.ENGINEER,
+ Product: JobTitleEnum.PRODUCT_MANAGER,
+ Architect: JobTitleEnum.ARCHITECT,
+ 'Engineering Manager': JobTitleEnum.ENGINEERING_MANAGER,
+ Designer: JobTitleEnum.DESIGNER,
+ 'CxO/Founder': JobTitleEnum.FOUNDER,
+ Marketing: JobTitleEnum.MARKETING_MANAGER,
+ 'Other (specify)': JobTitleEnum.OTHER,
+};
diff --git a/libs/shared-web/package.json b/libs/shared-web/package.json
index f373d2dbc7..96e63f89c3 100644
--- a/libs/shared-web/package.json
+++ b/libs/shared-web/package.json
@@ -31,6 +31,7 @@
"@mantine/hooks": "^5.7.1",
"@novu/shared": "^0.24.0",
"@segment/analytics-next": "1.59.0",
+ "@emotion/styled": "^11.6.0",
"@sentry/react": "^7.40.0",
"@tanstack/react-query": "^4.20.4",
"axios": "^1.6.0",
diff --git a/libs/shared-web/src/components/HubspotForm.tsx b/libs/shared-web/src/components/HubspotForm.tsx
new file mode 100644
index 0000000000..0707ec97ed
--- /dev/null
+++ b/libs/shared-web/src/components/HubspotForm.tsx
@@ -0,0 +1,239 @@
+import { useEffect } from 'react';
+import styled from '@emotion/styled';
+import { HUBSPOT_PORTAL_ID } from '../config';
+
+// TODO: remove design system colors after fixing circular dependency from ee
+const colors = {
+ white: '#FFFFFF',
+ black: '#000000',
+ B80: '#BEBECC',
+ B40: '#525266',
+ B20: '#292933',
+ vertical: `linear-gradient(0deg, #FF512F 0%, #DD2476 100%)`,
+ horizontal: `linear-gradient(99deg, #DD2476 0% 0%, #FF512F 100% 100%)`,
+};
+
+declare global {
+ interface Window {
+ hbspt: any;
+ }
+}
+
+/**
+ * For the full list of available Hubspot Form options:
+ *
+ * @see https://developers.hubspot.com/docs/methods/forms/advanced_form_options
+ */
+export type HubspotFormProps<
+ TProperties extends Record,
+ TKey extends keyof TProperties & string = keyof TProperties & string
+> = {
+ /**
+ * The Hubspot form ID. This can be found in the Hubspot form embed snippet.
+ */
+ formId: string;
+ /**
+ * Properties for prepopulating fields. Keys must match the field names created in the Hubspot form.
+ */
+ properties?: TProperties;
+ /**
+ * Read-only properties for prepopulating fields. Keys must match the field names created in the Hubspot form.
+ */
+ readonlyProperties?: Array;
+ /**
+ * The name of the property to focus when the form is ready.
+ */
+ focussedProperty?: TKey;
+ /**
+ * Callback function to be called when the form is submitted.
+ */
+ onFormSubmitted?: ($form?: any, values?: Record) => void;
+ /**
+ * colorScheme
+ */
+ colorScheme: 'dark' | 'light';
+};
+
+const HUBSPOT_FORMS_URL = 'https://js.hsforms.net/forms/v2.js';
+const HUBSPOT_REGION = 'na1';
+
+const cssClass = 'hubspot-form-wrapper';
+const StyledHubspotForm = styled.div<{ isDark: boolean }>`
+ .${cssClass} {
+ color: ${({ isDark }) => (isDark ? colors.B80 : colors.B40)};
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ /** Column layout */
+ .form-columns-1,
+ .form-columns-2 {
+ min-width: 100%;
+ display: flex;
+ gap: 20px;
+
+ > * {
+ width: 100%;
+ }
+
+ .hs-input {
+ width: 100%;
+ }
+ }
+
+ /** Hyperlinks */
+ a {
+ background-image: ${colors.horizontal};
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ text-decoration: none;
+ }
+
+ .input {
+ input,
+ textarea,
+ select {
+ &:focus-visible {
+ outline: none;
+ border-color: ${({ isDark }) => (isDark ? colors.white : colors.black)};
+ }
+ }
+ }
+
+ /** Form fields */
+ .hs-input {
+ appearance: none;
+ background-color: transparent;
+ border-radius: 7px;
+ border: 1px solid ${({ isDark }) => (isDark ? colors.B20 : colors.B80)};
+ box-sizing: border-box;
+ display: block;
+ font-family: Lato, sans serif;
+ font-size: 14px;
+ height: 42px;
+ line-height: 40px;
+ margin: 5px 0px; /* Adjusted margin */
+ min-height: 50px;
+ padding-left: 14px;
+ padding-right: 14px;
+ resize: none;
+ text-align: left;
+ transition: border-color 100ms ease;
+ width: 100%;
+ }
+
+ /** Form text area */
+ .hs-fieldtype-textarea {
+ resize: vertical;
+ min-height: 100px;
+ }
+
+ /** Form button */
+ .hs-button {
+ appearance: none;
+ background-color: transparent;
+ background-image: ${colors.horizontal};
+ border-radius: 7px;
+ border: 0;
+ box-sizing: border-box;
+ cursor: pointer;
+ display: inline-block;
+ font-family: Lato, sans serif;
+ font-size: 14px;
+ font-weight: 600;
+ height: 42px;
+ line-height: 1;
+ padding-left: 22px;
+ padding-right: 22px;
+ position: relative;
+ color: #fff;
+ text-align: right;
+ text-decoration: none;
+ user-select: none;
+ width: auto;
+ }
+
+ /** Form field label */
+ .hs-form-field label {
+ cursor: default;
+ display: inline-block;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 17px;
+ margin: 5px 0px;
+ word-break: break-word;
+ }
+
+ /** Form Submit action alignment */
+ .hs-submit .actions {
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ /** Legal consent container */
+ .legal-consent-container {
+ font-size: 12px;
+ color: ${colors.B40};
+ line-height: 16px;
+
+ .p {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
+ }
+`;
+
+export const HubspotForm = >(props: HubspotFormProps) => {
+ const elementId = `hubspotForm-${props.formId}`;
+
+ const createForm = () => {
+ if (window.hbspt) {
+ window.hbspt.forms.create({
+ target: `#${elementId}`,
+ portalId: HUBSPOT_PORTAL_ID,
+ region: HUBSPOT_REGION,
+ cssClass,
+ onFormReady: (form) => {
+ if (props.focussedProperty) {
+ const selector = CSS.escape(`${props.focussedProperty}-${props.formId}`);
+ const input = form.querySelector(`#${selector}`) as HTMLInputElement;
+ if (input) {
+ input.focus();
+ }
+ }
+ if (props.readonlyProperties) {
+ props.readonlyProperties.forEach((property) => {
+ const selector = CSS.escape(`${property}-${props.formId}`);
+ const input = form.querySelector(`#${selector}`) as HTMLInputElement;
+ if (input) {
+ input.setAttribute('readonly', 'true');
+ }
+ });
+ }
+ },
+ ...props,
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (!window.hbspt) {
+ const script = document.createElement('script');
+ script.src = HUBSPOT_FORMS_URL;
+ document.body.appendChild(script);
+
+ script.addEventListener('load', () => {
+ createForm();
+ });
+ } else {
+ createForm();
+ }
+ }, [props]);
+
+ return (
+
+
+
+ );
+};
diff --git a/libs/shared-web/src/components/index.ts b/libs/shared-web/src/components/index.ts
new file mode 100644
index 0000000000..b88b589cb4
--- /dev/null
+++ b/libs/shared-web/src/components/index.ts
@@ -0,0 +1 @@
+export * from './HubspotForm';
diff --git a/libs/shared-web/src/config.ts b/libs/shared-web/src/config.ts
index f7a9691463..a8b77f3875 100644
--- a/libs/shared-web/src/config.ts
+++ b/libs/shared-web/src/config.ts
@@ -65,3 +65,5 @@ export const FEATURE_FLAGS = Object.values(FeatureFlagsKeysEnum).reduce((acc, ke
return acc;
}, {} as Record);
+
+export const HUBSPOT_PORTAL_ID = window._env_.REACT_APP_HUBSPOT_EMBED || process.env.REACT_APP_HUBSPOT_EMBED;
diff --git a/libs/shared-web/src/index.ts b/libs/shared-web/src/index.ts
index d1df7e9f46..626bbdcc37 100644
--- a/libs/shared-web/src/index.ts
+++ b/libs/shared-web/src/index.ts
@@ -3,3 +3,4 @@ export * from './api';
export * from './hooks';
export * from './providers';
export * from './constants';
+export * from './components';
diff --git a/libs/shared/src/types/organization/index.ts b/libs/shared/src/types/organization/index.ts
index 7c49ccf5c8..7ee58494dc 100644
--- a/libs/shared/src/types/organization/index.ts
+++ b/libs/shared/src/types/organization/index.ts
@@ -22,6 +22,8 @@ export enum JobTitleEnum {
ARCHITECT = 'architect',
PRODUCT_MANAGER = 'product_manager',
DESIGNER = 'designer',
+ FOUNDER = 'cxo_founder',
+ MARKETING_MANAGER = 'marketing_manager',
OTHER = 'other',
}
@@ -31,5 +33,7 @@ export const jobTitleToLabelMapper = {
[JobTitleEnum.PRODUCT_MANAGER]: 'Product Manager',
[JobTitleEnum.DESIGNER]: 'Designer',
[JobTitleEnum.ENGINEERING_MANAGER]: 'Engineering Manager',
+ [JobTitleEnum.FOUNDER]: 'CXO Founder',
+ [JobTitleEnum.MARKETING_MANAGER]: 'Marketing Manager',
[JobTitleEnum.OTHER]: 'Other',
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4bf77feb36..d1a10ce56f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2740,6 +2740,9 @@ importers:
libs/shared-web:
dependencies:
+ '@emotion/styled':
+ specifier: ^11.6.0
+ version: 11.10.6(@emotion/react@11.10.6)(@types/react@17.0.62)(react@17.0.2)
'@mantine/hooks':
specifier: ^5.7.1
version: 5.10.5(react@17.0.2)
@@ -16225,7 +16228,7 @@ packages:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.21.0
+ '@babel/runtime': 7.23.2
'@emotion/babel-plugin': 11.10.6
'@emotion/is-prop-valid': 1.2.0
'@emotion/react': 11.10.6(@types/react@17.0.62)(react@17.0.2)
@@ -53886,7 +53889,7 @@ packages:
jest-worker: 26.6.2
rollup: 2.79.1
serialize-javascript: 4.0.0
- terser: 5.22.0
+ terser: 5.16.9
dev: true
/rollup-plugin-terser@7.0.2(rollup@3.20.2):