import { EditorBlockSchema, TEditorConfiguration } from "@stackframe/stack-emails/dist/editor/documents/editor/core"; import { typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects"; import { emailVerificationTemplate } from "./templates/email-verification"; import { passwordResetTemplate } from "./templates/password-reset"; import { magicLinkTemplate } from "./templates/magic-link"; import { render } from "@react-email/render"; import { Reader } from "@stackframe/stack-emails/dist/editor/email-builder/index"; import { Body, Head, Html, Preview } from "@react-email/components"; import * as Handlebars from 'handlebars/dist/handlebars.js'; import _ from 'lodash'; import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import { teamInvitationTemplate } from "./templates/team-invitation"; // TODO remove this one export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } const userVars = [ { name: 'userDisplayName', label: 'User Display Name', defined: false, example: 'John Doe' }, { name: 'userPrimaryEmail', label: 'User Primary Email', defined: true, example: 'example@email.com' }, ] as const; const projectVars = [ { name: 'projectDisplayName', label: 'Project Name', defined: true, example: '{{ projectDisplayName }}' }, ]; export type EmailTemplateVariable = { name: string, label: string, defined: boolean, example: string, }; export type EmailTemplateMetadata = { label: string, description: string, defaultContent: TEditorConfiguration, defaultSubject: string, variables: EmailTemplateVariable[], }; export const EMAIL_TEMPLATES_METADATA = { 'email_verification': { label: "Email Verification", description: "Will be sent to the user when they sign-up with email/password", defaultContent: emailVerificationTemplate, defaultSubject: "Verify your email at {{ projectDisplayName }}", variables: [ ...userVars, ...projectVars, { name: 'emailVerificationLink', label: 'Email Verification Link', defined: true, example: '' }, ], } satisfies EmailTemplateMetadata, 'password_reset': { label: "Password Reset", description: "Will be sent to the user when they request to reset their password (forgot password)", defaultContent: passwordResetTemplate, defaultSubject: "Reset your password at {{ projectDisplayName }}", variables: [ ...userVars, ...projectVars, { name: 'passwordResetLink', label: 'Reset Password Link', defined: true, example: '' }, ], } satisfies EmailTemplateMetadata, 'magic_link': { label: "Magic Link", description: "Will be sent to the user when they try to sign-up with magic link", defaultContent: magicLinkTemplate, defaultSubject: "Sign in to {{ projectDisplayName }}", variables: [ ...userVars, ...projectVars, { name: 'magicLink', label: 'Magic Link', defined: true, example: '' }, ], } satisfies EmailTemplateMetadata, 'team_invitation': { label: "Team Invitation", description: "Will be sent to the user when they are invited to a team", defaultContent: teamInvitationTemplate, defaultSubject: "You have been invited to join {{ teamDisplayName }}", variables: [ ...userVars, ...projectVars, { name: 'teamDisplayName', label: 'Team Display Name', defined: true, example: 'My Team' }, { name: 'teamInvitationLink', label: 'Team Invitation Link', defined: true, example: '' }, ], } satisfies EmailTemplateMetadata, } as const; export function validateEmailTemplateContent(content: any): content is TEditorConfiguration { try { for (const key of Object.keys(content)) { const block = content[key]; EditorBlockSchema.parse(block); } return true; } catch (e) { console.error(e); return false; } } type NestedObject = { [key: string]: any }; export function objectStringMap(obj: T, func: (s: string) => string): T { function mapStrings(obj: NestedObject): NestedObject { const result: NestedObject = Array.isArray(obj) ? [] : {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key]; if (typeof value === 'string') { result[key] = func(value); } else if (typeof value === 'object' && value !== null) { result[key] = mapStrings(value); } else { result[key] = value; } } } return result; } return mapStrings(obj) as T; } function renderString(str: string, variables: Record) { try { return Handlebars.compile(str, {noEscape: true})(variables); } catch (e) { return str; } } export function convertEmailTemplateMetadataExampleValues( metadata: EmailTemplateMetadata, projectDisplayName: string, ): EmailTemplateMetadata { const variables = metadata.variables.map((variable) => { return { ...variable, example: renderString(variable.example, { projectDisplayName }), }; }); return { ...metadata, variables, }; } export function convertEmailTemplateVariables( content: TEditorConfiguration, variables: EmailTemplateVariable[], ): TEditorConfiguration { const vars = typedFromEntries(variables.map((variable) => [variable.name, variable.example])); return objectStringMap(content, (str) => { return renderString(str, vars); }); } export function convertEmailSubjectVariables( subject: string, variables: EmailTemplateVariable[], ): string { const vars = typedFromEntries(variables.map((variable) => [variable.name, variable.example])); return renderString(subject, vars); } export function renderEmailTemplate( subject: string, content: TEditorConfiguration, variables: Record, ) { const mergedTemplate = objectStringMap(content, (str) => { return renderString(str, variables); }); const mergedSubject = renderString(subject, variables); const component = ( {mergedSubject} ); const html = render(component); const text = render(component, { plainText: true }); return { html, text, subject: mergedSubject }; }