have footer on emails (#11300)

# ISSUE 

- Closes #9622

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Nabhag Motivaras
2025-04-03 17:56:19 +05:30
committed by GitHub
parent cfae440a02
commit 7eec64b6e0
96 changed files with 9359 additions and 2120 deletions

View File

@ -1,4 +1,5 @@
import { Trans } from '@lingui/react/macro';
import { i18n } from '@lingui/core';
import { Trans } from '@lingui/react';
import { BaseEmail } from 'src/components/BaseEmail';
import { CallToAction } from 'src/components/CallToAction';
import { MainText } from 'src/components/MainText';
@ -20,32 +21,40 @@ export const CleanSuspendedWorkspaceEmail = ({
}: CleanSuspendedWorkspaceEmailProps) => {
return (
<BaseEmail width={333} locale={locale}>
<Title value={<Trans>Deleted Workspace</Trans>} />
<Title value={i18n._('Deleted Workspace')} />
<MainText>
{userName?.length > 1 ? (
<Trans>Dear {userName},</Trans>
<Trans id="Dear {userName}," values={{ userName }} />
) : (
<Trans>Hello,</Trans>
<Trans id="Hello," />
)}
<br />
<br />
<Trans>
Your workspace <b>{workspaceDisplayName}</b> has been deleted as your
subscription expired {daysSinceInactive} days ago.
</Trans>
<Trans
id="Your workspace <0>{workspaceDisplayName}</0> has been deleted as your subscription expired {daysSinceInactive} days ago."
values={{ workspaceDisplayName, daysSinceInactive }}
components={{ 0: <b /> }}
/>
<br />
<br />
<Trans>All data in this workspace has been permanently deleted.</Trans>
<Trans id="All data in this workspace has been permanently deleted." />
<br />
<br />
<Trans>
If you wish to use Twenty again, you can create a new workspace.
</Trans>
<Trans id="If you wish to use Twenty again, you can create a new workspace." />
</MainText>
<CallToAction
href="https://app.twenty.com/"
value={<Trans>Create a new workspace</Trans>}
value={i18n._('Create a new workspace')}
/>
</BaseEmail>
);
};
CleanSuspendedWorkspaceEmail.PreviewProps = {
daysSinceInactive: 1,
userName: 'John Doe',
workspaceDisplayName: 'My Workspace',
locale: 'en',
} as CleanSuspendedWorkspaceEmailProps;
export default CleanSuspendedWorkspaceEmail;

View File

@ -1,4 +1,5 @@
import { Trans } from '@lingui/react/macro';
import { i18n } from '@lingui/core';
import { Trans } from '@lingui/react';
import { BaseEmail } from 'src/components/BaseEmail';
import { CallToAction } from 'src/components/CallToAction';
import { Link } from 'src/components/Link';
@ -19,16 +20,24 @@ export const PasswordResetLinkEmail = ({
}: PasswordResetLinkEmailProps) => {
return (
<BaseEmail locale={locale}>
<Title value={<Trans>Reset your password 🗝</Trans>} />
<CallToAction href={link} value={<Trans>Reset</Trans>} />
<Title value={i18n._('Reset your password 🗝')} />
<CallToAction href={link} value={i18n._('Reset')} />
<MainText>
<Trans>
This link is only valid for the next {duration}. If the link does not
work, you can use the login verification link directly:
</Trans>
<Trans
id="This link is only valid for the next {duration}. If the link does not work, you can use the login verification link directly:"
values={{ duration }}
/>
<br />
<Link href={link} value={link} />
</MainText>
</BaseEmail>
);
};
PasswordResetLinkEmail.PreviewProps = {
duration: '24 hours',
link: 'https://app.twenty.com/reset-password/123',
locale: 'en',
} as PasswordResetLinkEmailProps;
export default PasswordResetLinkEmail;

View File

@ -1,7 +1,5 @@
import { i18n } from '@lingui/core';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { Trans } from '@lingui/react';
import { BaseEmail } from 'src/components/BaseEmail';
import { CallToAction } from 'src/components/CallToAction';
import { MainText } from 'src/components/MainText';
@ -21,29 +19,38 @@ export const PasswordUpdateNotifyEmail = ({
link,
locale,
}: PasswordUpdateNotifyEmailProps) => {
const helloString = userName?.length > 1 ? t`Dear ${userName}` : t`Hello`;
const formattedDate = i18n.date(new Date());
return (
<BaseEmail locale={locale}>
<Title value={<Trans>Password updated</Trans>} />
<Title value={i18n._('Password updated')} />
<MainText>
{helloString},
{userName?.length > 1 ? (
<Trans id="Dear {userName}," values={{ userName }} />
) : (
<Trans id="Hello," />
)}
<br />
<br />
<Trans>
This is a confirmation that password for your account ({email}) was
successfully changed on {formattedDate}.
</Trans>
<Trans
id="This is a confirmation that password for your account ({email}) was successfully changed on {formattedDate}."
values={{ email, formattedDate }}
/>
<br />
<br />
<Trans>
If you did not initiate this change, please contact your workspace
owner immediately.
</Trans>
<Trans id="If you did not initiate this change, please contact your workspace owner immediately." />
<br />
</MainText>
<CallToAction value={<Trans>Connect to Twenty</Trans>} href={link} />
<CallToAction value={i18n._('Connect to Twenty')} href={link} />
</BaseEmail>
);
};
PasswordUpdateNotifyEmail.PreviewProps = {
userName: 'John Doe',
email: 'john.doe@example.com',
link: 'https://app.twenty.com',
locale: 'en',
} as PasswordUpdateNotifyEmailProps;
export default PasswordUpdateNotifyEmail;

View File

@ -1,8 +1,7 @@
import { Trans } from '@lingui/react/macro';
import { i18n } from '@lingui/core';
import { Trans } from '@lingui/react';
import { BaseEmail } from 'src/components/BaseEmail';
import { CallToAction } from 'src/components/CallToAction';
import { Footer } from 'src/components/Footer';
import { MainText } from 'src/components/MainText';
import { Title } from 'src/components/Title';
import { APP_LOCALES } from 'twenty-shared/translations';
@ -18,18 +17,20 @@ export const SendEmailVerificationLinkEmail = ({
}: SendEmailVerificationLinkEmailProps) => {
return (
<BaseEmail width={333} locale={locale}>
<Title value={<Trans>Confirm your email address</Trans>} />
<CallToAction href={link} value={<Trans>Verify Email</Trans>} />
<Title value={i18n._('Confirm your email address')} />
<CallToAction href={link} value={i18n._('Verify Email')} />
<br />
<br />
<MainText>
<Trans>
Thanks for registering for an account on Twenty! Before we get
started, we just need to confirm that this is you. Click above to
verify your email address.
</Trans>
<Trans id="Thanks for registering for an account on Twenty! Before we get started, we just need to confirm that this is you. Click above to verify your email address." />
</MainText>
<Footer />
</BaseEmail>
);
};
SendEmailVerificationLinkEmail.PreviewProps = {
link: 'https://app.twenty.com/verify-email/123',
locale: 'en',
};
export default SendEmailVerificationLinkEmail;

View File

@ -1,4 +1,5 @@
import { Trans } from '@lingui/react/macro';
import { i18n } from '@lingui/core';
import { Trans } from '@lingui/react';
import { Img } from '@react-email/components';
import { emailTheme } from 'src/common-style';
@ -37,26 +38,58 @@ export const SendInviteLinkEmail = ({
? getImageAbsoluteURI({ imageUrl: workspace.logo, baseUrl: serverUrl })
: null;
const senderName = capitalize(sender.firstName);
const senderEmail = sender.email;
const workspaceName = workspace.name;
return (
<BaseEmail width={333} locale={locale}>
<Title value={<Trans>Join your team on Twenty</Trans>} />
<Title value={i18n._('Join your team on Twenty')} />
<MainText>
{capitalize(sender.firstName)} (
<Link
href={`mailto:${sender.email}`}
value={sender.email}
color={emailTheme.font.colors.blue}
<Trans
id="{senderName} (<0>{senderEmail}</0>) has invited you to join a workspace called <1>{workspaceName}</1>."
values={{ senderName, senderEmail, workspaceName }}
components={{
0: (
<Link
href={`mailto:${senderEmail}`}
value={senderEmail}
color={emailTheme.font.colors.blue}
/>
),
1: <b />,
}}
/>
) <Trans>has invited you to join a workspace called </Trans>
<b>{workspace.name}</b>
<br />
</MainText>
<HighlightedContainer>
{workspaceLogo && <Img src={workspaceLogo} width={40} height={40} />}
{workspace.name && <HighlightedText value={workspace.name} />}
<CallToAction href={link} value={<Trans>Accept invite</Trans>} />
{workspaceLogo ? (
<Img
src={workspaceLogo}
width={40}
height={40}
alt="Workspace logo"
/>
) : (
<></>
)}
{workspace.name ? <HighlightedText value={workspace.name} /> : <></>}
<CallToAction href={link} value={i18n._('Accept invite')} />
</HighlightedContainer>
<WhatIsTwenty />
</BaseEmail>
);
};
SendInviteLinkEmail.PreviewProps = {
link: 'https://app.twenty.com/invite/123',
workspace: {
name: 'Acme Inc.',
logo: 'https://fakeimg.pl/200x200/?text=ACME&font=lobster',
},
sender: { email: 'john.doe@example.com', firstName: 'John', lastName: 'Doe' },
serverUrl: 'https://app.twenty.com',
locale: 'en',
} as SendInviteLinkEmailProps;
export default SendInviteLinkEmail;

View File

@ -0,0 +1,24 @@
import { i18n } from '@lingui/core';
import { BaseEmail } from 'src/components/BaseEmail';
import { Title } from 'src/components/Title';
import { APP_LOCALES } from 'twenty-shared/translations';
type TestEmailProps = {
locale: keyof typeof APP_LOCALES;
};
// This is a test email which isn't used in production
// It's useful to do tests and play in a local environment
export const TestEmail = ({ locale }: TestEmailProps) => {
return (
<BaseEmail locale={locale}>
<Title value={i18n._('Test email')} />
</BaseEmail>
);
};
TestEmail.PreviewProps = {
locale: 'en',
} as TestEmailProps;
export default TestEmail;

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { i18n } from '@lingui/core';
import { Trans } from '@lingui/react';
import { Img } from '@react-email/components';
import { emailTheme } from 'src/common-style';
@ -10,7 +10,6 @@ import { HighlightedText } from 'src/components/HighlightedText';
import { Link } from 'src/components/Link';
import { MainText } from 'src/components/MainText';
import { Title } from 'src/components/Title';
import { WhatIsTwenty } from 'src/components/WhatIsTwenty';
import { capitalize } from 'src/utils/capitalize';
import { APP_LOCALES } from 'twenty-shared/translations';
import { getImageAbsoluteURI } from 'twenty-shared/utils';
@ -40,30 +39,61 @@ export const SendApprovedAccessDomainValidation = ({
? getImageAbsoluteURI({ imageUrl: workspace.logo, baseUrl: serverUrl })
: null;
const senderName = capitalize(sender.firstName);
const senderEmail = sender.email;
return (
<BaseEmail width={333} locale={locale}>
<Title value={t`Validate domain`} />
<Title value={i18n._('Validate domain')} />
<MainText>
{capitalize(sender.firstName)} (
<Link
href={`mailto:${sender.email}`}
value={sender.email}
color={emailTheme.font.colors.blue}
<Trans
id="{senderName} (<0>{senderEmail}</0>): Please validate this domain to allow users with <1>@{domain}</1> email addresses to join your workspace without requiring an invitation."
values={{ senderName, senderEmail, domain }}
components={{
0: (
<Link
href={`mailto:${senderEmail}`}
value={senderEmail}
color={emailTheme.font.colors.blue}
/>
),
1: <b />,
}}
/>
) <Trans>Please validate this domain to allow users with</Trans>{' '}
<b>@{domain}</b>{' '}
<Trans>
email addresses to join your workspace without requiring an
invitation.
</Trans>
<br />
</MainText>
<HighlightedContainer>
{workspaceLogo && <Img src={workspaceLogo} width={40} height={40} />}
{workspace.name && <HighlightedText value={workspace.name} />}
<CallToAction href={link} value={t`Validate domain`} />
{workspaceLogo ? (
<Img
src={workspaceLogo}
width={40}
height={40}
alt="Workspace logo"
/>
) : (
<></>
)}
{workspace.name ? <HighlightedText value={workspace.name} /> : <></>}
<CallToAction href={link} value={i18n._('Validate domain')} />
</HighlightedContainer>
<WhatIsTwenty />
</BaseEmail>
);
};
SendApprovedAccessDomainValidation.PreviewProps = {
link: 'https://app.twenty.com/validate-domain',
domain: 'example.com',
workspace: {
name: 'Acme Inc.',
logo: 'https://fakeimg.pl/200x200/?text=ACME&font=lobster',
},
sender: {
email: 'john.doe@example.com',
firstName: 'John',
lastName: 'Doe',
},
serverUrl: 'https://app.twenty.com',
locale: 'en',
} as SendApprovedAccessDomainValidationProps;
export default SendApprovedAccessDomainValidation;

View File

@ -1,4 +1,5 @@
import { Trans } from '@lingui/react/macro';
import { i18n } from '@lingui/core';
import { Trans } from '@lingui/react';
import { BaseEmail } from 'src/components/BaseEmail';
import { CallToAction } from 'src/components/CallToAction';
import { MainText } from 'src/components/MainText';
@ -22,38 +23,51 @@ export const WarnSuspendedWorkspaceEmail = ({
}: WarnSuspendedWorkspaceEmailProps) => {
const daysLeft = inactiveDaysBeforeDelete - daysSinceInactive;
const dayOrDays = daysLeft > 1 ? 'days' : 'day';
const remainingDays = daysLeft > 0 ? `${daysLeft} ` : '';
const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello';
const remainingDays = daysLeft > 0 ? daysLeft : 0;
return (
<BaseEmail width={333} locale={locale}>
<Title value={<Trans>Suspended Workspace </Trans>} />
<Title value={i18n._('Suspended Workspace')} />
<MainText>
{helloString},
{userName?.length > 1 ? (
<Trans id="Dear {userName}," values={{ userName }} />
) : (
<Trans id="Hello," />
)}
<br />
<br />
<Trans>
It appears that your workspace <b>{workspaceDisplayName}</b> has been
suspended for {daysSinceInactive} days.
</Trans>
<Trans
id="It appears that your workspace <0>{workspaceDisplayName}</0> has been suspended for {daysSinceInactive} days."
values={{ workspaceDisplayName, daysSinceInactive }}
components={{ 0: <b /> }}
/>
<br />
<br />
<Trans>
The workspace will be deactivated in {remainingDays} {dayOrDays}, and
all its data will be deleted.
</Trans>
<Trans
id="The workspace will be deactivated in {remainingDays} {dayOrDays}, and all its data will be deleted."
values={{ remainingDays, dayOrDays }}
/>
<br />
<br />
<Trans>
If you wish to continue using Twenty, please update your subscription
within the next {remainingDays} {dayOrDays}.
</Trans>
<Trans
id="If you wish to continue using Twenty, please update your subscription within the next {remainingDays} {dayOrDays}."
values={{ remainingDays, dayOrDays }}
/>
</MainText>
<CallToAction
href="https://app.twenty.com/settings/billing"
value={<Trans>Update your subscription</Trans>}
value={i18n._('Update your subscription')}
/>
</BaseEmail>
);
};
WarnSuspendedWorkspaceEmail.PreviewProps = {
daysSinceInactive: 10,
inactiveDaysBeforeDelete: 14,
userName: 'John Doe',
workspaceDisplayName: 'Acme Inc.',
locale: 'en',
};
export default WarnSuspendedWorkspaceEmail;