diff --git a/apps/api/src/mail/emails/components/base-email-template.tsx b/apps/api/src/mail/emails/components/base-email-template.tsx new file mode 100644 index 00000000..891ddfe0 --- /dev/null +++ b/apps/api/src/mail/emails/components/base-email-template.tsx @@ -0,0 +1,84 @@ +import * as React from 'react' +import { + Body, + Container, + Head, + Heading, + Html, + Link, + Preview, + Section, + Text +} from '@react-email/components' +import { + container, + content, + footer, + footerText, + h1, + link, + main, + text +} from '../styles/common-styles' + +interface BaseEmailTemplateProps { + previewText: string + heading: string + children: React.ReactNode +} + +export const BaseEmailTemplate: React.FC = ({ + previewText, + heading, + children +}) => { + return ( + + + {previewText} + + +
+ {heading} + {children} + + If you believe this action was taken in error or have any + questions regarding this change, please contact your project + administrator or our support team. + + + We appreciate your understanding and thank you for your + contributions to the project. + + + Cheers, +
+ Team Keyshade +
+
+
+ + This is an automated message. Please do not reply to this email. + + + Read our{' '} + + Privacy Policy + {' '} + and{' '} + + Terms and Conditions + {' '} + for more information on how we manage your data and services. + +
+
+ + + ) +} + +export default BaseEmailTemplate \ No newline at end of file diff --git a/apps/api/src/mail/emails/styles/common-styles.ts b/apps/api/src/mail/emails/styles/common-styles.ts new file mode 100644 index 00000000..027deedb --- /dev/null +++ b/apps/api/src/mail/emails/styles/common-styles.ts @@ -0,0 +1,78 @@ +import { CSSProperties } from 'react' + +export const main: CSSProperties = { + fontFamily: "'Segoe UI', 'Roboto', sans-serif", + lineHeight: '1.6', + color: '#04050a', + backgroundColor: '#fafafa', + margin: '0', + padding: '20px' +} + +export const container: CSSProperties = { + maxWidth: '600px', + margin: '0 auto', + backgroundColor: '#fff', + borderRadius: '5px', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)' +} + +export const content: CSSProperties = { + padding: '20px 40px' +} + +export const h1: CSSProperties = { + color: '#000', + marginBottom: '20px', + fontSize: '24px', + fontWeight: '600' +} + +export const text: CSSProperties = { + marginBottom: '5px', + color: '#666' +} + +export const workspaceDetails: CSSProperties = { + width: '100%', + backgroundColor: '#fafafa', + borderRadius: '5px', + margin: '20px 0px', + padding: '10px 20px' +} + +export const workspaceInfo: CSSProperties = { + margin: '7px 0px' +} + +export const ctaButton: CSSProperties = { + width: '100px', + color: '#ffffff', + fontSize: '14px', + fontWeight: '500', + textAlign: 'center', + marginTop: '10px', + cursor: 'pointer', + display: 'inline-block', + backgroundColor: '#000', + textDecoration: 'none', + padding: '10px 22px', + borderRadius: '5px' +} + +export const footer: CSSProperties = { + borderTop: '1px solid #eaeaea', + padding: '20px' +} + +export const footerText: CSSProperties = { + fontSize: '12px', + color: '#999', + textAlign: 'center' as const, + margin: '0' +} + +export const link: CSSProperties = { + color: '#000', + textDecoration: 'underline' +} diff --git a/apps/api/src/mail/emails/workspace-invitation.tsx b/apps/api/src/mail/emails/workspace-invitation.tsx new file mode 100644 index 00000000..37b4fc0f --- /dev/null +++ b/apps/api/src/mail/emails/workspace-invitation.tsx @@ -0,0 +1,67 @@ +import * as React from 'react' +import { Button, Section, Text } from '@react-email/components' +import dayjs from 'dayjs' +import { + ctaButton, + text, + workspaceDetails, + workspaceInfo +} from './styles/common-styles' +import BaseEmailTemplate from './components/base-email-template' + +interface WorkspaceInvitationEmailProps { + workspaceName: string + actionUrl: string + invitedBy: string + invitedOn: string + forRegisteredUser: boolean +} + +export const WorkspaceInvitationEmail = ({ + workspaceName, + actionUrl, + invitedBy, + invitedOn, + forRegisteredUser +}: WorkspaceInvitationEmailProps) => { + const formattedInvitedOnDate = dayjs(invitedOn).format( + 'ddd, MMM D, YYYY h:mm A' + ) + + const previewText = forRegisteredUser + ? 'Welcome Back! Join Your Workspace' + : 'You are Invited to Join the Workspace' + + return ( + + Dear User, + + We're excited to inform you that you've been invited to join a + workspace on Keyshade. Here are the details of your invitation: + +
+ + Workspace Name: {workspaceName} + + + Invited By: {invitedBy} + + + Invited On: {formattedInvitedOnDate} + +
+ + Join the project by clicking the button below - we're excited to + have you! + + +
+ ) +} + +export default WorkspaceInvitationEmail \ No newline at end of file diff --git a/apps/api/src/mail/emails/workspace-removal.tsx b/apps/api/src/mail/emails/workspace-removal.tsx index da1ed0aa..29a28693 100644 --- a/apps/api/src/mail/emails/workspace-removal.tsx +++ b/apps/api/src/mail/emails/workspace-removal.tsx @@ -1,16 +1,8 @@ import * as React from 'react' -import { - Body, - Container, - Head, - Heading, - Html, - Link, - Preview, - Section, - Text -} from '@react-email/components' +import { Text, Section } from '@react-email/components' import dayjs from 'dayjs' +import { text, workspaceDetails, workspaceInfo } from './styles/common-styles' +import BaseEmailTemplate from './components/base-email-template' interface WorkspaceRemovalEmailProps { workspaceName: string @@ -26,126 +18,25 @@ export const RemovedFromWorkspaceEmail = ({ ) return ( - - - Removal from Workspace - - -
- Removal from Workspace - Dear User, - - We hope this email finds you well. We are writing to inform you - that your access to the following workspace has been removed: - -
- - Workspace Name: {workspaceName} - - - Removed On: {formattedRemovedOnDate} - -
- - If you believe this action was taken in error or have any - questions regarding this change, please contact your project - administrator or our support team. - - - We appreciate your understanding and thank you for your - contributions to the project. - - - Cheers, -
- Team Keyshade -
-
-
- - This is an automated message. Please do not reply to this email. - - - Read our{' '} - - Privacy Policy - {' '} - and{' '} - - Terms and Conditions - {' '} - for more information on how we manage your data and services. - -
-
- - + + Dear User, + + We hope this email finds you well. We are writing to inform you + that your access to the following workspace has been removed: + +
+ + Workspace Name: {workspaceName} + + + Removed On: {formattedRemovedOnDate} + +
+
) } -export default RemovedFromWorkspaceEmail - -const main = { - fontFamily: "'Segoe UI', 'Roboto', sans-serif", - lineHeight: '1.6', - color: '#04050a', - backgroundColor: '#fafafa', - margin: '0', - padding: '20px' -} - -const container = { - maxWidth: '600px', - margin: '0 auto', - backgroundColor: '#fff', - borderRadius: '5px', - boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)' -} - -const content = { - padding: '20px 40px' -} - -const h1 = { - color: '#000', - marginBottom: '20px', - fontSize: '24px', - fontWeight: '600' -} - -const text = { - marginBottom: '5px', - color: '#666' -} - -const workspaceDetails = { - width: '100%', - backgroundColor: '#fafafa', - borderRadius: '5px', - margin: '20px 0px', - padding: '10px 20px' -} - -const workspaceInfo = { - margin: '7px 0px' -} - -const footer = { - borderTop: '1px solid #eaeaea', - padding: '20px' -} - -const footerText = { - fontSize: '12px', - color: '#999', - textAlign: 'center' as const, - margin: '0' -} - -const link = { - color: '#000', - textDecoration: 'underline' -} +export default RemovedFromWorkspaceEmail \ No newline at end of file diff --git a/apps/api/src/mail/services/interface.service.ts b/apps/api/src/mail/services/interface.service.ts index 539a8b40..a600be93 100644 --- a/apps/api/src/mail/services/interface.service.ts +++ b/apps/api/src/mail/services/interface.service.ts @@ -5,11 +5,12 @@ export interface IMailService { sendEmailChangedOtp(email: string, otp: string): Promise - workspaceInvitationMailForUsers( + invitedToWorkspace( email: string, - workspace: string, - actionUrl: string, + workspaceName: string, + projectUrl: string, invitedBy: string, + invitedOn: string, forRegisteredUser: boolean ): Promise diff --git a/apps/api/src/mail/services/mail.service.ts b/apps/api/src/mail/services/mail.service.ts index dc5f4dfd..3a7dca54 100644 --- a/apps/api/src/mail/services/mail.service.ts +++ b/apps/api/src/mail/services/mail.service.ts @@ -7,6 +7,7 @@ import { IMailService } from './interface.service' import { Transporter, createTransport } from 'nodemailer' import RemovedFromWorkspaceEmail from '../emails/workspace-removal' import { render } from '@react-email/render' +import WorkspaceInvitationEmail from '../emails/workspace-invitation' @Injectable() export class MailService implements IMailService { @@ -24,33 +25,27 @@ export class MailService implements IMailService { } }) } - async workspaceInvitationMailForUsers( + async invitedToWorkspace( email: string, - workspace: string, + workspaceName: string, actionUrl: string, - invitee: string, + invitedBy: string, + invitedOn: string, forRegisteredUser: boolean ): Promise { - const subject = `You have been invited to a ${workspace}` - const intro = forRegisteredUser - ? `Hello again! You've been invited to join a new workspace.` - : `Hello there! We're excited to welcome you to Keyshade.` - const body = ` - - - Workspace Invitation - - -

Welcome to keyshade!

-

${intro}

-

You have been invited to join the workspace ${workspace} by ${invitee}.

-

Please click on the link below to accept the invitation.

-

Accept Invitation

-

Thank you for choosing us.

-

Best Regards,

-

keyshade Team

- - ` + const subject = forRegisteredUser + ? 'Welcome Back! Join Your Workspace' + : 'You are Invited to Join the Workspace' + + const body = await render( + WorkspaceInvitationEmail({ + workspaceName, + actionUrl, + invitedBy, + invitedOn, + forRegisteredUser + }) + ) await this.sendEmail(email, subject, body) } diff --git a/apps/api/src/mail/services/mock.service.ts b/apps/api/src/mail/services/mock.service.ts index 2a07f1e8..3513313e 100644 --- a/apps/api/src/mail/services/mock.service.ts +++ b/apps/api/src/mail/services/mock.service.ts @@ -5,17 +5,18 @@ import { IMailService } from './interface.service' export class MockMailService implements IMailService { private readonly log = new Logger(MockMailService.name) - async workspaceInvitationMailForUsers( + async invitedToWorkspace( email: string, - workspace: string, + workspaceName: string, actionUrl: string, - invitee: string, + invitedBy: string, + invitedOn: string, forRegisteredUser: boolean ): Promise { this.log.log( forRegisteredUser - ? `Workspace Invitation Mail for Registered User: ${email}, ${workspace}, ${actionUrl}, ${invitee}` - : `Workspace Invitation Mail for Non Registered User: ${email}, ${workspace}, ${actionUrl}, ${invitee}` + ? `User ${email} has been invited to the workspace ${workspaceName} by ${invitedBy} on ${invitedOn}. Invitation details can be accessed at ${actionUrl}.` + : `User ${email} has been invited to the workspace ${workspaceName} by ${invitedBy} on ${invitedOn}. Since the user is not registered, they can sign up and access the invitation details at ${actionUrl}.` ) } diff --git a/apps/api/src/workspace-membership/service/workspace-membership.service.ts b/apps/api/src/workspace-membership/service/workspace-membership.service.ts index e9f07e3d..d10032c5 100644 --- a/apps/api/src/workspace-membership/service/workspace-membership.service.ts +++ b/apps/api/src/workspace-membership/service/workspace-membership.service.ts @@ -899,11 +899,12 @@ export class WorkspaceMembershipService { if (memberUser) { await this.prisma.$transaction([createMembership]) - this.mailService.workspaceInvitationMailForUsers( + this.mailService.invitedToWorkspace( member.email, workspace.name, `${process.env.WORKSPACE_FRONTEND_URL}/workspace/${workspace.slug}/join`, currentUser.name, + new Date().toISOString(), true ) @@ -925,7 +926,7 @@ export class WorkspaceMembershipService { this.log.debug(`Created non-registered user ${memberUser}`) - this.mailService.workspaceInvitationMailForUsers( + this.mailService.invitedToWorkspace( member.email, workspace.name, `${process.env.WORKSPACE_FRONTEND_URL}/workspace/${ @@ -934,6 +935,7 @@ export class WorkspaceMembershipService { id: userId })}`, currentUser.name, + new Date().toISOString(), false )