Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add hubspot component in shared web #5286

Merged
merged 11 commits into from Mar 13, 2024
4 changes: 4 additions & 0 deletions .cspell.json
Expand Up @@ -604,6 +604,10 @@
"Pyroscope",
"PYROSCOPE",
"usecases",
"hbspt",
"prepopulating",
"Vonage",
"fieldtype",
"usecase",
"zulip"
],
Expand Down
8 changes: 1 addition & 7 deletions 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()
Expand Down
@@ -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()
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/constants/hubspotForms.ts
@@ -0,0 +1,3 @@
export const HUBSPOT_FORM_IDS = {
SIGN_UP: 'c551a45f-a9fe-47eb-a2e5-4e8540d27695',
};
7 changes: 5 additions & 2 deletions apps/web/src/pages/auth/QuestionnairePage.tsx
Expand Up @@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one, I think this is a suitable approach to dealing with local development, it will ensure we serve the Questionnaire form that does not require an internet connection in local.

For the no internet case in Prod, the User would not be able to reach the Novu API to complete the Organization creation anyway, so no further action needed on this path.


return (
<AuthLayout>
Expand All @@ -14,9 +17,9 @@ export default function QuestionnairePage() {
) : (
<AuthContainer
title="Customize your experience"
description="Your answers can decrease the time to get started"
description={!isNovuProd ? 'Your answers can decrease the time to get started' : ''}
>
<QuestionnaireForm />
{!isNovuProd ? <QuestionnaireForm /> : <HubspotSignupForm />}
</AuthContainer>
)}
</AuthLayout>
Expand Down
140 changes: 140 additions & 0 deletions 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() {
jainpawan21 marked this conversation as resolved.
Show resolved Hide resolved
const [loading, setLoading] = useState<boolean>();
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<IJwtPayload>(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<IJwtPayload>(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 <SetupLoader title="Loading..." />;
} else {
return (
<HubspotForm
formId={HUBSPOT_FORM_IDS.SIGN_UP}
properties={{
firstname: currentUser?.firstName as string,
lastname: currentUser?.lastName as string,
email: currentUser?.email as string,

company: '',
role___onboarding: '',
heard_about_novu: '',
use_case___onboarding: '',
role___onboarding__other_: '',
heard_about_novu__other_: '',
}}
readonlyProperties={['email', 'firstname', 'lastname']}
focussedProperty="company"
onFormSubmitted={($form, values) => {
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<string, JobTitleEnum> = {
'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,
};
1 change: 1 addition & 0 deletions libs/shared-web/package.json
Expand Up @@ -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",
Expand Down