Thanks to visit codestin.com
Credit goes to github.com

Skip to content
8 changes: 7 additions & 1 deletion apps/web/app/(ee)/api/hubspot/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { DubApiError, handleAndReturnErrorResponse } from "@/lib/api/errors";
import { HUBSPOT_CLIENT_SECRET } from "@/lib/integrations/hubspot/constants";
import { refreshAccessToken } from "@/lib/integrations/hubspot/refresh-token";
import { hubSpotWebhookSchema } from "@/lib/integrations/hubspot/schema";
import {
hubSpotSettingsSchema,
hubSpotWebhookSchema,
} from "@/lib/integrations/hubspot/schema";
import { trackHubSpotLeadEvent } from "@/lib/integrations/hubspot/track-lead";
import { trackHubSpotSaleEvent } from "@/lib/integrations/hubspot/track-sale";
import { HubSpotAuthToken } from "@/lib/integrations/hubspot/types";
Expand Down Expand Up @@ -122,10 +125,13 @@ async function processWebhookEvent(event: any) {

// Track the sale event when deal is closed won
if (subscriptionType === "object.propertyChange") {
const settings = hubSpotSettingsSchema.parse(installation.settings ?? {});

await trackHubSpotSaleEvent({
payload: event,
workspace,
authToken,
closedWonDealStageId: settings?.closedWonDealStageId,
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { getIntegrationInstallUrl } from "@/lib/actions/get-integration-install-url";
import { HubSpotSettings } from "@/lib/integrations/hubspot/ui/settings";
import { SegmentSettings } from "@/lib/integrations/segment/ui/settings";
import { SlackSettings } from "@/lib/integrations/slack/ui/settings";
import { ZapierSettings } from "@/lib/integrations/zapier/ui/settings";
Expand Down Expand Up @@ -56,6 +57,7 @@ const integrationSettings = {
[ZAPIER_INTEGRATION_ID]: ZapierSettings,
[SLACK_INTEGRATION_ID]: SlackSettings,
[SEGMENT_INTEGRATION_ID]: SegmentSettings,
[HUBSPOT_INTEGRATION_ID]: HubSpotSettings,
};

export default function IntegrationPageClient({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export default async function IntegrationPage({
? integration.installations[0]?.credentials
: undefined;

const settings = installed
? integration.installations[0]?.settings
: undefined;

// TODO:
// Fix this, we only displaying the first webhook only
const webhookId = installed
Expand All @@ -82,6 +86,7 @@ export default async function IntegrationPage({
}
: null,
credentials,
settings,
webhookId,
}}
/>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/lib/integrations/hubspot/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export const HUBSPOT_OBJECT_TYPE_IDS = [
"0-1", // contact
"0-3", // deal
] as const;

export const DEFAULT_CLOSED_WON_DEAL_STAGE_ID = "closedwon";
2 changes: 1 addition & 1 deletion apps/web/lib/integrations/hubspot/get-contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function getHubSpotContact({
}) {
try {
const response = await fetch(
`${HUBSPOT_API_HOST}/crm/v3/objects/contacts/${contactId}?properties=email,firstname,lastname,dub_id`,
`${HUBSPOT_API_HOST}/crm/v3/objects/contacts/${contactId}?properties=email,firstname,lastname,dub_id,dub_link,dub_partner_email`,
{
method: "GET",
headers: {
Expand Down
12 changes: 11 additions & 1 deletion apps/web/lib/integrations/hubspot/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export const hubSpotAuthTokenSchema = z.object({
created_at: z.number(),
});

// Integration settings
export const hubSpotSettingsSchema = z.object({
closedWonDealStageId: z
.string()
.nullish()
.describe("The ID of the deal stage that represents a closed won deal."),
});

export const hubSpotRefreshTokenSchema = z.object({
access_token: z.string(),
refresh_token: z.string(),
Expand All @@ -26,7 +34,9 @@ export const hubSpotContactSchema = z.object({
email: z.string(),
firstname: z.string().nullable(),
lastname: z.string().nullable(),
dub_id: z.string().nullable(),
dub_id: z.string().nullish(),
dub_link: z.string().nullish(),
dub_partner_email: z.string().nullish(),
}),
});

Expand Down
95 changes: 86 additions & 9 deletions apps/web/lib/integrations/hubspot/track-lead.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { trackLead } from "@/lib/api/conversions/track-lead";
import { WorkspaceProps } from "@/lib/types";
import { TrackLeadResponse, WorkspaceProps } from "@/lib/types";
import { prisma } from "@dub/prisma";
import { waitUntil } from "@vercel/functions";
import { getHubSpotContact } from "./get-contact";
import { getHubSpotDeal } from "./get-deal";
import { hubSpotLeadEventSchema } from "./schema";
import { HubSpotAuthToken } from "./types";
import { HubSpotAuthToken, HubSpotContact } from "./types";
import { updateHubSpotContact } from "./update-contact";

export const trackHubSpotLeadEvent = async ({
payload,
Expand All @@ -14,21 +17,20 @@ export const trackHubSpotLeadEvent = async ({
workspace: Pick<WorkspaceProps, "id" | "stripeConnectId" | "webhookEnabled">;
authToken: HubSpotAuthToken;
}) => {
const { objectId, objectTypeId, subscriptionType } =
hubSpotLeadEventSchema.parse(payload);
const { objectId, objectTypeId } = hubSpotLeadEventSchema.parse(payload);

// A new contact is created (deferred lead tracking)
if (objectTypeId === "0-1") {
const contact = await getHubSpotContact({
const contactInfo = await getHubSpotContact({
contactId: objectId,
accessToken: authToken.access_token,
});

if (!contact) {
if (!contactInfo) {
return;
}

const { properties } = contact;
const { properties } = contactInfo;

if (!properties.dub_id) {
console.error(`[HubSpot] No dub_id found for contact ${objectId}.`);
Expand All @@ -39,7 +41,7 @@ export const trackHubSpotLeadEvent = async ({
[properties.firstname, properties.lastname].filter(Boolean).join(" ") ||
null;

return await trackLead({
const trackLeadResult = await trackLead({
clickId: properties.dub_id,
eventName: "Sign up",
customerEmail: properties.email,
Expand All @@ -49,6 +51,18 @@ export const trackHubSpotLeadEvent = async ({
workspace,
rawBody: payload,
});

if (trackLeadResult) {
waitUntil(
_updateHubSpotContact({
contact: contactInfo,
trackLeadResult,
accessToken: authToken.access_token,
}),
);
}

return trackLeadResult;
}

// A deal is created for the contact (Eg: lead is tracked)
Expand Down Expand Up @@ -82,13 +96,76 @@ export const trackHubSpotLeadEvent = async ({
return;
}

return await trackLead({
const trackLeadResult = await trackLead({
clickId: "",
eventName: `Deal ${properties.dealstage}`,
customerExternalId: contactInfo.properties.email,
customerName: `${contactInfo.properties.firstname} ${contactInfo.properties.lastname}`,
customerEmail: contactInfo.properties.email,
mode: "async",
workspace,
rawBody: payload,
});

if (trackLeadResult) {
waitUntil(
_updateHubSpotContact({
contact: contactInfo,
trackLeadResult,
accessToken: authToken.access_token,
}),
);
}

return trackLeadResult;
}
};

// Update the HubSpot contact with `dub_link` and `dub_partner_email`
export const _updateHubSpotContact = async ({
accessToken,
contact,
trackLeadResult,
}: {
accessToken: string;
contact: HubSpotContact;
trackLeadResult: TrackLeadResponse;
}) => {
if (contact.properties.dub_link && contact.properties.dub_partner_email) {
console.log(
`[HubSpot] Contact ${contact.id} already has dub_link and dub_partner_email. Skipping update.`,
);
return;
}

const properties: Record<string, string> = {};

if (trackLeadResult.link?.partnerId) {
const partner = await prisma.partner.findUniqueOrThrow({
where: {
id: trackLeadResult.link.partnerId,
},
select: {
email: true,
},
});

if (partner.email) {
properties["dub_partner_email"] = partner.email;
}
}

if (trackLeadResult.link?.shortLink) {
properties["dub_link"] = trackLeadResult.link.shortLink;
}

if (Object.keys(properties).length === 0) {
return;
}

await updateHubSpotContact({
contactId: contact.id,
accessToken,
properties,
});
};
18 changes: 14 additions & 4 deletions apps/web/lib/integrations/hubspot/track-sale.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { trackSale } from "@/lib/api/conversions/track-sale";
import { WorkspaceProps } from "@/lib/types";
import { DEFAULT_CLOSED_WON_DEAL_STAGE_ID } from "./constants";
import { getHubSpotContact } from "./get-contact";
import { getHubSpotDeal } from "./get-deal";
import { hubSpotSaleEventSchema } from "./schema";
Expand All @@ -9,26 +10,35 @@ export const trackHubSpotSaleEvent = async ({
payload,
workspace,
authToken,
closedWonDealStageId,
}: {
payload: Record<string, any>;
workspace: Pick<WorkspaceProps, "id" | "stripeConnectId" | "webhookEnabled">;
authToken: HubSpotAuthToken;
closedWonDealStageId?: string | null;
}) => {
closedWonDealStageId =
closedWonDealStageId ?? DEFAULT_CLOSED_WON_DEAL_STAGE_ID;

const { objectId, subscriptionType, propertyName, propertyValue } =
hubSpotSaleEventSchema.parse(payload);

if (subscriptionType !== "object.propertyChange") {
console.error(`[HubSpot] Unknown subscriptionType ${subscriptionType}`);
console.log(`[HubSpot] Unknown subscriptionType ${subscriptionType}`);
return;
}

if (propertyName !== "dealstage") {
console.error(`[HubSpot] Unknown propertyName ${propertyName}`);
console.log(
`[HubSpot] Unknown propertyName ${propertyName}. Expected dealstage.`,
);
return;
}

if (propertyValue !== "closedwon") {
console.error(`[HubSpot] Unknown propertyValue ${propertyValue}`);
if (propertyValue !== closedWonDealStageId) {
console.error(
`[HubSpot] Unknown propertyValue ${propertyValue}. Expected ${closedWonDealStageId}.`,
);
return;
}

Expand Down
8 changes: 7 additions & 1 deletion apps/web/lib/integrations/hubspot/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { z } from "zod";
import { hubSpotAuthTokenSchema, hubSpotRefreshTokenSchema } from "./schema";
import {
hubSpotAuthTokenSchema,
hubSpotContactSchema,
hubSpotRefreshTokenSchema,
} from "./schema";

export type HubSpotAuthToken = z.infer<typeof hubSpotAuthTokenSchema>;

export type HubSpotRefreshToken = z.infer<typeof hubSpotRefreshTokenSchema>;

export type HubSpotContact = z.infer<typeof hubSpotContactSchema>;
Loading