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; /** * 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; export type EmailTemplateParseInput = { 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 = { name: EmailTemplateName; description: string; describe: (t?: MailTranslator) => string; parse: (input: EmailTemplateParseInput) => { 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 = {}) { return template.replace(/\{\{\s*([.\w]+)\s*\}\}/g, (_match, key: string) => String(vars[key] ?? "")); } function mailText(t: MailTranslator | undefined, key: string, fallback: string, vars?: Record) { 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, };