Skip to content

Commit 2b4d966

Browse files
author
joeldevelops
committed
Update emails to profesional templates
1 parent aba13d2 commit 2b4d966

File tree

9 files changed

+640
-88
lines changed

9 files changed

+640
-88
lines changed

packages/core/deno.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
"tasks": {
55
"dev": "deno run --env-file=../../.env -A --watch src/index.ts",
66
"migrate": "deno run -A src/db/scripts/migrate.ts",
7-
"build": "deno compile --allow-all --include src/db/migrations --output ./dist/core ./src/index.ts"
7+
"build": "deno compile --allow-all --include src/db/migrations --output ./dist/core ./src/index.ts",
8+
"email": "email dev -d src/email/templates"
89
},
910
"imports": {
1011
"@oak/oak": "jsr:@oak/oak@^17.1.4",
12+
"@react-email/components": "npm:@react-email/components@^0.1.0",
13+
"@react-email/render": "npm:@react-email/render@^1.1.2",
1114
"@sentry/deno": "npm:@sentry/deno@^9.28.1",
1215
"@std/assert": "jsr:@std/assert@1",
1316
"@std/expect": "jsr:@std/expect@^1.0.16",
@@ -18,6 +21,9 @@
1821
"pg": "npm:pg@^8.16.0",
1922
"pg-pool": "npm:pg-pool@^3.10.0",
2023
"djwt": "https://deno.land/x/[email protected]/mod.ts",
24+
"react": "npm:react@^19.1.0",
25+
"react-dom": "npm:react-dom@^19.1.0",
26+
"react-email": "npm:react-email@^4.0.16",
2127
"resend": "npm:resend@^4.5.2",
2228
"stripe": "npm:stripe@^18.2.1",
2329
"zod": "npm:zod@^3.24.4"

packages/core/src/email/index.ts

Lines changed: 62 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1+
import type { ReactNode } from "react";
12
import type {
23
StripeBillingCycle,
34
StripeProduct,
45
} from "../db/models/workspace.ts";
56
import { Resend } from "resend";
7+
import { render } from "@react-email/render";
68
import settings from "../settings.ts";
79

10+
import OtpEmail from "./templates/OtpEmail.tsx";
11+
import WelcomeEmail from "./templates/WelcomeEmail.tsx";
12+
import InvitationEmail from "./templates/InvitationEmail.tsx";
13+
import UpgradeEmail from "./templates/UpgradeEmail.tsx";
14+
import DowngradeEmail from "./templates/DowngradeEmail.tsx";
15+
816
export type SendEmail = (options: {
917
to: string[];
1018
cc?: string[];
1119
bcc?: string[];
1220
subject: string;
1321
text: string;
14-
html?: string;
22+
react?: ReactNode;
1523
}) => Promise<void>;
1624

1725
const resend = new Resend(settings.EMAIL.RESEND_API_KEY);
@@ -22,7 +30,7 @@ const sendEmailWithResend: SendEmail = async (options: {
2230
bcc?: string[];
2331
subject: string;
2432
text: string;
25-
html?: string;
33+
react?: ReactNode;
2634
}) => {
2735
const response = await resend.emails.send({
2836
from: `${settings.EMAIL.FROM_EMAIL} <${settings.EMAIL.FROM_EMAIL}>`,
@@ -31,7 +39,7 @@ const sendEmailWithResend: SendEmail = async (options: {
3139
bcc: options.bcc,
3240
subject: options.subject,
3341
text: options.text,
34-
html: options.html,
42+
react: options.react,
3543
});
3644

3745
if (response.error) {
@@ -46,7 +54,7 @@ const sendEmailWithConsole: SendEmail = async (options: {
4654
bcc?: string[];
4755
subject: string;
4856
text: string;
49-
html?: string;
57+
react?: ReactNode;
5058
}) => {
5159
console.info(`Sending email to ${options.to}: ${options.subject}`);
5260
console.info(options.text);
@@ -60,52 +68,35 @@ function getSendEmail() {
6068
return sendEmailWithResend;
6169
}
6270

63-
export function sendOtpEmail(email: string, otp: string) {
71+
export async function sendOtpEmail(email: string, otp: string) {
72+
const emailPlainText = await render(OtpEmail({ otp }), {
73+
plainText: true,
74+
});
75+
6476
const sendEmail = getSendEmail();
6577
sendEmail({
6678
to: [email],
6779
subject: "Your One-Time Password (OTP)",
68-
text: `Hi there,
69-
70-
You've requested a one-time password to access your account. Please use the code below to complete your authentication:
71-
72-
**${otp}**
73-
74-
This code is valid for a limited time and can only be used once. For your security, please do not share this code with anyone.
75-
76-
If you didn't request this code, please ignore this email or contact our support team if you have concerns about your account security.
77-
78-
Best regards,
79-
The Team`,
80+
text: emailPlainText,
81+
react: OtpEmail({ otp }),
8082
});
8183
}
8284

83-
export function sendWelcomeEmail(email: string) {
85+
export async function sendWelcomeEmail(email: string) {
86+
const emailPlainText = await render(WelcomeEmail(), {
87+
plainText: true,
88+
});
89+
8490
const sendEmail = getSendEmail();
8591
sendEmail({
8692
to: [email],
8793
subject: "Welcome to our platform!",
88-
text: `Hi there,
89-
90-
Welcome to our platform! We're thrilled to have you join our community.
91-
92-
Your account has been successfully created and you're all set to get started. Here's what you can do next:
93-
94-
• Explore the dashboard and familiarize yourself with the interface
95-
• Set up your profile and preferences
96-
• Create your first workspace or join an existing one
97-
• Invite team members to collaborate with you
98-
99-
If you have any questions or need assistance getting started, our support team is here to help. Don't hesitate to reach out!
100-
101-
We're excited to see what you'll accomplish with our platform.
102-
103-
Best regards,
104-
The Team`,
94+
text: emailPlainText,
95+
react: WelcomeEmail(),
10596
});
10697
}
10798

108-
export function sendInvitationEmail(
99+
export async function sendInvitationEmail(
109100
email: string,
110101
workspaceName: string,
111102
invitationUuid: string,
@@ -118,27 +109,28 @@ export function sendInvitationEmail(
118109

119110
const invitationLink = `${returnUrl}?${searchParams.toString()}`;
120111

112+
const emailPlainText = await render(
113+
InvitationEmail({
114+
workspaceName,
115+
invitationLink,
116+
}),
117+
{
118+
plainText: true,
119+
},
120+
);
121+
121122
sendEmail({
122123
to: [email],
123124
subject: `Invitation to join ${workspaceName}`,
124-
text: `Hi there,
125-
126-
You've been invited to join "${workspaceName}"!
127-
128-
We're excited to have you as part of our team. To get started, simply click the link below to accept your invitation and set up your account:
129-
130-
${invitationLink}
131-
132-
This invitation link is unique to you and will expire after a certain period for security reasons. If you have any questions or need assistance, please don't hesitate to reach out to your team administrator.
133-
134-
We look forward to collaborating with you!
135-
136-
Best regards,
137-
The Team`,
125+
text: emailPlainText,
126+
react: InvitationEmail({
127+
workspaceName,
128+
invitationLink,
129+
}),
138130
});
139131
}
140132

141-
export function sendSubscriptionUpgradedEmail(
133+
export async function sendSubscriptionUpgradedEmail(
142134
payload: {
143135
email: string;
144136
workspaceName: string;
@@ -152,31 +144,23 @@ export function sendSubscriptionUpgradedEmail(
152144
};
153145
},
154146
) {
147+
const emailPlainText = await render(
148+
UpgradeEmail(payload),
149+
{
150+
plainText: true,
151+
},
152+
);
153+
155154
const sendEmail = getSendEmail();
156155
sendEmail({
157156
to: [payload.email],
158157
subject: "Subscription upgraded",
159-
text: `Hi there,
160-
161-
Your subscription for workspace "${payload.workspaceName}" has been successfully upgraded!
162-
163-
Previous subscription: ${payload.oldSubscription.product} (${
164-
payload.oldSubscription.billingCycle ?? "Custom billing cycle"
165-
})
166-
New subscription: ${payload.newSubscription.product} (${
167-
payload.newSubscription.billingCycle ?? "Custom billing cycle"
168-
})
169-
170-
This change is effective immediately, and you now have access to all the features included in your new subscription.
171-
172-
If you have any questions about your upgraded subscription, please don't hesitate to contact our support team.
173-
174-
Best regards,
175-
The Team`,
158+
text: emailPlainText,
159+
react: UpgradeEmail(payload),
176160
});
177161
}
178162

179-
export function sendSubscriptionDowngradedEmail(
163+
export async function sendSubscriptionDowngradedEmail(
180164
payload: {
181165
email: string;
182166
workspaceName: string;
@@ -191,27 +175,18 @@ export function sendSubscriptionDowngradedEmail(
191175
newSubscriptionDate: string;
192176
},
193177
) {
178+
const emailPlainText = await render(
179+
DowngradeEmail(payload),
180+
{
181+
plainText: true,
182+
},
183+
);
184+
194185
const sendEmail = getSendEmail();
195186
sendEmail({
196187
to: [payload.email],
197188
subject: "Subscription downgraded",
198-
text: `Hi there,
199-
200-
Your subscription for workspace "${payload.workspaceName}" has been scheduled for downgrade.
201-
202-
Current subscription: ${payload.oldSubscription.product} (${
203-
payload.oldSubscription.billingCycle ?? "Custom billing cycle"
204-
})
205-
New subscription: ${payload.newSubscription.product} (${
206-
payload.newSubscription.billingCycle ?? "Custom billing cycle"
207-
})
208-
Effective date: ${payload.newSubscriptionDate}
209-
210-
Your current subscription will remain active until the end of your billing period, and the new subscription will take effect on ${payload.newSubscriptionDate}.
211-
212-
If you have any questions, please don't hesitate to contact our support team.
213-
214-
Best regards,
215-
The Team`,
189+
text: emailPlainText,
190+
react: DowngradeEmail(payload),
216191
});
217192
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Heading, Section, Text } from "@react-email/components";
2+
import { baseTemplate } from "./base.tsx";
3+
import { headingStyle } from "./styles.tsx";
4+
import type {
5+
StripeBillingCycle,
6+
StripeProduct,
7+
} from "../../db/models/workspace.ts";
8+
9+
type DowngradeEmailProps = {
10+
email: string;
11+
workspaceName: string;
12+
oldSubscription: {
13+
product: StripeProduct;
14+
billingCycle: StripeBillingCycle | null;
15+
};
16+
newSubscription: {
17+
product: StripeProduct;
18+
billingCycle: StripeBillingCycle | null;
19+
};
20+
newSubscriptionDate: string;
21+
};
22+
23+
const DowngradeEmail = (props: DowngradeEmailProps) => {
24+
const previewText = "Confirmation of your subscription downgrade | NanoAPI";
25+
return baseTemplate(
26+
previewText,
27+
<>
28+
<Section>
29+
<Heading as="h2" style={headingStyle}>
30+
We are sorry to see you go
31+
</Heading>
32+
<Text>
33+
Your subscription for workspace {props.workspaceName}{" "}
34+
has been scheduled for downgrade.
35+
</Text>
36+
</Section>
37+
<Section>
38+
<Text
39+
style={{ textAlign: "center", fontSize: "20px", fontWeight: "bold" }}
40+
>
41+
Previous subscription: {props.oldSubscription.product}{" "}
42+
({props.oldSubscription.billingCycle ?? "Custom billing cycle"})
43+
</Text>
44+
<Text
45+
style={{ textAlign: "center", fontSize: "20px", fontWeight: "bold" }}
46+
>
47+
New subscription: {props.newSubscription.product}{" "}
48+
({props.newSubscription.billingCycle ?? "Custom billing cycle"})
49+
</Text>
50+
<Text style={{ textAlign: "center", fontWeight: "bold" }}>
51+
Effective date: {props.newSubscriptionDate}
52+
</Text>
53+
</Section>
54+
<Section>
55+
<Text>
56+
Your current subscription will remain active until the end of your
57+
billing period, and the new subscription will take effect on{" "}
58+
{props.newSubscriptionDate}.
59+
</Text>
60+
<Text>
61+
If you have any questions, please don't hesitate to contact our
62+
support team.
63+
</Text>
64+
<Text>Best regards - Team Nano</Text>
65+
</Section>
66+
</>,
67+
);
68+
};
69+
70+
DowngradeEmail.PreviewProps = {
71+
72+
workspaceName: "My Workspace",
73+
oldSubscription: {
74+
product: "Pro Plan",
75+
billingCycle: "Yearly",
76+
},
77+
newSubscription: {
78+
product: "Basic Plan",
79+
billingCycle: "Monthly",
80+
},
81+
newSubscriptionDate: "2023-10-01",
82+
};
83+
84+
export default DowngradeEmail;

0 commit comments

Comments
 (0)