mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-17 00:15:28 +02:00
✨ Send recovery url to users with expired checkout sessions (#1555)
This commit is contained in:
parent
5437b91c10
commit
9fdd5f3ea3
13 changed files with 341 additions and 23 deletions
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
12
packages/emails/src/previews/abandoned-checkout.tsx
Normal file
12
packages/emails/src/previews/abandoned-checkout.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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[];
|
||||
|
|
140
packages/emails/src/templates/abandoned-checkout.tsx
Normal file
140
packages/emails/src/templates/abandoned-checkout.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue