@@ -5,13 +5,36 @@ import type {
5
5
import { Resend } from "resend" ;
6
6
import settings from "../settings.ts" ;
7
7
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
+
8
32
export type SendEmail = ( options : {
9
33
to : string [ ] ;
10
34
cc ?: string [ ] ;
11
35
bcc ?: string [ ] ;
12
36
subject : string ;
13
- text : string ;
14
- html ?: string ;
37
+ react : ReactNode ;
15
38
} ) => Promise < void > ;
16
39
17
40
const resend = new Resend ( settings . EMAIL . RESEND_API_KEY ) ;
@@ -21,91 +44,64 @@ const sendEmailWithResend: SendEmail = async (options: {
21
44
cc ?: string [ ] ;
22
45
bcc ?: string [ ] ;
23
46
subject : string ;
24
- text : string ;
25
- html ?: string ;
47
+ react : ReactNode ;
26
48
} ) => {
49
+ const emailPlainText = await getPlainText ( options . react ) ;
50
+
27
51
const response = await resend . emails . send ( {
28
52
from : `${ settings . EMAIL . FROM_EMAIL } <${ settings . EMAIL . FROM_EMAIL } >` ,
29
53
to : options . to ,
30
54
cc : options . cc ,
31
55
bcc : options . bcc ,
32
56
subject : options . subject ,
33
- text : options . text ,
34
- html : options . html ,
57
+ text : emailPlainText ,
58
+ react : options . react ,
35
59
} ) ;
36
60
37
61
if ( response . error ) {
38
62
throw new Error ( `Failed to send email: ${ response . error . message } ` ) ;
39
63
}
40
64
} ;
41
65
42
- // deno-lint-ignore require-await
43
66
const sendEmailWithConsole : SendEmail = async ( options : {
44
67
to : string [ ] ;
45
68
cc ?: string [ ] ;
46
69
bcc ?: string [ ] ;
47
70
subject : string ;
48
- text : string ;
49
- html ?: string ;
71
+ react : ReactNode ;
50
72
} ) => {
73
+ const emailPlainText = await getPlainText ( options . react ) ;
74
+
51
75
console . info ( `Sending email to ${ options . to } : ${ options . subject } ` ) ;
52
- console . info ( options . text ) ;
76
+ console . info ( emailPlainText ) ;
53
77
} ;
54
78
55
79
function getSendEmail ( ) {
56
80
if ( settings . EMAIL . USE_CONSOLE ) {
57
81
return sendEmailWithConsole ;
58
82
}
59
-
60
83
return sendEmailWithResend ;
61
84
}
62
85
63
- export function sendOtpEmail ( email : string , otp : string ) {
86
+ export async function sendOtpEmail ( email : string , otp : string ) {
64
87
const sendEmail = getSendEmail ( ) ;
65
- sendEmail ( {
88
+ await sendEmail ( {
66
89
to : [ email ] ,
67
90
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 } ) ,
80
92
} ) ;
81
93
}
82
94
83
- export function sendWelcomeEmail ( email : string ) {
95
+ export async function sendWelcomeEmail ( email : string ) {
84
96
const sendEmail = getSendEmail ( ) ;
85
- sendEmail ( {
97
+ await sendEmail ( {
86
98
to : [ email ] ,
87
99
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 ( ) ,
105
101
} ) ;
106
102
}
107
103
108
- export function sendInvitationEmail (
104
+ export async function sendInvitationEmail (
109
105
email : string ,
110
106
workspaceName : string ,
111
107
invitationUuid : string ,
@@ -118,29 +114,19 @@ export function sendInvitationEmail(
118
114
119
115
const invitationLink = `${ returnUrl } ?${ searchParams . toString ( ) } ` ;
120
116
121
- sendEmail ( {
117
+ await sendEmail ( {
122
118
to : [ email ] ,
123
119
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
+ } ) ,
138
124
} ) ;
139
125
}
140
126
141
- export function sendSubscriptionUpgradedEmail (
127
+ export async function sendSubscriptionUpgradedEmail (
142
128
payload : {
143
- email : string ;
129
+ emails : string [ ] ;
144
130
workspaceName : string ;
145
131
oldSubscription : {
146
132
product : StripeProduct ;
@@ -153,32 +139,16 @@ export function sendSubscriptionUpgradedEmail(
153
139
} ,
154
140
) {
155
141
const sendEmail = getSendEmail ( ) ;
156
- sendEmail ( {
157
- to : [ payload . email ] ,
142
+ await sendEmail ( {
143
+ to : payload . emails ,
158
144
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 ) ,
176
146
} ) ;
177
147
}
178
148
179
- export function sendSubscriptionDowngradedEmail (
149
+ export async function sendSubscriptionDowngradedEmail (
180
150
payload : {
181
- email : string ;
151
+ emails : string [ ] ;
182
152
workspaceName : string ;
183
153
oldSubscription : {
184
154
product : StripeProduct ;
@@ -192,26 +162,9 @@ export function sendSubscriptionDowngradedEmail(
192
162
} ,
193
163
) {
194
164
const sendEmail = getSendEmail ( ) ;
195
- sendEmail ( {
196
- to : [ payload . email ] ,
165
+ await sendEmail ( {
166
+ to : payload . emails ,
197
167
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 ) ,
216
169
} ) ;
217
170
}
0 commit comments