Skip to content

Commit a3722ed

Browse files
committed
fix convertion email to text + some improvements
1 parent 7c9578d commit a3722ed

File tree

10 files changed

+125
-174
lines changed

10 files changed

+125
-174
lines changed

deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"stripe:cli": "docker run --rm -it --env-file=.env stripe/stripe-cli:latest",
1919
"stripe:forward": "deno task stripe:cli listen --forward-to http://host.docker.internal:4000/billing/webhook",
2020
"stripe:mock": "docker run --rm -it -p 12111-12112:12111-12112 stripe/stripe-mock:latest",
21-
"bucket:mock": "docker run --rm -it -p 4443:4443 fsouza/fake-gcs-server:latest -scheme http -public-host localhost:4443"
21+
"bucket:mock": "docker run --rm -it -p 4443:4443 fsouza/fake-gcs-server:latest -scheme http -public-host localhost:4443",
22+
"email": "deno task --config ./packages/core/deno.json email"
2223
},
2324
"lint": {
2425
"exclude": [

packages/core/deno.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
"@google-cloud/storage": "npm:@google-cloud/storage@^7.16.0",
1212
"@oak/oak": "jsr:@oak/oak@^17.1.4",
1313
"@react-email/components": "npm:@react-email/components@^0.1.0",
14-
"@react-email/render": "npm:@react-email/render@^1.1.2",
1514
"@sentry/deno": "npm:@sentry/deno@^9.28.1",
1615
"@std/assert": "jsr:@std/assert@1",
1716
"@std/expect": "jsr:@std/expect@^1.0.16",
1817
"@std/path": "jsr:@std/path@^1.0.9",
18+
"@types/html-to-text": "npm:@types/html-to-text@^9.0.4",
1919
"@types/pg": "npm:@types/pg@^8.15.2",
2020
"@types/pg-pool": "npm:@types/pg-pool@^2.0.6",
21+
"@types/react": "npm:@types/react@^19.1.8",
22+
"html-to-text": "npm:html-to-text@^9.0.5",
2123
"kysely": "npm:kysely@^0.28.2",
2224
"pg": "npm:pg@^8.16.0",
2325
"pg-pool": "npm:pg-pool@^3.10.0",
@@ -30,6 +32,8 @@
3032
"zod": "npm:zod@^3.24.4"
3133
},
3234
"compilerOptions": {
33-
"jsx": "react"
35+
"jsx": "react-jsx",
36+
"jsxImportSource": "react",
37+
"jsxImportSourceTypes": "@types/react"
3438
}
3539
}

packages/core/src/api/billing/service.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -355,14 +355,12 @@ export class BillingService {
355355
.where("member.role", "=", ADMIN_ROLE)
356356
.execute();
357357

358-
for (const email of emails) {
359-
await sendSubscriptionUpgradedEmail({
360-
email: email.email,
361-
workspaceName: workspace.name,
362-
oldSubscription,
363-
newSubscription,
364-
});
365-
}
358+
await sendSubscriptionUpgradedEmail({
359+
emails: emails.map((e) => e.email),
360+
workspaceName: workspace.name,
361+
oldSubscription,
362+
newSubscription,
363+
});
366364

367365
return {};
368366
}
@@ -494,15 +492,13 @@ export class BillingService {
494492
? currentSubscription.cancelAt.toISOString()
495493
: "unknown";
496494

497-
for (const email of emails) {
498-
await sendSubscriptionDowngradedEmail({
499-
email: email.email,
500-
workspaceName: workspace.name,
501-
oldSubscription,
502-
newSubscription,
503-
newSubscriptionDate,
504-
});
505-
}
495+
await sendSubscriptionDowngradedEmail({
496+
emails: emails.map((e) => e.email),
497+
workspaceName: workspace.name,
498+
oldSubscription,
499+
newSubscription,
500+
newSubscriptionDate,
501+
});
506502

507503
return {};
508504
}

packages/core/src/email/index.ts

Lines changed: 36 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
1-
import type { ReactNode } from "react";
21
import type {
32
StripeBillingCycle,
43
StripeProduct,
54
} from "../db/models/workspace.ts";
65
import { Resend } from "resend";
7-
import { render } from "@react-email/render";
86
import settings from "../settings.ts";
97

108
import OtpEmail from "./templates/OtpEmail.tsx";
119
import WelcomeEmail from "./templates/WelcomeEmail.tsx";
1210
import InvitationEmail from "./templates/InvitationEmail.tsx";
1311
import UpgradeEmail from "./templates/UpgradeEmail.tsx";
1412
import DowngradeEmail from "./templates/DowngradeEmail.tsx";
13+
import type { ReactNode } from "react";
14+
import { renderToString } from "react-dom/server";
15+
import { convert } from "html-to-text";
16+
17+
async function getPlainText(react: ReactNode) {
18+
const emailText = await renderToString(react);
19+
const plainText = convert(emailText, {
20+
selectors: [
21+
{ selector: "img", format: "skip" },
22+
{ selector: "[data-skip-in-text=true]", format: "skip" },
23+
{
24+
selector: "a",
25+
options: { linkBrackets: false },
26+
},
27+
],
28+
});
29+
return plainText;
30+
}
1531

1632
export type SendEmail = (options: {
1733
to: string[];
1834
cc?: string[];
1935
bcc?: string[];
2036
subject: string;
21-
text: string;
22-
react?: ReactNode;
37+
react: ReactNode;
2338
}) => Promise<void>;
2439

2540
const resend = new Resend(settings.EMAIL.RESEND_API_KEY);
@@ -29,16 +44,17 @@ const sendEmailWithResend: SendEmail = async (options: {
2944
cc?: string[];
3045
bcc?: string[];
3146
subject: string;
32-
text: string;
33-
react?: ReactNode;
47+
react: ReactNode;
3448
}) => {
49+
const emailPlainText = await getPlainText(options.react);
50+
3551
const response = await resend.emails.send({
3652
from: `${settings.EMAIL.FROM_EMAIL} <${settings.EMAIL.FROM_EMAIL}>`,
3753
to: options.to,
3854
cc: options.cc,
3955
bcc: options.bcc,
4056
subject: options.subject,
41-
text: options.text,
57+
text: emailPlainText,
4258
react: options.react,
4359
});
4460

@@ -47,51 +63,40 @@ const sendEmailWithResend: SendEmail = async (options: {
4763
}
4864
};
4965

50-
// deno-lint-ignore require-await
5166
const sendEmailWithConsole: SendEmail = async (options: {
5267
to: string[];
5368
cc?: string[];
5469
bcc?: string[];
5570
subject: string;
56-
text: string;
57-
react?: ReactNode;
71+
react: ReactNode;
5872
}) => {
73+
const emailPlainText = await getPlainText(options.react);
74+
5975
console.info(`Sending email to ${options.to}: ${options.subject}`);
60-
console.info(options.text);
76+
console.info(emailPlainText);
6177
};
6278

6379
function getSendEmail() {
6480
if (settings.EMAIL.USE_CONSOLE) {
6581
return sendEmailWithConsole;
6682
}
67-
6883
return sendEmailWithResend;
6984
}
7085

7186
export async function sendOtpEmail(email: string, otp: string) {
72-
const emailPlainText = await render(OtpEmail({ otp }), {
73-
plainText: true,
74-
});
75-
7687
const sendEmail = getSendEmail();
77-
sendEmail({
88+
await sendEmail({
7889
to: [email],
7990
subject: "Your One-Time Password (OTP)",
80-
text: emailPlainText,
8191
react: OtpEmail({ otp }),
8292
});
8393
}
8494

8595
export async function sendWelcomeEmail(email: string) {
86-
const emailPlainText = await render(WelcomeEmail(), {
87-
plainText: true,
88-
});
89-
9096
const sendEmail = getSendEmail();
91-
sendEmail({
97+
await sendEmail({
9298
to: [email],
9399
subject: "Welcome to our platform!",
94-
text: emailPlainText,
95100
react: WelcomeEmail(),
96101
});
97102
}
@@ -109,20 +114,9 @@ export async function sendInvitationEmail(
109114

110115
const invitationLink = `${returnUrl}?${searchParams.toString()}`;
111116

112-
const emailPlainText = await render(
113-
InvitationEmail({
114-
workspaceName,
115-
invitationLink,
116-
}),
117-
{
118-
plainText: true,
119-
},
120-
);
121-
122-
sendEmail({
117+
await sendEmail({
123118
to: [email],
124119
subject: `Invitation to join ${workspaceName}`,
125-
text: emailPlainText,
126120
react: InvitationEmail({
127121
workspaceName,
128122
invitationLink,
@@ -132,7 +126,7 @@ export async function sendInvitationEmail(
132126

133127
export async function sendSubscriptionUpgradedEmail(
134128
payload: {
135-
email: string;
129+
emails: string[];
136130
workspaceName: string;
137131
oldSubscription: {
138132
product: StripeProduct;
@@ -144,25 +138,17 @@ export async function sendSubscriptionUpgradedEmail(
144138
};
145139
},
146140
) {
147-
const emailPlainText = await render(
148-
UpgradeEmail(payload),
149-
{
150-
plainText: true,
151-
},
152-
);
153-
154141
const sendEmail = getSendEmail();
155-
sendEmail({
156-
to: [payload.email],
142+
await sendEmail({
143+
to: payload.emails,
157144
subject: "Subscription upgraded",
158-
text: emailPlainText,
159145
react: UpgradeEmail(payload),
160146
});
161147
}
162148

163149
export async function sendSubscriptionDowngradedEmail(
164150
payload: {
165-
email: string;
151+
emails: string[];
166152
workspaceName: string;
167153
oldSubscription: {
168154
product: StripeProduct;
@@ -175,18 +161,10 @@ export async function sendSubscriptionDowngradedEmail(
175161
newSubscriptionDate: string;
176162
},
177163
) {
178-
const emailPlainText = await render(
179-
DowngradeEmail(payload),
180-
{
181-
plainText: true,
182-
},
183-
);
184-
185164
const sendEmail = getSendEmail();
186-
sendEmail({
187-
to: [payload.email],
165+
await sendEmail({
166+
to: payload.emails,
188167
subject: "Subscription downgraded",
189-
text: emailPlainText,
190168
react: DowngradeEmail(payload),
191169
});
192170
}

packages/core/src/email/templates/DowngradeEmail.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
/** @jsx React.createElement */
2-
/** @jsxFrag React.Fragment */
3-
// deno-lint-ignore verbatim-module-syntax
4-
import React from "react";
51
import { Heading, Section, Text } from "@react-email/components";
62
import { baseTemplate } from "./base.tsx";
73
import { headingStyle } from "./styles.tsx";
@@ -10,8 +6,8 @@ import type {
106
StripeProduct,
117
} from "../../db/models/workspace.ts";
128

13-
type DowngradeEmailProps = {
14-
email: string;
9+
const DowngradeEmail = (props: {
10+
emails: string[];
1511
workspaceName: string;
1612
oldSubscription: {
1713
product: StripeProduct;
@@ -22,9 +18,7 @@ type DowngradeEmailProps = {
2218
billingCycle: StripeBillingCycle | null;
2319
};
2420
newSubscriptionDate: string;
25-
};
26-
27-
const DowngradeEmail = (props: DowngradeEmailProps) => {
21+
}) => {
2822
const previewText = "Confirmation of your subscription downgrade | NanoAPI";
2923
return baseTemplate(
3024
previewText,
@@ -72,7 +66,7 @@ const DowngradeEmail = (props: DowngradeEmailProps) => {
7266
};
7367

7468
DowngradeEmail.PreviewProps = {
75-
69+
emails: ["[email protected]"],
7670
workspaceName: "My Workspace",
7771
oldSubscription: {
7872
product: "Pro Plan",

packages/core/src/email/templates/InvitationEmail.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
/** @jsx React.createElement */
2-
/** @jsxFrag React.Fragment */
3-
// deno-lint-ignore verbatim-module-syntax
4-
import React from "react";
51
import { Button, Heading, Section, Text } from "@react-email/components";
62
import { baseTemplate } from "./base.tsx";
73
import { headingStyle } from "./styles.tsx";
84

9-
type InvitationEmailProps = {
5+
const InvitationEmail = (props: {
106
workspaceName: string;
117
invitationLink: string;
12-
};
13-
14-
const InvitationEmail = ({
15-
workspaceName,
16-
invitationLink,
17-
}: InvitationEmailProps) => {
18-
const previewText = `Invitation to join "${workspaceName}" on NanoAPI`;
8+
}) => {
9+
const previewText = `Invitation to join "${props.workspaceName}" on NanoAPI`;
1910
return baseTemplate(
2011
previewText,
2112
<>
2213
<Section>
2314
<Heading as="h2" style={headingStyle}>
24-
You have been invited to join "{workspaceName}"
15+
You have been invited to join "{props.workspaceName}"
2516
</Heading>
2617
<Text>
2718
We're excited to have you as part of our team. To get started, simply
@@ -33,7 +24,7 @@ const InvitationEmail = ({
3324
style={{ display: "flex", justifyContent: "center", padding: 20 }}
3425
>
3526
<Button
36-
href={invitationLink}
27+
href={props.invitationLink}
3728
style={{
3829
boxSizing: "border-box",
3930
padding: 12,

0 commit comments

Comments
 (0)