stack/packages/stack-emails/src/utils.tsx
Zai Shi ce857fb0ac
fixed send team invitation api (#295)
* fixed send team invitation api

* added team invitation tests
2024-10-09 22:43:15 +02:00

191 lines
6.2 KiB
TypeScript

import { Body, Head, Html, Preview } from "@react-email/components";
import { render } from "@react-email/render";
import { typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects";
import * as Handlebars from 'handlebars/dist/handlebars.js';
import { EditorBlockSchema, TEditorConfiguration } from "./editor/documents/editor/core";
import { Reader } from "./editor/email-builder/index";
import { emailVerificationTemplate } from "./templates/email-verification";
import { magicLinkTemplate } from "./templates/magic-link";
import { magicLinkTemplateOld } from "./templates/magic-link-old";
import { passwordResetTemplate } from "./templates/password-reset";
import { teamInvitationTemplate } from "./templates/team-invitation";
const userVars = [
{ name: 'userDisplayName', label: 'User Display Name', defined: false, example: 'John Doe' },
] 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: { 1: TEditorConfiguration, 2: 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: { 1: emailVerificationTemplate, 2: emailVerificationTemplate },
defaultSubject: "Verify your email at {{ projectDisplayName }}",
variables: [
...userVars,
...projectVars,
{ name: 'emailVerificationLink', label: 'Email Verification Link', defined: true, example: '<email verification link>' },
],
} satisfies EmailTemplateMetadata,
'password_reset': {
label: "Password Reset",
description: "Will be sent to the user when they request to reset their password (forgot password)",
defaultContent: { 1: passwordResetTemplate, 2: passwordResetTemplate },
defaultSubject: "Reset your password at {{ projectDisplayName }}",
variables: [
...userVars,
...projectVars,
{ name: 'passwordResetLink', label: 'Reset Password Link', defined: true, example: '<reset password link>' },
],
} satisfies EmailTemplateMetadata,
'magic_link': {
label: "Magic Link/OTP",
description: "Will be sent to the user when they try to sign-up with magic link",
defaultContent: { 1: magicLinkTemplateOld, 2: magicLinkTemplate },
defaultSubject: "Sign in to {{ projectDisplayName }}",
variables: [
...userVars,
...projectVars,
{ name: 'magicLink', label: 'Magic Link', defined: true, example: '<magic link>' },
{ name: 'otp', label: 'OTP', defined: true, example: '3SLSWZ' },
],
} satisfies EmailTemplateMetadata,
'team_invitation': {
label: "Team Invitation",
description: "Will be sent to the user when they are invited to a team",
defaultContent: { 1: teamInvitationTemplate, 2: 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: '<team invitation link>' },
],
} 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<T extends NestedObject>(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<string, string | null>) {
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<string, string | null>,
) {
const mergedTemplate = objectStringMap(content, (str) => {
return renderString(str, variables);
});
const mergedSubject = renderString(subject, variables);
const component = (
<Html>
<Head></Head>
<Preview>{mergedSubject}</Preview>
<Body>
<Reader document={mergedTemplate} rootBlockId='root' />
</Body>
</Html>
);
const html = render(component);
const text = render(component, { plainText: true });
return { html, text, subject: mergedSubject };
}