Skip to content

Commit 1d7731a

Browse files
authored
Merge pull request #1107 from trycompai/main
[comp] Production Deploy
2 parents 69580c0 + 42f3317 commit 1d7731a

31 files changed

Lines changed: 2604 additions & 792 deletions

File tree

apps/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"use-debounce": "^10.0.4",
8282
"use-long-press": "^3.3.0",
8383
"xml2js": "^0.6.2",
84+
"zaraz-ts": "^1.2.0",
8485
"zustand": "^5.0.3"
8586
},
8687
"devDependencies": {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use server';
2+
3+
import { db } from '@comp/db';
4+
5+
/**
6+
* Fetch framework names by IDs and convert them to lowercase with no spaces
7+
* @param frameworkIds - Array of framework IDs
8+
* @returns Array of framework names in lowercase with no spaces
9+
*/
10+
export async function getFrameworkNames(frameworkIds: string[]): Promise<string[]> {
11+
if (!frameworkIds || frameworkIds.length === 0) {
12+
return [];
13+
}
14+
15+
const frameworks = await db.frameworkEditorFramework.findMany({
16+
where: {
17+
id: { in: frameworkIds },
18+
},
19+
select: {
20+
name: true,
21+
},
22+
});
23+
24+
return frameworks
25+
.map((framework) => {
26+
// Convert framework name to lowercase with no spaces
27+
// e.g., "SOC 2" -> "soc2", "ISO 27001" -> "iso27001", "GDPR" -> "gdpr"
28+
return framework.name
29+
.toLowerCase()
30+
.replace(/\s+/g, '') // Remove all spaces
31+
.replace(/[^a-z0-9]/g, ''); // Remove special characters
32+
})
33+
.filter(Boolean); // Remove any empty strings
34+
}

apps/app/src/app/(app)/[orgId]/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export default async function Layout({
112112
</div>
113113
<AssistantSheet />
114114
<Suspense fallback={null}>
115-
<CheckoutCompleteDialog />
115+
<CheckoutCompleteDialog orgId={organization.id} />
116116
</Suspense>
117117
</AnimatedLayout>
118118
<HotKeys />

apps/app/src/app/(app)/onboarding/[orgId]/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default async function OnboardingRouteLayout({
4444
return (
4545
<OnboardingLayout variant="onboarding" currentOrganization={organization}>
4646
{children}
47-
<CheckoutCompleteDialog />
47+
<CheckoutCompleteDialog orgId={organization.id} />
4848
</OnboardingLayout>
4949
);
5050
}

apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
'use server';
22

33
import { createStripeCustomer } from '@/actions/organization/lib/create-stripe-customer';
4+
import { getFrameworkNames } from '@/actions/organization/lib/get-framework-names';
45
import { initializeOrganization } from '@/actions/organization/lib/initialize-organization';
56
import { authActionClientWithoutOrg } from '@/actions/safe-action';
7+
import { isHubSpotConfigured } from '@/hubspot/api-client';
8+
import { createOrUpdateCompany, findCompanyByDomain } from '@/hubspot/companies';
9+
import { findContactByEmail } from '@/hubspot/contacts';
610
import { auth } from '@/utils/auth';
711
import { db } from '@comp/db';
812
import { revalidatePath } from 'next/cache';
@@ -63,6 +67,60 @@ export const createOrganizationMinimal = authActionClientWithoutOrg
6367

6468
const orgId = newOrg.id;
6569

70+
// Create HubSpot company if configured
71+
if (isHubSpotConfigured()) {
72+
try {
73+
console.log('[HubSpot] Creating company for organization:', orgId);
74+
75+
// Extract domain from website URL
76+
let domain = '';
77+
try {
78+
const url = new URL(parsedInput.website);
79+
domain = url.hostname.replace('www.', '');
80+
} catch (e) {
81+
console.warn('[HubSpot] Could not parse website URL:', parsedInput.website);
82+
}
83+
84+
// Check if company already exists
85+
let companyId: string | null = null;
86+
if (domain) {
87+
const existingCompany = await findCompanyByDomain(domain);
88+
companyId = existingCompany.companyId;
89+
}
90+
91+
// Get framework names in lowercase snake_case format
92+
const frameworkNames = await getFrameworkNames(parsedInput.frameworkIds);
93+
94+
// Check if contact exists for this user
95+
const contactResult = await findContactByEmail(session.user.email);
96+
97+
// Create or update company
98+
const hubspotCompanyId = await createOrUpdateCompany({
99+
companyName: parsedInput.organizationName,
100+
// Don't pass companySize - we don't have employee count in minimal setup
101+
complianceNeeds: frameworkNames,
102+
existingCompanyId: companyId || undefined,
103+
domain,
104+
orgId,
105+
});
106+
107+
if (hubspotCompanyId && contactResult.contactId) {
108+
// Associate contact with company
109+
console.log('[HubSpot] Company created/updated:', hubspotCompanyId);
110+
111+
const { associateContactWithCompany } = await import('@/hubspot/contacts');
112+
await associateContactWithCompany({
113+
contactId: contactResult.contactId,
114+
companyId: hubspotCompanyId,
115+
});
116+
console.log('[HubSpot] Associated contact with company');
117+
}
118+
} catch (error) {
119+
console.error('[HubSpot] Error creating company:', error);
120+
// Don't throw - we don't want to block organization creation if HubSpot fails
121+
}
122+
}
123+
66124
// Create onboarding record for new org
67125
await db.onboarding.create({
68126
data: {

apps/app/src/app/(app)/setup/actions/create-organization.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
'use server';
22

33
import { createStripeCustomer } from '@/actions/organization/lib/create-stripe-customer';
4+
import { getFrameworkNames } from '@/actions/organization/lib/get-framework-names';
45
import { initializeOrganization } from '@/actions/organization/lib/initialize-organization';
56
import { authActionClientWithoutOrg } from '@/actions/safe-action';
7+
import { isHubSpotConfigured } from '@/hubspot/api-client';
8+
import { createOrUpdateCompany, findCompanyByDomain } from '@/hubspot/companies';
9+
import { findContactByEmail } from '@/hubspot/contacts';
610
import { createFleetLabelForOrg } from '@/jobs/tasks/device/create-fleet-label-for-org';
711
import { onboardOrganization as onboardOrganizationTask } from '@/jobs/tasks/onboarding/onboard-organization';
812
import { auth } from '@/utils/auth';
@@ -64,6 +68,75 @@ export const createOrganization = authActionClientWithoutOrg
6468

6569
const orgId = newOrg.id;
6670

71+
// Create HubSpot company if configured
72+
if (isHubSpotConfigured()) {
73+
try {
74+
console.log('[HubSpot] Creating company for organization:', orgId);
75+
76+
// Extract domain from website URL
77+
let domain = '';
78+
try {
79+
const url = new URL(parsedInput.website);
80+
domain = url.hostname.replace('www.', '');
81+
} catch (e) {
82+
console.warn('[HubSpot] Could not parse website URL:', parsedInput.website);
83+
}
84+
85+
// Check if company already exists
86+
let companyId: string | null = null;
87+
if (domain) {
88+
const existingCompany = await findCompanyByDomain(domain);
89+
companyId = existingCompany.companyId;
90+
}
91+
92+
// Get framework names in lowercase snake_case format
93+
const frameworkNames = await getFrameworkNames(parsedInput.frameworkIds);
94+
95+
// Extract employee count from the context answers
96+
let employeeCount = 0;
97+
const employeeCountAnswer = parsedInput.teamSize;
98+
if (employeeCountAnswer) {
99+
// Parse employee count range (e.g., "10-50" -> 30)
100+
const match = employeeCountAnswer.match(/(\d+)-(\d+)/);
101+
if (match) {
102+
employeeCount = Math.floor((parseInt(match[1]) + parseInt(match[2])) / 2);
103+
} else if (employeeCountAnswer.includes('+')) {
104+
employeeCount = parseInt(employeeCountAnswer.replace('+', ''));
105+
} else {
106+
employeeCount = parseInt(employeeCountAnswer) || 0;
107+
}
108+
}
109+
110+
// Check if contact exists for this user
111+
const contactResult = await findContactByEmail(session.user.email);
112+
113+
// Create or update company
114+
const hubspotCompanyId = await createOrUpdateCompany({
115+
companyName: parsedInput.organizationName,
116+
...(employeeCount > 0 && { companySize: employeeCount }),
117+
complianceNeeds: frameworkNames,
118+
existingCompanyId: companyId || undefined,
119+
domain,
120+
orgId,
121+
});
122+
123+
if (hubspotCompanyId && contactResult.contactId) {
124+
// Associate contact with company
125+
console.log('[HubSpot] Company created/updated:', hubspotCompanyId);
126+
127+
const { associateContactWithCompany } = await import('@/hubspot/contacts');
128+
await associateContactWithCompany({
129+
contactId: contactResult.contactId,
130+
companyId: hubspotCompanyId,
131+
});
132+
console.log('[HubSpot] Associated contact with company');
133+
}
134+
} catch (error) {
135+
console.error('[HubSpot] Error creating company:', error);
136+
// Don't throw - we don't want to block organization creation if HubSpot fails
137+
}
138+
}
139+
67140
// Create onboarding record for new org
68141
await db.onboarding.create({
69142
data: {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use client';
2+
3+
import { SubscriptionType } from '@comp/db/types';
4+
import { Button } from '@comp/ui/button';
5+
import { Card } from '@comp/ui/card';
6+
import { ArrowRight } from 'lucide-react';
7+
import Link from 'next/link';
8+
9+
export function BookingStep({
10+
email,
11+
name,
12+
company,
13+
orgId,
14+
complianceFrameworks,
15+
planType,
16+
}: {
17+
email: string;
18+
name: string;
19+
company: string;
20+
orgId: string;
21+
complianceFrameworks: string[];
22+
planType: SubscriptionType;
23+
}) {
24+
const title =
25+
planType === 'FREE' || planType === 'STARTER'
26+
? 'Talk to us to upgrade'
27+
: `Let's get ${company} approved`;
28+
29+
const description =
30+
planType === 'FREE' || planType === 'STARTER'
31+
? 'A quick 20-minute call with our team to understand your compliance needs and upgrade your plan.'
32+
: `A quick 20-minute call with our team to understand your compliance needs and approve your organization for access.`;
33+
34+
const cta = planType === 'FREE' || planType === 'STARTER' ? 'Book a Call' : 'Book Your Demo';
35+
36+
return (
37+
<div className="flex justify-center w-full animate-in fade-in-50 duration-500">
38+
<Card className="w-full max-w-xl border border-gray-100 dark:border-gray-800 shadow-lg shadow-gray-200/30 dark:shadow-black/20 bg-card">
39+
<div className="p-8 space-y-8">
40+
{/* Header */}
41+
<div className="text-center space-y-3 mb-6">
42+
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
43+
<p className="text-muted-foreground text-base max-w-xl mx-auto">{description}</p>
44+
</div>
45+
46+
{/* CTA Button */}
47+
<div className="flex justify-center">
48+
<Link
49+
href={`https://trycomp.ai/demo?email=${email}&name=${name}&company=${company}&orgId=${orgId}&complianceFrameworks=${complianceFrameworks.join(',')}`}
50+
target="_blank"
51+
rel="noopener noreferrer"
52+
>
53+
<Button size="lg" className="min-w-[200px]">
54+
{cta} <ArrowRight className="w-4 h-4" />
55+
</Button>
56+
</Link>
57+
</div>
58+
59+
{/* Already spoke to us section */}
60+
<div className="border-gray-200 dark:border-gray-800">
61+
<p className="text-center text-sm text-muted-foreground">
62+
Already had a demo? Ask your point of contact to activate your account.
63+
</p>
64+
</div>
65+
</div>
66+
</Card>
67+
</div>
68+
);
69+
}

0 commit comments

Comments
 (0)