refactor: update email template system with improved type safety and locale handling
- Replace `templateId` with `template` in email sending function - Rename `translations` to `messages` in email template props - Consolidate email template and sending logic in `send.ts` - Remove separate `templates.ts` file and merge functionality - Update email template components to use new message structure - Improve type definitions for email-related functions and props
This commit is contained in:
parent
620cedb8c3
commit
988a1a6b58
@ -54,7 +54,7 @@ export const auth = betterAuth({
|
||||
console.log("sendResetPassword, locale:", locale);
|
||||
await send({
|
||||
to: user.email,
|
||||
templateId: "forgotPassword",
|
||||
template: "forgotPassword",
|
||||
context: {
|
||||
url,
|
||||
name: user.name,
|
||||
@ -72,7 +72,7 @@ export const auth = betterAuth({
|
||||
console.log("sendVerificationEmail, locale:", locale);
|
||||
await send({
|
||||
to: user.email,
|
||||
templateId: "verifyEmail",
|
||||
template: "verifyEmail",
|
||||
context: {
|
||||
url,
|
||||
name: user.name,
|
||||
|
@ -1,30 +1,29 @@
|
||||
import { Link, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { createTranslator } from "use-intl/core";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import EmailButton from "@/mail/components/EmailButton";
|
||||
import EmailLayout from "@/mail/components/EmailLayout";
|
||||
import { defaultLocale, defaultTranslations } from "@/mail/translations";
|
||||
import { defaultLocale, defaultMessages } from "@/mail/messages";
|
||||
import type { BaseMailProps } from "@/mail/types";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { Text } from "@react-email/components";
|
||||
import { createTranslator } from "use-intl/core";
|
||||
|
||||
export function ForgotPassword({
|
||||
url,
|
||||
name,
|
||||
locale,
|
||||
translations,
|
||||
messages,
|
||||
}: {
|
||||
url: string;
|
||||
name: string;
|
||||
} & BaseMailProps) {
|
||||
const t = createTranslator({
|
||||
locale,
|
||||
messages: translations,
|
||||
messages,
|
||||
});
|
||||
|
||||
return (
|
||||
<EmailLayout>
|
||||
<Text>{t("mail.forgotPassword.title", { name })}</Text>
|
||||
|
||||
|
||||
<Text>{t("mail.forgotPassword.body")}</Text>
|
||||
|
||||
<EmailButton href={url}>
|
||||
@ -41,7 +40,7 @@ export function ForgotPassword({
|
||||
|
||||
ForgotPassword.PreviewProps = {
|
||||
locale: defaultLocale,
|
||||
translations: defaultTranslations,
|
||||
messages: defaultMessages,
|
||||
url: "https://mksaas.com",
|
||||
name: "username",
|
||||
};
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { Heading, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { createTranslator } from "use-intl/core";
|
||||
import EmailLayout from "@/mail/components/EmailLayout";
|
||||
import { defaultLocale, defaultTranslations } from "@/mail/translations";
|
||||
import { defaultLocale, defaultMessages } from "@/mail/messages";
|
||||
import type { BaseMailProps } from "@/mail/types";
|
||||
import { Heading, Text } from "@react-email/components";
|
||||
import { createTranslator } from "use-intl/core";
|
||||
|
||||
export function SubscribeNewsletter({ locale, translations }: BaseMailProps) {
|
||||
export function SubscribeNewsletter({
|
||||
locale,
|
||||
messages,
|
||||
}: BaseMailProps) {
|
||||
const t = createTranslator({
|
||||
locale,
|
||||
messages: translations,
|
||||
messages,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -23,7 +25,7 @@ export function SubscribeNewsletter({ locale, translations }: BaseMailProps) {
|
||||
|
||||
SubscribeNewsletter.PreviewProps = {
|
||||
locale: defaultLocale,
|
||||
translations: defaultTranslations,
|
||||
messages: defaultMessages,
|
||||
};
|
||||
|
||||
export default SubscribeNewsletter;
|
||||
|
@ -1,30 +1,29 @@
|
||||
import { Link, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
import { createTranslator } from "use-intl/core";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import EmailButton from "@/mail/components/EmailButton";
|
||||
import EmailLayout from "@/mail/components/EmailLayout";
|
||||
import { defaultLocale, defaultTranslations } from "@/mail/translations";
|
||||
import { defaultLocale, defaultMessages } from "@/mail/messages";
|
||||
import type { BaseMailProps } from "@/mail/types";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { Text } from "@react-email/components";
|
||||
import { createTranslator } from "use-intl/core";
|
||||
|
||||
export function VerifyEmail({
|
||||
url,
|
||||
name,
|
||||
locale,
|
||||
translations,
|
||||
messages,
|
||||
}: {
|
||||
url: string;
|
||||
name: string;
|
||||
} & BaseMailProps) {
|
||||
const t = createTranslator({
|
||||
locale,
|
||||
messages: translations,
|
||||
messages,
|
||||
});
|
||||
|
||||
return (
|
||||
<EmailLayout>
|
||||
<Text>{t("mail.verifyEmail.title", { name })}</Text>
|
||||
|
||||
|
||||
<Text>{t("mail.verifyEmail.body")}</Text>
|
||||
|
||||
<EmailButton href={url}>
|
||||
@ -34,14 +33,14 @@ export function VerifyEmail({
|
||||
<br /><br /><br />
|
||||
|
||||
<Text>{t("mail.common.team", { name: siteConfig.name })}</Text>
|
||||
<Text>{t("mail.common.copyright", { year: new Date().getFullYear()})}</Text>
|
||||
<Text>{t("mail.common.copyright", { year: new Date().getFullYear() })}</Text>
|
||||
</EmailLayout>
|
||||
);
|
||||
}
|
||||
|
||||
VerifyEmail.PreviewProps = {
|
||||
locale: defaultLocale,
|
||||
translations: defaultTranslations,
|
||||
messages: defaultMessages,
|
||||
url: "https://mksaas.com",
|
||||
name: "username",
|
||||
};
|
||||
|
@ -2,6 +2,6 @@ import { routing } from "@/i18n/routing";
|
||||
|
||||
const { defaultLocale } = routing;
|
||||
|
||||
export { default as defaultTranslations } from "../../messages/en.json";
|
||||
export { default as defaultMessages } from "../../messages/en.json";
|
||||
|
||||
export { defaultLocale };
|
@ -1,30 +1,36 @@
|
||||
import type { Messages } from "@/i18n/messages";
|
||||
import { getMessagesForLocale } from "@/i18n/messages";
|
||||
import { Locale, routing } from "@/i18n/routing";
|
||||
import type { mailTemplates } from "@/mail/emails";
|
||||
import { mailTemplates } from "@/mail/emails";
|
||||
import { sendEmail } from "@/mail/provider/resend";
|
||||
import type { TemplateId } from "./templates";
|
||||
import { getTemplate } from "./templates";
|
||||
import { render } from "@react-email/render";
|
||||
|
||||
export type Template = keyof typeof mailTemplates;
|
||||
|
||||
/**
|
||||
* send email with given template, locale, and context
|
||||
* send email
|
||||
*
|
||||
* 1. with given template, and context
|
||||
* 2. with given subject, text, and html
|
||||
*/
|
||||
export async function send<T extends TemplateId>(
|
||||
export async function send<T extends Template>(
|
||||
params: {
|
||||
to: string;
|
||||
locale?: Locale;
|
||||
} & (
|
||||
| {
|
||||
templateId: T;
|
||||
| {
|
||||
template: T;
|
||||
context: Omit<
|
||||
Parameters<(typeof mailTemplates)[T]>[0],
|
||||
"locale" | "translations"
|
||||
"locale" | "messages"
|
||||
>;
|
||||
}
|
||||
| {
|
||||
}
|
||||
| {
|
||||
subject: string;
|
||||
text?: string;
|
||||
html?: string;
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
) {
|
||||
const { to, locale = routing.defaultLocale } = params;
|
||||
console.log("send, locale:", locale);
|
||||
@ -33,16 +39,18 @@ export async function send<T extends TemplateId>(
|
||||
let text: string;
|
||||
let subject: string;
|
||||
|
||||
if ("templateId" in params) {
|
||||
const { templateId, context } = params;
|
||||
const template = await getTemplate({
|
||||
templateId,
|
||||
// if template is provided, get the template
|
||||
// otherwise, use the subject, text, and html
|
||||
if ("template" in params) {
|
||||
const { template, context } = params;
|
||||
const mailTemplate = await getTemplate({
|
||||
template,
|
||||
context,
|
||||
locale,
|
||||
});
|
||||
subject = template.subject;
|
||||
text = template.text;
|
||||
html = template.html;
|
||||
subject = mailTemplate.subject;
|
||||
text = mailTemplate.text;
|
||||
html = mailTemplate.html;
|
||||
} else {
|
||||
subject = params.subject;
|
||||
text = params.text ?? "";
|
||||
@ -62,3 +70,38 @@ export async function send<T extends TemplateId>(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get rendered email for given template, context, and locale
|
||||
*/
|
||||
export async function getTemplate<T extends Template>({
|
||||
template,
|
||||
context,
|
||||
locale,
|
||||
}: {
|
||||
template: T;
|
||||
context: Omit<
|
||||
Parameters<(typeof mailTemplates)[T]>[0],
|
||||
"locale" | "messages"
|
||||
>;
|
||||
locale: Locale;
|
||||
}) {
|
||||
const mainTemplate = mailTemplates[template];
|
||||
const messages = await getMessagesForLocale(locale);
|
||||
|
||||
const email = mainTemplate({
|
||||
...(context as any),
|
||||
locale,
|
||||
messages,
|
||||
});
|
||||
|
||||
// get the subject from the messages
|
||||
const subject =
|
||||
"subject" in messages.mail[template as keyof Messages["mail"]]
|
||||
? messages.mail[template].subject
|
||||
: "";
|
||||
|
||||
const html = await render(email);
|
||||
const text = await render(email, { plainText: true });
|
||||
return { html, text, subject };
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
import type { Messages } from "@/i18n/messages";
|
||||
import { getMessagesForLocale } from "@/i18n/messages";
|
||||
import type { Locale } from "@/i18n/routing";
|
||||
import { mailTemplates } from "@/mail/emails";
|
||||
import { render } from "@react-email/render";
|
||||
|
||||
export type TemplateId = keyof typeof mailTemplates;
|
||||
|
||||
/**
|
||||
* get rendered email for given template id, context, and locale
|
||||
*/
|
||||
export async function getTemplate<T extends TemplateId>({
|
||||
templateId,
|
||||
context,
|
||||
locale,
|
||||
}: {
|
||||
templateId: T;
|
||||
context: Omit<
|
||||
Parameters<(typeof mailTemplates)[T]>[0],
|
||||
"locale" | "translations"
|
||||
>;
|
||||
locale: Locale;
|
||||
}) {
|
||||
const template = mailTemplates[templateId];
|
||||
const translations = await getMessagesForLocale(locale);
|
||||
|
||||
const email = template({
|
||||
...(context as any),
|
||||
locale,
|
||||
translations,
|
||||
});
|
||||
|
||||
const subject =
|
||||
"subject" in translations.mail[templateId as keyof Messages["mail"]]
|
||||
? translations.mail[templateId].subject
|
||||
: "";
|
||||
|
||||
const html = await render(email);
|
||||
const text = await render(email, { plainText: true });
|
||||
return { html, text, subject };
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import type { Locale } from "@/i18n/routing";
|
||||
import type { Messages } from "@/i18n/messages";
|
||||
|
||||
export interface SendEmailParams {
|
||||
export interface EmailParams {
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
html?: string;
|
||||
}
|
||||
|
||||
export type SendEmailHandler = (params: SendEmailParams) => Promise<void>;
|
||||
export type SendEmailHandler = (params: EmailParams) => Promise<void>;
|
||||
|
||||
export interface MailProvider {
|
||||
send: SendEmailHandler;
|
||||
@ -15,5 +16,5 @@ export interface MailProvider {
|
||||
|
||||
export type BaseMailProps = {
|
||||
locale: Locale;
|
||||
translations: any;
|
||||
messages: Messages;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user