Skip to content

Commit 5afd2dc

Browse files
github-actions[bot]ItsnotakaMarfuen
authored
feat(api): update dependencies and refactor email service imports (#1782)
Co-authored-by: Daniel Fu <itsnotaka@gmail.com> Co-authored-by: Mariano Fuentes <marfuen98@gmail.com>
1 parent a73872e commit 5afd2dc

File tree

13 files changed

+602
-135
lines changed

13 files changed

+602
-135
lines changed

apps/api/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
"@nestjs/platform-express": "^11.1.5",
1313
"@nestjs/swagger": "^11.2.0",
1414
"@prisma/client": "^6.13.0",
15-
"@trycompai/db": "^1.3.17",
16-
"@trycompai/email": "workspace:*",
15+
"@trycompai/db": "workspace:*",
16+
"@react-email/components": "^0.0.41",
17+
"react": "^19.1.1",
18+
"react-dom": "^19.1.0",
1719
"archiver": "^7.0.1",
1820
"axios": "^1.12.2",
1921
"better-auth": "^1.3.27",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Hr, Link, Section, Text } from '@react-email/components';
2+
3+
export function Footer() {
4+
return (
5+
<Section className="w-full">
6+
<Hr />
7+
8+
<Text className="font-regular text-[14px]">
9+
AI that handles compliance for you -{' '}
10+
<Link href="https://trycomp.ai?utm_source=email&utm_medium=footer">Comp AI</Link>.
11+
</Text>
12+
13+
<Text className="text-xs text-[#B8B8B8]">
14+
Comp AI | 2261 Market Street, San Francisco, CA 94114
15+
</Text>
16+
</Section>
17+
);
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Img, Section } from '@react-email/components';
2+
3+
export function Logo() {
4+
return (
5+
<Section className="mt-[32px]">
6+
<Img
7+
src={'https://assets.trycomp.ai/logo.png'}
8+
width="45"
9+
height="45"
10+
alt="Comp AI"
11+
className="mx-auto my-0 block"
12+
/>
13+
</Section>
14+
);
15+
}

apps/api/src/email/resend.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Resend } from 'resend';
2+
import * as React from 'react';
3+
4+
export const resend = process.env.RESEND_API_KEY
5+
? new Resend(process.env.RESEND_API_KEY)
6+
: null;
7+
8+
export const sendEmail = async ({
9+
to,
10+
subject,
11+
react,
12+
marketing,
13+
system,
14+
test,
15+
cc,
16+
scheduledAt,
17+
}: {
18+
to: string;
19+
subject: string;
20+
react: React.ReactNode;
21+
marketing?: boolean;
22+
system?: boolean;
23+
test?: boolean;
24+
cc?: string | string[];
25+
scheduledAt?: string;
26+
}) => {
27+
if (!resend) {
28+
throw new Error('Resend not initialized - missing API key');
29+
}
30+
31+
// 1) Pull each env var into its own constant
32+
const fromMarketing = process.env.RESEND_FROM_MARKETING;
33+
const fromSystem = process.env.RESEND_FROM_SYSTEM;
34+
const fromDefault = process.env.RESEND_FROM_DEFAULT;
35+
const toTest = process.env.RESEND_TO_TEST;
36+
const replyMarketing = process.env.RESEND_REPLY_TO_MARKETING;
37+
38+
// 2) Decide which one you need for this email
39+
const fromAddress = marketing
40+
? fromMarketing
41+
: system
42+
? fromSystem
43+
: fromDefault;
44+
45+
const toAddress = test ? toTest : to;
46+
47+
const replyTo = marketing ? replyMarketing : undefined;
48+
49+
// 3) Guard against undefined
50+
if (!fromAddress) {
51+
throw new Error('Missing FROM address in environment variables');
52+
}
53+
if (!toAddress) {
54+
throw new Error('Missing TO address in environment variables');
55+
}
56+
57+
try {
58+
const { data, error } = await resend.emails.send({
59+
from: fromAddress, // now always a string
60+
to: toAddress, // now always a string
61+
cc,
62+
replyTo,
63+
subject,
64+
// @ts-ignore – React node allowed by the SDK
65+
react,
66+
scheduledAt,
67+
});
68+
69+
if (error) {
70+
console.error('Resend API error:', error);
71+
throw new Error(`Failed to send email: ${error.message}`);
72+
}
73+
74+
return {
75+
message: 'Email sent successfully',
76+
id: data?.id,
77+
};
78+
} catch (error) {
79+
console.error('Email sending error:', error);
80+
throw error instanceof Error ? error : new Error('Failed to send email');
81+
}
82+
};
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {
2+
Body,
3+
Button,
4+
Container,
5+
Font,
6+
Heading,
7+
Html,
8+
Preview,
9+
Section,
10+
Tailwind,
11+
Text,
12+
} from '@react-email/components';
13+
import { Footer } from '../components/footer';
14+
import { Logo } from '../components/logo';
15+
16+
interface Props {
17+
toName: string;
18+
organizationName: string;
19+
expiresAt: Date;
20+
portalUrl?: string | null;
21+
}
22+
23+
export const AccessGrantedEmail = ({
24+
toName,
25+
organizationName,
26+
expiresAt,
27+
portalUrl,
28+
}: Props) => {
29+
return (
30+
<Html>
31+
<Tailwind>
32+
<head>
33+
<Font
34+
fontFamily="Geist"
35+
fallbackFontFamily="Helvetica"
36+
fontWeight={400}
37+
fontStyle="normal"
38+
/>
39+
<Font
40+
fontFamily="Geist"
41+
fallbackFontFamily="Helvetica"
42+
fontWeight={500}
43+
fontStyle="normal"
44+
/>
45+
</head>
46+
<Preview>Access Granted - {organizationName}</Preview>
47+
48+
<Body className="mx-auto my-auto bg-[#fff] font-sans">
49+
<Container
50+
className="mx-auto my-[40px] max-w-[600px] border-transparent p-[20px] md:border-[#E8E7E1]"
51+
style={{ borderStyle: 'solid', borderWidth: 1 }}
52+
>
53+
<Logo />
54+
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-[#121212]">
55+
Access Granted ✓
56+
</Heading>
57+
58+
<Text className="text-[14px] leading-[24px] text-[#121212]">
59+
Hello {toName},
60+
</Text>
61+
62+
<Text className="text-[14px] leading-[24px] text-[#121212]">
63+
Your NDA has been signed and your access to{' '}
64+
<strong>{organizationName}</strong>'s policy documentation is now
65+
active.
66+
</Text>
67+
68+
<Text className="text-[14px] leading-[24px] text-[#121212]">
69+
Your access will expire on:{' '}
70+
<strong>
71+
{expiresAt.toLocaleDateString('en-US', {
72+
year: 'numeric',
73+
month: 'long',
74+
day: 'numeric',
75+
})}
76+
</strong>
77+
</Text>
78+
79+
{portalUrl && (
80+
<Section className="mt-[32px] mb-[32px] text-center">
81+
<Button
82+
className="rounded-[3px] bg-[#121212] px-[20px] py-[12px] text-center text-[14px] font-semibold text-white no-underline"
83+
href={portalUrl}
84+
>
85+
View Documents
86+
</Button>
87+
</Section>
88+
)}
89+
90+
<Text className="text-[14px] leading-[24px] text-[#121212]">
91+
You can download your signed NDA for your records from the
92+
confirmation page or by accessing the portal above.
93+
</Text>
94+
95+
<Section
96+
className="mt-[30px] mb-[20px] rounded-[3px] border-l-4 p-[15px]"
97+
style={{ backgroundColor: '#f8f9fa', borderColor: '#121212' }}
98+
>
99+
<Text className="m-0 text-[14px] leading-[24px] text-[#121212]">
100+
<strong>Lost your access link?</strong>
101+
<br />
102+
Visit the trust portal and click "Already have access?" to
103+
receive a new access link via email.
104+
</Text>
105+
</Section>
106+
107+
<br />
108+
109+
<Footer />
110+
</Container>
111+
</Body>
112+
</Tailwind>
113+
</Html>
114+
);
115+
};
116+
117+
export default AccessGrantedEmail;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
Body,
3+
Button,
4+
Container,
5+
Font,
6+
Heading,
7+
Html,
8+
Link,
9+
Preview,
10+
Section,
11+
Tailwind,
12+
Text,
13+
} from '@react-email/components';
14+
import { Footer } from '../components/footer';
15+
import { Logo } from '../components/logo';
16+
17+
interface Props {
18+
toName: string;
19+
organizationName: string;
20+
accessLink: string;
21+
expiresAt: Date;
22+
}
23+
24+
export const AccessReclaimEmail = ({
25+
toName,
26+
organizationName,
27+
accessLink,
28+
expiresAt,
29+
}: Props) => {
30+
return (
31+
<Html>
32+
<Tailwind>
33+
<head>
34+
<Font
35+
fontFamily="Geist"
36+
fallbackFontFamily="Helvetica"
37+
fontWeight={400}
38+
fontStyle="normal"
39+
/>
40+
<Font
41+
fontFamily="Geist"
42+
fallbackFontFamily="Helvetica"
43+
fontWeight={500}
44+
fontStyle="normal"
45+
/>
46+
</head>
47+
<Preview>Access Your Compliance Data - {organizationName}</Preview>
48+
49+
<Body className="mx-auto my-auto bg-[#fff] font-sans">
50+
<Container
51+
className="mx-auto my-[40px] max-w-[600px] border-transparent p-[20px] md:border-[#E8E7E1]"
52+
style={{ borderStyle: 'solid', borderWidth: 1 }}
53+
>
54+
<Logo />
55+
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-[#121212]">
56+
Access Your Data
57+
</Heading>
58+
59+
<Text className="text-[14px] leading-[24px] text-[#121212]">
60+
Hello {toName},
61+
</Text>
62+
63+
<Text className="text-[14px] leading-[24px] text-[#121212]">
64+
You requested access to <strong>{organizationName}</strong>'s
65+
compliance documentation.
66+
</Text>
67+
68+
<Text className="text-[14px] leading-[24px] text-[#121212]">
69+
Click the button below to access your data:
70+
</Text>
71+
72+
<Section className="mt-[32px] mb-[42px] text-center">
73+
<Button
74+
className="text-primary border border-solid border-[#121212] bg-transparent px-6 py-3 text-center text-[14px] font-medium text-[#121212] no-underline"
75+
href={accessLink}
76+
>
77+
Access Compliance Data
78+
</Button>
79+
</Section>
80+
81+
<Text className="text-[14px] leading-[24px] break-all text-[#707070]">
82+
or copy and paste this URL into your browser{' '}
83+
<Link href={accessLink} className="text-[#707070] underline">
84+
{accessLink}
85+
</Link>
86+
</Text>
87+
88+
<br />
89+
<Section>
90+
<Text className="text-[12px] leading-[24px] text-[#666666]">
91+
This link will expire in 24 hours. Your grant expires on:{' '}
92+
<strong>
93+
{expiresAt.toLocaleDateString('en-US', {
94+
year: 'numeric',
95+
month: 'long',
96+
day: 'numeric',
97+
})}
98+
</strong>
99+
</Text>
100+
</Section>
101+
102+
<br />
103+
104+
<Footer />
105+
</Container>
106+
</Body>
107+
</Tailwind>
108+
</Html>
109+
);
110+
};
111+
112+
export default AccessReclaimEmail;

0 commit comments

Comments
 (0)