@@ -5,13 +5,36 @@ import type {
55import { Resend } from "resend" ;
66import settings from "../settings.ts" ;
77
8+ import OtpEmail from "./templates/OtpEmail.tsx" ;
9+ import WelcomeEmail from "./templates/WelcomeEmail.tsx" ;
10+ import InvitationEmail from "./templates/InvitationEmail.tsx" ;
11+ import UpgradeEmail from "./templates/UpgradeEmail.tsx" ;
12+ 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+ }
31+
832export type SendEmail = ( options : {
933 to : string [ ] ;
1034 cc ?: string [ ] ;
1135 bcc ?: string [ ] ;
1236 subject : string ;
13- text : string ;
14- html ?: string ;
37+ react : ReactNode ;
1538} ) => Promise < void > ;
1639
1740const resend = new Resend ( settings . EMAIL . RESEND_API_KEY ) ;
@@ -21,91 +44,64 @@ const sendEmailWithResend: SendEmail = async (options: {
2144 cc ?: string [ ] ;
2245 bcc ?: string [ ] ;
2346 subject : string ;
24- text : string ;
25- html ?: string ;
47+ react : ReactNode ;
2648} ) => {
49+ const emailPlainText = await getPlainText ( options . react ) ;
50+
2751 const response = await resend . emails . send ( {
2852 from : `${ settings . EMAIL . FROM_EMAIL } <${ settings . EMAIL . FROM_EMAIL } >` ,
2953 to : options . to ,
3054 cc : options . cc ,
3155 bcc : options . bcc ,
3256 subject : options . subject ,
33- text : options . text ,
34- html : options . html ,
57+ text : emailPlainText ,
58+ react : options . react ,
3559 } ) ;
3660
3761 if ( response . error ) {
3862 throw new Error ( `Failed to send email: ${ response . error . message } ` ) ;
3963 }
4064} ;
4165
42- // deno-lint-ignore require-await
4366const sendEmailWithConsole : SendEmail = async ( options : {
4467 to : string [ ] ;
4568 cc ?: string [ ] ;
4669 bcc ?: string [ ] ;
4770 subject : string ;
48- text : string ;
49- html ?: string ;
71+ react : ReactNode ;
5072} ) => {
73+ const emailPlainText = await getPlainText ( options . react ) ;
74+
5175 console . info ( `Sending email to ${ options . to } : ${ options . subject } ` ) ;
52- console . info ( options . text ) ;
76+ console . info ( emailPlainText ) ;
5377} ;
5478
5579function getSendEmail ( ) {
5680 if ( settings . EMAIL . USE_CONSOLE ) {
5781 return sendEmailWithConsole ;
5882 }
59-
6083 return sendEmailWithResend ;
6184}
6285
63- export function sendOtpEmail ( email : string , otp : string ) {
86+ export async function sendOtpEmail ( email : string , otp : string ) {
6487 const sendEmail = getSendEmail ( ) ;
65- sendEmail ( {
88+ await sendEmail ( {
6689 to : [ email ] ,
6790 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` ,
91+ react : OtpEmail ( { otp } ) ,
8092 } ) ;
8193}
8294
83- export function sendWelcomeEmail ( email : string ) {
95+ export async function sendWelcomeEmail ( email : string ) {
8496 const sendEmail = getSendEmail ( ) ;
85- sendEmail ( {
97+ await sendEmail ( {
8698 to : [ email ] ,
8799 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` ,
100+ react : WelcomeEmail ( ) ,
105101 } ) ;
106102}
107103
108- export function sendInvitationEmail (
104+ export async function sendInvitationEmail (
109105 email : string ,
110106 workspaceName : string ,
111107 invitationUuid : string ,
@@ -118,29 +114,19 @@ export function sendInvitationEmail(
118114
119115 const invitationLink = `${ returnUrl } ?${ searchParams . toString ( ) } ` ;
120116
121- sendEmail ( {
117+ await sendEmail ( {
122118 to : [ email ] ,
123119 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` ,
120+ react : InvitationEmail ( {
121+ workspaceName,
122+ invitationLink,
123+ } ) ,
138124 } ) ;
139125}
140126
141- export function sendSubscriptionUpgradedEmail (
127+ export async function sendSubscriptionUpgradedEmail (
142128 payload : {
143- email : string ;
129+ emails : string [ ] ;
144130 workspaceName : string ;
145131 oldSubscription : {
146132 product : StripeProduct ;
@@ -153,32 +139,16 @@ export function sendSubscriptionUpgradedEmail(
153139 } ,
154140) {
155141 const sendEmail = getSendEmail ( ) ;
156- sendEmail ( {
157- to : [ payload . email ] ,
142+ await sendEmail ( {
143+ to : payload . emails ,
158144 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` ,
145+ react : UpgradeEmail ( payload ) ,
176146 } ) ;
177147}
178148
179- export function sendSubscriptionDowngradedEmail (
149+ export async function sendSubscriptionDowngradedEmail (
180150 payload : {
181- email : string ;
151+ emails : string [ ] ;
182152 workspaceName : string ;
183153 oldSubscription : {
184154 product : StripeProduct ;
@@ -192,26 +162,9 @@ export function sendSubscriptionDowngradedEmail(
192162 } ,
193163) {
194164 const sendEmail = getSendEmail ( ) ;
195- sendEmail ( {
196- to : [ payload . email ] ,
165+ await sendEmail ( {
166+ to : payload . emails ,
197167 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` ,
168+ react : DowngradeEmail ( payload ) ,
216169 } ) ;
217170}
0 commit comments