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
3 changes: 3 additions & 0 deletions .cspell.json
Expand Up @@ -604,6 +604,9 @@
"Pyroscope",
"PYROSCOPE",
"usecases",
"hbspt",
"prepopulating",
"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
@@ -1,4 +1,4 @@
import { IsDefined, IsEnum, IsOptional, IsString } from 'class-validator';
import { IsDefined, IsOptional, IsString } from 'class-validator';

import { JobTitleEnum, ProductUseCases } from '@novu/shared';

Expand All @@ -14,7 +14,7 @@ export class CreateOrganizationCommand extends AuthenticatedCommand {
public readonly logo?: string;

@IsOptional()
@IsEnum(JobTitleEnum)
@IsString()
jobTitle?: JobTitleEnum;
jainpawan21 marked this conversation as resolved.
Show resolved Hide resolved

@IsString()
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/config/index.ts
Expand Up @@ -14,6 +14,7 @@ import {
MAIL_SERVER_DOMAIN,
LAUNCH_DARKLY_CLIENT_SIDE_ID,
FEATURE_FLAGS,
HUBSPOT_PORTAL_ID,
jainpawan21 marked this conversation as resolved.
Show resolved Hide resolved
} from '@novu/shared-web';

export {
Expand All @@ -32,6 +33,7 @@ export {
MAIL_SERVER_DOMAIN,
LAUNCH_DARKLY_CLIENT_SIDE_ID,
FEATURE_FLAGS,
HUBSPOT_PORTAL_ID,
};

export const IS_EU_ENV = (ENV === 'production' || ENV === 'prod') && API_ROOT.includes('eu.api.novu.co');
3 changes: 3 additions & 0 deletions apps/web/src/constants/hubspotForms.ts
@@ -0,0 +1,3 @@
export const HUBSPOT_FORM_IDS = {
SIGN_UP: 'ae2194a3-2b9d-4625-9c64-454187c42e8b',
};
6 changes: 4 additions & 2 deletions apps/web/src/pages/auth/QuestionnairePage.tsx
Expand Up @@ -3,6 +3,8 @@ import AuthContainer from '../../components/layout/components/AuthContainer';
import { QuestionnaireForm } from './components/QuestionnaireForm';
import { useVercelIntegration } from '../../hooks';
import SetupLoader from './components/SetupLoader';
import { IS_DOCKER_HOSTED } from '@novu/shared-web';
import { HubspotSignupForm } from './components/HubspotSignupForm';

export default function QuestionnairePage() {
const { isLoading } = useVercelIntegration();
Expand All @@ -14,9 +16,9 @@ export default function QuestionnairePage() {
) : (
<AuthContainer
title="Customize your experience"
description="Your answers can decrease the time to get started"
description={IS_DOCKER_HOSTED ? 'Your answers can decrease the time to get started' : ''}
>
<QuestionnaireForm />
{IS_DOCKER_HOSTED ? <QuestionnaireForm /> : <HubspotSignupForm />}
jainpawan21 marked this conversation as resolved.
Show resolved Hide resolved
</AuthContainer>
)}
</AuthLayout>
Expand Down
134 changes: 134 additions & 0 deletions apps/web/src/pages/auth/components/HubspotSignupForm.tsx
@@ -0,0 +1,134 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation } from '@tanstack/react-query';
import decode from 'jwt-decode';

import { JobTitleEnum } from '@novu/shared';
import type { ProductUseCases, IResponseError, ICreateOrganizationDto, IJwtPayload } from '@novu/shared';

import { api } from '../../../api/api.client';
import { useAuthContext } from '../../../components/providers/AuthProvider';
import { useVercelIntegration, useVercelParams } from '../../../hooks';
import { ROUTES } from '../../../constants/routes.enum';
import { HubspotForm } from '@novu/shared-web';
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 { 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 onCreateOrganization = async (data: IOrganizationCreateForm) => {
jainpawan21 marked this conversation as resolved.
Show resolved Hide resolved
if (!data?.organizationName) return;

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;
};

onCreateOrganization({
organizationName: submissionValues?.company,
jobTitle: hubspotRoleToJobTitleMapping[submissionValues?.role___onboarding],
});
}}
colorScheme="dark"
/>
);
}
}

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