Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/emails/utils/EmailTemplates.ts
T

240 lines
9.9 KiB
TypeScript

import { resolveTransactionalBrandTokens } from "@toolctrl/brand";
import { buildBrand, renderTransactionalEmail, type MailBrand } from "@toolctrl/mail";
import { AppConfiguration } from "~/utils/db/appConfiguration.db.server";
/**
* Legacy template identifiers preserved for downstream call-sites.
* The Stripe-19 / React-Email migration consolidates these onto the
* React-Email pipeline:
*
* - `welcome` -> React `welcome`
* - `password-reset` -> React `password-reset`
* - `user-invitation` -> React `invitation`
* - `email-verification` -> React `magic-link` (verification variant)
* - `account-setup` -> React `welcome` (account-setup variant)
*/
export type EmailTemplateName = "welcome" | "password-reset" | "user-invitation" | "email-verification" | "account-setup";
export type EmailTemplateData = Record<string, unknown>;
/**
* Mail-side translator signature retained for call-site compatibility.
* Legacy callers pass `t` + `locale` from `getMailTranslator`; the wrapper
* now uses that hook for subject/body chrome while keeping English fallback
* strings for tests and non-request call-sites.
*/
export type MailTranslator = (key: string, vars?: Record<string, unknown>) => string;
export type EmailTemplateParseInput<TData extends EmailTemplateData = EmailTemplateData> = {
appConfiguration: AppConfiguration;
data: TData;
/** Reserved for future i18n hooks. Currently a no-op. */
t?: MailTranslator;
/** User/request locale (informational; reserved for future i18n hooks). */
locale?: string;
};
type EmailTemplate<TData extends EmailTemplateData = EmailTemplateData> = {
name: EmailTemplateName;
description: string;
describe: (t?: MailTranslator) => string;
parse: (input: EmailTemplateParseInput<TData>) => {
subject: string;
body: string;
data: TData;
};
};
function toBrandConfiguration(appConfiguration: AppConfiguration) {
return {
productName: appConfiguration.app?.name,
supportEmail: appConfiguration.email?.supportEmail,
supportFromName: appConfiguration.email?.fromName,
emailFromName: appConfiguration.email?.fromName,
logoUrl: appConfiguration.branding?.logo,
faviconUrl: appConfiguration.branding?.favicon,
};
}
function brandFor(appConfiguration: AppConfiguration): MailBrand {
return buildBrand(resolveTransactionalBrandTokens(toBrandConfiguration(appConfiguration)));
}
function interpolate(template: string, vars: Record<string, unknown> = {}) {
return template.replace(/\{\{\s*([.\w]+)\s*\}\}/g, (_match, key: string) => String(vars[key] ?? ""));
}
function mailText(t: MailTranslator | undefined, key: string, fallback: string, vars?: Record<string, unknown>) {
const translated = t?.(key, vars);
return translated && translated !== key ? translated : interpolate(fallback, vars);
}
function description(key: string, fallback: string) {
return {
description: mailText(undefined, key, fallback),
describe: (t?: MailTranslator) => mailText(t, key, fallback),
};
}
const WELCOME_EMAIL: EmailTemplate<{ name?: string; action_url: string }> = {
name: "welcome",
...description("registry.welcomeDescription", "Welcome email with login link"),
parse: ({ appConfiguration, data, t }) => {
const brand = brandFor(appConfiguration);
const subject = data.name
? mailText(t, "welcome.subject_named", "Welcome to {{productName}}, {{name}}!", { productName: brand.productName, name: data.name })
: mailText(t, "welcome.subject", "Welcome to {{productName}}!", { productName: brand.productName });
const { html } = renderTransactionalEmail("welcome", {
brand,
recipientName: data.name,
actionUrl: data.action_url,
subject,
eyebrow: mailText(t, "welcome.eyebrow", "Welcome"),
title: data.name
? mailText(t, "welcome.title_named", "Welcome, {{name}}!", { name: data.name })
: mailText(t, "welcome.title", "Welcome!"),
intro: mailText(t, "welcome.intro", "Thanks for trying {{productName}}.", { productName: brand.productName }),
actionLabel: mailText(t, "welcome.actionLabel", "Click here to log in"),
signoffThanks: mailText(t, "common.signoff.thanks", "Thanks,"),
});
return { subject, body: html, data };
},
};
const PASSWORD_RESET_EMAIL: EmailTemplate<{ name?: string; action_url: string }> = {
name: "password-reset",
...description("registry.passwordResetDescription", "Email sent to users who requested a password reset"),
parse: ({ appConfiguration, data, t }) => {
const brand = brandFor(appConfiguration);
const subject = mailText(t, "passwordReset.subject", "Reset your {{productName}} password", { productName: brand.productName });
const { html } = renderTransactionalEmail("password-reset", {
brand,
recipientName: data.name,
actionUrl: data.action_url,
subject,
eyebrow: mailText(t, "passwordReset.eyebrow", "Password reset"),
title: data.name
? mailText(t, "passwordReset.title_named", "Hello, {{name}}!", { name: data.name })
: mailText(t, "passwordReset.title", "Hello!"),
intro: mailText(t, "passwordReset.intro", "You recently requested to reset your password for your {{productName}} account.", {
productName: brand.productName,
}),
actionLabel: mailText(t, "passwordReset.actionLabel", "Reset password"),
outro: mailText(
t,
"passwordReset.outro",
"If you did not request a password reset, please ignore this email or contact support if you have questions."
),
signoffThanks: mailText(t, "common.signoff.thanks", "Thanks,"),
});
return { subject, body: html, data };
},
};
const USER_INVITATION_EMAIL: EmailTemplate<{
name?: string;
invite_sender_name: string;
invite_sender_organization: string;
action_url: string;
}> = {
name: "user-invitation",
...description("registry.userInvitationDescription", "Email sent to users who are invited to join an organization"),
parse: ({ appConfiguration, data, t }) => {
const brand = brandFor(appConfiguration);
const vars = {
productName: brand.productName,
senderName: data.invite_sender_name,
senderOrganization: data.invite_sender_organization,
name: data.name,
};
const subject = mailText(t, "userInvitation.subject", "{{senderName}} invited you to {{senderOrganization}} on {{productName}}", vars);
const { html } = renderTransactionalEmail("invitation", {
brand,
recipientName: data.name,
senderName: data.invite_sender_name,
senderOrganization: data.invite_sender_organization,
actionUrl: data.action_url,
subject,
eyebrow: mailText(t, "userInvitation.eyebrow", "Invitation"),
title: data.name ? mailText(t, "userInvitation.title_named", "Hello, {{name}}!", vars) : mailText(t, "userInvitation.title", "Hello!"),
intro: mailText(
t,
"userInvitation.intro",
"{{senderName}} with {{senderOrganization}} has invited you to use {{productName}} to collaborate with them.",
vars
),
actionLabel: mailText(t, "userInvitation.actionLabel", "Join {{senderOrganization}}", vars),
signoffThanks: mailText(t, "common.signoff.thanks", "Thanks,"),
});
return { subject, body: html, data };
},
};
const VERIFICATION_EMAIL: EmailTemplate<{ name?: string; action_url: string }> = {
name: "email-verification",
...description("registry.emailVerificationDescription", "Email sent to users to verify their email address"),
parse: ({ appConfiguration, data, t }) => {
const brand = brandFor(appConfiguration);
const vars = { productName: brand.productName, name: data.name };
const subject = mailText(t, "emailVerification.subject", "Verify your {{productName}} email address", vars);
const { html } = renderTransactionalEmail("magic-link", {
brand,
recipientName: data.name,
actionUrl: data.action_url,
subject,
eyebrow: mailText(t, "emailVerification.eyebrow", "Email verification"),
title: data.name
? mailText(t, "emailVerification.title_named", "Thanks for signing up, {{name}}!", vars)
: mailText(t, "emailVerification.title", "Thanks for signing up!"),
intro: mailText(t, "emailVerification.intro", "Confirm your email address to finish setting up your {{productName}} account.", vars),
actionLabel: mailText(t, "emailVerification.actionLabel", "Click here to verify your email"),
expiresInMinutes: 30,
signoffThanks: mailText(t, "common.signoff.thanks", "Thanks,"),
});
return { subject, body: html, data };
},
};
const ACCOUNT_SETUP_EMAIL: EmailTemplate<{ email: string; plan: string; action_url: string }> = {
name: "account-setup",
...description("registry.accountSetupDescription", "Email sent to users to set up their account"),
parse: ({ appConfiguration, data, t }) => {
const brand = brandFor(appConfiguration);
const vars = { productName: brand.productName, plan: data.plan, email: data.email };
const subject = mailText(t, "accountSetup.subject", "Finish setting up your {{productName}} account", vars);
const { html } = renderTransactionalEmail("welcome", {
brand,
actionUrl: data.action_url,
subject,
eyebrow: mailText(t, "accountSetup.eyebrow", "Account setup"),
title: mailText(t, "accountSetup.title", "Almost there - let's get you started", vars),
intro: mailText(
t,
"accountSetup.intro",
"Thanks for subscribing to the {{plan}} plan. Use the button below to finish setting up your account for {{email}}.",
vars
),
actionLabel: mailText(t, "accountSetup.actionLabel", "Set up account"),
signoffThanks: mailText(t, "common.signoff.thanks", "Thanks,"),
});
return { subject, body: html, data };
},
};
const allTemplates = [
WELCOME_EMAIL,
PASSWORD_RESET_EMAIL,
USER_INVITATION_EMAIL,
VERIFICATION_EMAIL,
ACCOUNT_SETUP_EMAIL,
];
export default {
allTemplates,
WELCOME_EMAIL,
PASSWORD_RESET_EMAIL,
USER_INVITATION_EMAIL,
VERIFICATION_EMAIL,
ACCOUNT_SETUP_EMAIL,
};