Send recovery url to users with expired checkout sessions (#1555)

This commit is contained in:
Luke Vella 2025-02-10 13:15:59 +07:00 committed by GitHub
parent 5437b91c10
commit 9fdd5f3ea3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 341 additions and 23 deletions

View file

@ -7,7 +7,7 @@ i18nInstance.init({
});
export const previewEmailContext: EmailContext = {
logoUrl: "https://rallly-public.s3.amazonaws.com/images/rallly-logo-mark.png",
logoUrl: "https://d39ixtfgglw55o.cloudfront.net/images/rallly-logo-mark.png",
baseUrl: "https://rallly.co",
domain: "rallly.co",
supportEmail: "support@rallly.co",

View file

@ -15,6 +15,7 @@ import { darkTextColor, fontFamily, Link, Text } from "./styled-components";
export interface EmailLayoutProps {
preview: string;
ctx: EmailContext;
poweredBy?: boolean;
}
const containerStyles = {
@ -30,6 +31,7 @@ export const EmailLayout = ({
preview,
children,
ctx,
poweredBy = true,
}: React.PropsWithChildren<EmailLayoutProps>) => {
const { logoUrl } = ctx;
return (
@ -48,23 +50,25 @@ export const EmailLayout = ({
alt="Rallly Logo"
/>
{children}
<Section style={{ marginTop: 32 }}>
<Text light={true}>
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="common_poweredBy"
ns="emails"
defaults="Powered by <a>{{domain}}</a>"
values={{ domain: "rallly.co" }}
components={{
a: (
<Link href="https://rallly.co?utm_source=email&utm_medium=transactional" />
),
}}
/>
</Text>
</Section>
{poweredBy ? (
<Section>
<Text light={true}>
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="common_poweredBy"
ns="emails"
defaults="Powered by <a>{{domain}}</a>"
values={{ domain: "rallly.co" }}
components={{
a: (
<Link href="https://rallly.co?utm_source=email&utm_medium=transactional" />
),
}}
/>
</Text>
</Section>
) : null}
</Container>
</Body>
</Html>

View file

@ -16,6 +16,7 @@ import type { EmailContext } from "../types";
export const lightTextColor = "#4B5563";
export const darkTextColor = "#1F2937";
export const borderColor = "#E2E8F0";
export const Text = (
props: TextProps & { light?: boolean; small?: boolean },
) => {
@ -48,14 +49,15 @@ export const Button = (props: React.ComponentProps<typeof UnstyledButton>) => {
style={{
backgroundColor: "#4F46E5",
borderRadius: "4px",
padding: "12px 14px",
padding: "14px",
fontFamily,
boxSizing: "border-box",
display: "block",
width: "100%",
maxWidth: "100%",
textAlign: "center",
fontSize: "16px",
fontSize: "14px",
fontWeight: "bold",
color: "white",
}}
/>
@ -150,6 +152,36 @@ export const Card = (props: SectionProps) => {
);
};
export const Signature = () => {
return (
<Section>
<UnstyledText
style={{
fontSize: 16,
margin: 0,
fontWeight: "bold",
color: darkTextColor,
fontFamily,
}}
>
Luke Vella
</UnstyledText>
<UnstyledText
style={{ fontSize: 16, margin: 0, color: lightTextColor, fontFamily }}
>
Founder
</UnstyledText>
<img
src="https://d39ixtfgglw55o.cloudfront.net/images/luke.jpg"
alt="Luke Vella"
style={{ borderRadius: "50%", marginTop: 16 }}
width={48}
height={48}
/>
</Section>
);
};
export const trackingWide = {
letterSpacing: 2,
};

View file

@ -0,0 +1,12 @@
import { previewEmailContext } from "../components/email-context";
import { AbandonedCheckoutEmail } from "../templates/abandoned-checkout";
export default function AbandonedCheckoutEmailPreview() {
return (
<AbandonedCheckoutEmail
ctx={previewEmailContext}
recoveryUrl="https://example.com"
name="John Doe"
/>
);
}

View file

@ -12,6 +12,10 @@ import type { TemplateComponent, TemplateName, TemplateProps } from "./types";
type SendEmailOptions<T extends TemplateName> = {
to: string;
from?: {
name: string;
address: string;
};
props: TemplateProps<T>;
attachments?: Mail.Options["attachments"];
};
@ -106,7 +110,7 @@ export class EmailClient {
try {
await this.sendEmail({
from: this.config.mail.from,
from: options.from || this.config.mail.from,
to: options.to,
subject,
html,

View file

@ -1,3 +1,4 @@
import { AbandonedCheckoutEmail } from "./templates/abandoned-checkout";
import { ChangeEmailRequest } from "./templates/change-email-request";
import { FinalizeHostEmail } from "./templates/finalized-host";
import { FinalizeParticipantEmail } from "./templates/finalized-participant";
@ -19,6 +20,7 @@ const templates = {
NewPollEmail,
RegisterEmail,
ChangeEmailRequest,
AbandonedCheckoutEmail,
};
export const emailTemplates = Object.keys(templates) as TemplateName[];

View file

@ -0,0 +1,140 @@
import { Section } from "@react-email/components";
import { Trans } from "react-i18next/TransWithoutContext";
import { EmailLayout } from "../components/email-layout";
import { Button, Card, Signature, Text } from "../components/styled-components";
import type { EmailContext } from "../types";
interface AbandonedCheckoutEmailProps {
recoveryUrl: string;
name?: string;
ctx: EmailContext;
}
export const AbandonedCheckoutEmail = ({
recoveryUrl,
name,
ctx,
}: AbandonedCheckoutEmailProps) => {
return (
<EmailLayout
ctx={ctx}
poweredBy={false}
preview={ctx.t("abandoned_checkout_preview", {
defaultValue:
"Exclusive offer: Get 20% off your first year of Rallly Pro!",
ns: "emails",
})}
>
{name ? (
<Text>
<Trans
t={ctx.t}
i18n={ctx.i18n}
i18nKey="abandoned_checkout_name"
defaults="Hey {{name}},"
values={{ name }}
ns="emails"
/>
</Text>
) : (
<Text>
<Trans
t={ctx.t}
i18n={ctx.i18n}
i18nKey="abandoned_checkout_noname"
defaults="Hey there,"
ns="emails"
/>
</Text>
)}
<Text>
<Trans
t={ctx.t}
i18n={ctx.i18n}
i18nKey="abandoned_checkout_content"
defaults="I noticed you were exploring <b>Rallly Pro</b> and wanted to personally reach out. I'd love to hear what features caught your interest and answer any questions you might have."
ns="emails"
components={{
b: <b />,
}}
/>
</Text>
<Text>
<Trans
t={ctx.t}
i18n={ctx.i18n}
i18nKey="abandoned_checkout_offer"
defaults="To help you get started, I'd like to offer you <b>{{discount}}% off your first year</b> with Rallly Pro. Simply use this code during checkout:"
ns="emails"
values={{
discount: 20,
}}
components={{
b: <b />,
}}
/>
</Text>
<Section style={{ marginTop: 16, marginBottom: 16 }}>
<Card>
<Text
style={{
textAlign: "center",
fontFamily: "monospace",
fontWeight: "bold",
}}
>
GETPRO1Y20
</Text>
</Card>
<Button href={recoveryUrl} id="recoveryUrl">
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="abandoned_checkout_button"
defaults="Upgrade to Rallly Pro"
ns="emails"
/>
</Button>
</Section>
<Section>
<Text>
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="abandoned_checkout_support"
defaults="Have questions or need assistance? Just reply to this email."
ns="emails"
/>
</Text>
</Section>
<Section>
<Text>
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="abandoned_checkout_signoff"
defaults="Best regards,"
ns="emails"
/>
</Text>
</Section>
<Signature />
</EmailLayout>
);
};
AbandonedCheckoutEmail.getSubject = (
props: AbandonedCheckoutEmailProps,
ctx: EmailContext,
) => {
return (
"🎉 " +
ctx.t("abandoned_checkout_subject", {
defaultValue: "Get 20% off your first year of Rallly Pro",
ns: "emails",
})
);
};
export default AbandonedCheckoutEmail;