diff --git a/.changeset/cyan-ears-shout.md b/.changeset/cyan-ears-shout.md new file mode 100644 index 0000000000..1e016cc557 --- /dev/null +++ b/.changeset/cyan-ears-shout.md @@ -0,0 +1,5 @@ +--- +'hive': patch +--- + +Standardize the design and content of all email templates for consistency. diff --git a/packages/services/emails/src/mjml.ts b/packages/services/emails/src/mjml.ts index cbcbd5b9bb..87b789d157 100644 --- a/packages/services/emails/src/mjml.ts +++ b/packages/services/emails/src/mjml.ts @@ -8,7 +8,7 @@ type RawValue = { readonly content: string; }; type SpecialValues = RawValue; -type ValueExpression = string | SpecialValues; +type ValueExpression = string | SpecialValues | MJMLValue; export function mjml(parts: TemplateStringsArray, ...values: ValueExpression[]): MJMLValue { let content = ''; @@ -29,6 +29,8 @@ export function mjml(parts: TemplateStringsArray, ...values: ValueExpression[]): content += token.content; } else if (typeof token === 'string') { content += escapeHtml(token); + } else if (token.kind === 'mjml') { + content += token.content; } else { throw new TypeError('mjml: Unexpected value expression.'); } diff --git a/packages/services/emails/src/templates/audit-logs-report.ts b/packages/services/emails/src/templates/audit-logs-report.ts index 399123b39b..63b9c7e70e 100644 --- a/packages/services/emails/src/templates/audit-logs-report.ts +++ b/packages/services/emails/src/templates/audit-logs-report.ts @@ -1,4 +1,4 @@ -import { mjml } from '../mjml'; +import { button, email, mjml, paragraph } from './components'; export function renderAuditLogsReportEmail(input: { organizationName: string; @@ -6,22 +6,13 @@ export function renderAuditLogsReportEmail(input: { formattedEndDate: string; url: string; }) { - return mjml` - - - - - - - - Audit Logs for your organization ${input.organizationName} from ${input.formattedStartDate} to ${input.formattedEndDate} - . - - Download Audit Logs CSV - - - - - - `.content; + return email({ + title: 'Your Requested Audit Logs Are Ready', + body: mjml` + ${paragraph(mjml`You requested audit logs for ${input.formattedStartDate} – ${input.formattedEndDate}, and they are now ready for download.`)} + ${paragraph('Click the link below to download your CSV file:')} + ${button({ url: input.url, text: 'Download Audit Logs' })} + ${paragraph(`If you didn't request this, please contact support@graphql-hive.com.`)} + `, + }); } diff --git a/packages/services/emails/src/templates/components.ts b/packages/services/emails/src/templates/components.ts new file mode 100644 index 0000000000..6194168b1f --- /dev/null +++ b/packages/services/emails/src/templates/components.ts @@ -0,0 +1,52 @@ +import { mjml, type MJMLValue } from '../mjml'; + +export { mjml }; + +export function paragraph(content: string | MJMLValue) { + return mjml` + + ${content} + + `; +} + +export function button(input: { url: string; text: string }) { + return mjml` + + ${input.text} + + `; +} + +export function email(input: { title: string | MJMLValue; body: MJMLValue }) { + return mjml` + + + + + + Hive + + + + + + + + ${input.title} + + ${input.body} + + + + + + + © ${mjml.raw(String(new Date().getFullYear()))} Hive. All rights reserved. + + + + + + `.content; +} diff --git a/packages/services/emails/src/templates/email-verification.ts b/packages/services/emails/src/templates/email-verification.ts index ba46386544..806bb95525 100644 --- a/packages/services/emails/src/templates/email-verification.ts +++ b/packages/services/emails/src/templates/email-verification.ts @@ -1,923 +1,16 @@ -export function renderEmailVerificationEmail(params: { +import { button, email, mjml, paragraph } from './components'; + +export function renderEmailVerificationEmail(input: { subject: string; verificationLink: string; toEmail: string; }) { - return ` - - - - - - - - ${params.subject} - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
- - -
-
- -

- Please verify your email address for Hive - by clicking the button below.

- - -
-
-

- Alternatively, you can directly paste this link - in your browser
- ${params.verificationLink} -

-
-
- - - - -
-
- -
- - - - - -
- - - - - - -
- - -

- This email is meant for ${params.toEmail} -

-
-
- -
- -
-
- - - - -`; + return email({ + title: `Verify Your Email Address`, + body: mjml` + ${paragraph(`To complete your sign-up, please verify your email address by clicking the link below:`)} + ${button({ url: input.verificationLink, text: 'Verify Email' })} + ${paragraph(`If you didn't sign up, you can ignore this email.`)} + `, + }); } diff --git a/packages/services/emails/src/templates/organization-invitation.ts b/packages/services/emails/src/templates/organization-invitation.ts index 88ec0900c2..5f89fc9dd4 100644 --- a/packages/services/emails/src/templates/organization-invitation.ts +++ b/packages/services/emails/src/templates/organization-invitation.ts @@ -1,20 +1,11 @@ -import { mjml } from '../mjml'; +import { button, email, mjml, paragraph } from './components'; export function renderOrganizationInvitation(input: { organizationName: string; link: string }) { - return mjml` - - - - - - - Someone from ${input.organizationName} invited you to join GraphQL Hive. - . - - Accept the invitation - - - - - `.content; + return email({ + title: `Join ${input.organizationName}`, + body: mjml` + ${paragraph(mjml`You've been invited to join ${input.organizationName} on GraphQL Hive.`)} + ${button({ url: input.link, text: 'Accept the invitation' })} + `, + }); } diff --git a/packages/services/emails/src/templates/organization-ownership-transfer.ts b/packages/services/emails/src/templates/organization-ownership-transfer.ts index 556b5b52c5..3cc836cc4e 100644 --- a/packages/services/emails/src/templates/organization-ownership-transfer.ts +++ b/packages/services/emails/src/templates/organization-ownership-transfer.ts @@ -1,29 +1,18 @@ -import { mjml } from '../mjml'; +import { button, email, mjml, paragraph } from './components'; export function renderOrganizationOwnershipTransferEmail(input: { authorName: string; organizationName: string; link: string; }) { - return mjml` - - - - - - - - ${input.authorName} wants to transfer the ownership of the ${input.organizationName} organization. - - - Accept the transfer - - - This link will expire in a day. - - - - - - `.content; + return email({ + title: 'Organization Ownership Transfer Initiated', + body: mjml` + ${paragraph( + mjml`${input.authorName} wants to transfer the ownership of the ${input.organizationName} organization.`, + )} + ${button({ url: input.link, text: 'Accept the transfer' })} + ${paragraph(`This link will expire in a day.`)} + `, + }); } diff --git a/packages/services/emails/src/templates/password-reset.ts b/packages/services/emails/src/templates/password-reset.ts index 6db6316ca9..4f7b2c8423 100644 --- a/packages/services/emails/src/templates/password-reset.ts +++ b/packages/services/emails/src/templates/password-reset.ts @@ -1,921 +1,16 @@ -export function renderPasswordResetEmail(params: { +import { button, email, mjml, paragraph } from './components'; + +export function renderPasswordResetEmail(input: { subject: string; passwordResetLink: string; toEmail: string; }) { - return ` - - - - - - - - ${params.subject} - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
- - -
-
- -

- A password reset request for your account on - Hive has been received. -

- - -
-
-

- Alternatively, you can directly paste this link - in your browser
- ${params.passwordResetLink} -

-
-
- - - - -
- - - - - - -
-

- This email is meant for ${params.toEmail} -

-
-
- -
- - - - - -
- -
- -
-
- - - - -`; + return email({ + title: `Reset Your Password`, + body: mjml` + ${paragraph(`We received a request to reset your password. Click the link below to set a new password:`)} + ${button({ url: input.passwordResetLink, text: 'Reset your password' })} + ${paragraph(`If you didn't request a password reset, you can ignore this email.`)} + `, + }); } diff --git a/packages/services/emails/src/templates/rate-limit-exceeded.ts b/packages/services/emails/src/templates/rate-limit-exceeded.ts index edaae6eb87..122ae81216 100644 --- a/packages/services/emails/src/templates/rate-limit-exceeded.ts +++ b/packages/services/emails/src/templates/rate-limit-exceeded.ts @@ -1,4 +1,4 @@ -import { mjml } from '../mjml'; +import { button, email, mjml, paragraph } from './components'; const numberFormatter = new Intl.NumberFormat(); @@ -8,30 +8,18 @@ export function renderRateLimitExceededEmail(input: { currentUsage: number; subscriptionManagementLink: string; }) { - return mjml` - - - - - - - - Your Hive organization ${ - input.organizationName - } has reached over 100% of the operations limit quota. - Used ${numberFormatter.format(input.currentUsage)} of ${numberFormatter.format( - input.limit, - )}. - . - - We recommend to increase the limit. - - - Manage your subscription - - - - - - `.content; + return email({ + title: 'Rate Limit Reached', + body: mjml` + ${paragraph( + mjml`Your Hive organization ${ + input.organizationName + } has reached over 100% of the operations limit quota.. Used ${numberFormatter.format(input.currentUsage)} of ${numberFormatter.format( + input.limit, + )}.`, + )} + ${paragraph(`We recommend to increase the limit.`)} + ${button({ url: input.subscriptionManagementLink, text: 'Manage your subscription' })} + `, + }); } diff --git a/packages/services/emails/src/templates/rate-limit-warning.ts b/packages/services/emails/src/templates/rate-limit-warning.ts index d5adf6d0b2..d1fd137ffd 100644 --- a/packages/services/emails/src/templates/rate-limit-warning.ts +++ b/packages/services/emails/src/templates/rate-limit-warning.ts @@ -1,4 +1,4 @@ -import { mjml } from '../mjml'; +import { button, email, mjml, paragraph } from './components'; const numberFormatter = new Intl.NumberFormat(); @@ -8,30 +8,18 @@ export function renderRateLimitWarningEmail(input: { currentUsage: number; subscriptionManagementLink: string; }) { - return mjml` - - - - - - - - Your Hive organization ${ - input.organizationName - } is approaching its operations limit quota. - Used ${numberFormatter.format(input.currentUsage)} of ${numberFormatter.format( - input.limit, - )}. - . - - We recommend to increase the limit. - - - Manage your subscription - - - - - - `.content; + return email({ + title: 'Approaching Rate Limit', + body: mjml` + ${paragraph( + mjml`Your Hive organization ${ + input.organizationName + } is approaching its operations limit quota. Used ${numberFormatter.format(input.currentUsage)} of ${numberFormatter.format( + input.limit, + )}.`, + )} + ${paragraph(`We recommend to increase the limit.`)} + ${button({ url: input.subscriptionManagementLink, text: 'Manage your subscription' })} + `, + }); }