improvement(emails): update unsub page, standardize unsub process (#2881)

This commit is contained in:
Waleed
2026-01-18 20:42:04 -08:00
committed by GitHub
parent 1dbf92db3f
commit b4c2294e67
23 changed files with 240 additions and 306 deletions

View File

@@ -34,7 +34,7 @@ export function OTPVerificationEmail({
const brand = getBrandConfig()
return (
<EmailLayout preview={getSubjectByType(type, brand.name, chatTitle)}>
<EmailLayout preview={getSubjectByType(type, brand.name, chatTitle)} showUnsubscribe={false}>
<Text style={baseStyles.paragraph}>Your verification code:</Text>
<Section style={baseStyles.codeContainer}>

View File

@@ -12,7 +12,7 @@ export function ResetPasswordEmail({ username = '', resetLink = '' }: ResetPassw
const brand = getBrandConfig()
return (
<EmailLayout preview={`Reset your ${brand.name} password`}>
<EmailLayout preview={`Reset your ${brand.name} password`} showUnsubscribe={false}>
<Text style={baseStyles.paragraph}>Hello {username},</Text>
<Text style={baseStyles.paragraph}>
A password reset was requested for your {brand.name} account. Click below to set a new

View File

@@ -13,7 +13,7 @@ export function WelcomeEmail({ userName }: WelcomeEmailProps) {
const baseUrl = getBaseUrl()
return (
<EmailLayout preview={`Welcome to ${brand.name}`}>
<EmailLayout preview={`Welcome to ${brand.name}`} showUnsubscribe={false}>
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>
{userName ? `Hey ${userName},` : 'Hey,'}
</Text>

View File

@@ -23,7 +23,7 @@ export function CreditPurchaseEmail({
const previewText = `${brand.name}: $${amount.toFixed(2)} in credits added to your account`
return (
<EmailLayout preview={previewText}>
<EmailLayout preview={previewText} showUnsubscribe={false}>
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>
{userName ? `Hi ${userName},` : 'Hi,'}
</Text>

View File

@@ -18,7 +18,10 @@ export function EnterpriseSubscriptionEmail({
const effectiveLoginLink = loginLink || `${baseUrl}/login`
return (
<EmailLayout preview={`Your Enterprise Plan is now active on ${brand.name}`}>
<EmailLayout
preview={`Your Enterprise Plan is now active on ${brand.name}`}
showUnsubscribe={false}
>
<Text style={baseStyles.paragraph}>Hello {userName},</Text>
<Text style={baseStyles.paragraph}>
Your <strong>Enterprise Plan</strong> is now active. You have full access to advanced

View File

@@ -31,7 +31,7 @@ export function FreeTierUpgradeEmail({
const previewText = `${brand.name}: You've used ${percentUsed}% of your free credits`
return (
<EmailLayout preview={previewText}>
<EmailLayout preview={previewText} showUnsubscribe={true}>
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>
{userName ? `Hi ${userName},` : 'Hi,'}
</Text>

View File

@@ -25,7 +25,7 @@ export function PaymentFailedEmail({
const previewText = `${brand.name}: Payment Failed - Action Required`
return (
<EmailLayout preview={previewText}>
<EmailLayout preview={previewText} showUnsubscribe={false}>
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>
{userName ? `Hi ${userName},` : 'Hi,'}
</Text>

View File

@@ -18,7 +18,7 @@ export function PlanWelcomeEmail({ planName, userName, loginLink }: PlanWelcomeE
const previewText = `${brand.name}: Your ${planName} plan is active`
return (
<EmailLayout preview={previewText}>
<EmailLayout preview={previewText} showUnsubscribe={true}>
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>
{userName ? `Hi ${userName},` : 'Hi,'}
</Text>

View File

@@ -25,7 +25,7 @@ export function UsageThresholdEmail({
const previewText = `${brand.name}: You're at ${percentUsed}% of your ${planName} monthly budget`
return (
<EmailLayout preview={previewText}>
<EmailLayout preview={previewText} showUnsubscribe={true}>
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>
{userName ? `Hi ${userName},` : 'Hi,'}
</Text>

View File

@@ -20,7 +20,10 @@ export function CareersConfirmationEmail({
const baseUrl = getBaseUrl()
return (
<EmailLayout preview={`Your application to ${brand.name} has been received`}>
<EmailLayout
preview={`Your application to ${brand.name} has been received`}
showUnsubscribe={false}
>
<Text style={baseStyles.paragraph}>Hello {name},</Text>
<Text style={baseStyles.paragraph}>
We've received your application for <strong>{position}</strong>. Our team reviews every

View File

@@ -40,7 +40,7 @@ export function CareersSubmissionEmail({
submittedDate = new Date(),
}: CareersSubmissionEmailProps) {
return (
<EmailLayout preview={`New Career Application from ${name}`} hideFooter>
<EmailLayout preview={`New Career Application from ${name}`} hideFooter showUnsubscribe={false}>
<Text
style={{
...baseStyles.paragraph,

View File

@@ -4,22 +4,29 @@ import { getBrandConfig } from '@/lib/branding/branding'
import { isHosted } from '@/lib/core/config/feature-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
interface UnsubscribeOptions {
unsubscribeToken?: string
email?: string
}
interface EmailFooterProps {
baseUrl?: string
unsubscribe?: UnsubscribeOptions
messageId?: string
/**
* Whether to show unsubscribe link. Defaults to true.
* Set to false for transactional emails where unsubscribe doesn't apply.
*/
showUnsubscribe?: boolean
}
/**
* Email footer component styled to match Stripe's email design.
* Sits in the gray area below the main white card.
*
* For non-transactional emails, the unsubscribe link uses placeholders
* {{UNSUBSCRIBE_TOKEN}} and {{UNSUBSCRIBE_EMAIL}} which are replaced
* by the mailer when sending.
*/
export function EmailFooter({ baseUrl = getBaseUrl(), unsubscribe, messageId }: EmailFooterProps) {
export function EmailFooter({
baseUrl = getBaseUrl(),
messageId,
showUnsubscribe = true,
}: EmailFooterProps) {
const brand = getBrandConfig()
const footerLinkStyle = {
@@ -181,19 +188,20 @@ export function EmailFooter({ baseUrl = getBaseUrl(), unsubscribe, messageId }:
{' '}
<a href={`${baseUrl}/terms`} style={footerLinkStyle} rel='noopener noreferrer'>
Terms of Service
</a>{' '}
{' '}
<a
href={
unsubscribe?.unsubscribeToken && unsubscribe?.email
? `${baseUrl}/unsubscribe?token=${unsubscribe.unsubscribeToken}&email=${encodeURIComponent(unsubscribe.email)}`
: `mailto:${brand.supportEmail}?subject=Unsubscribe%20Request&body=Please%20unsubscribe%20me%20from%20all%20emails.`
}
style={footerLinkStyle}
rel='noopener noreferrer'
>
Unsubscribe
</a>
{showUnsubscribe && (
<>
{' '}
{' '}
<a
href={`${baseUrl}/unsubscribe?token={{UNSUBSCRIBE_TOKEN}}&email={{UNSUBSCRIBE_EMAIL}}`}
style={footerLinkStyle}
rel='noopener noreferrer'
>
Unsubscribe
</a>
</>
)}
</td>
<td style={baseStyles.gutter} width={spacing.gutter}>
&nbsp;

View File

@@ -11,13 +11,23 @@ interface EmailLayoutProps {
children: React.ReactNode
/** Optional: hide footer for internal emails */
hideFooter?: boolean
/**
* Whether to show unsubscribe link in footer.
* Set to false for transactional emails where unsubscribe doesn't apply.
*/
showUnsubscribe: boolean
}
/**
* Shared email layout wrapper providing consistent structure.
* Includes Html, Head, Body, Container with logo header, and Footer.
*/
export function EmailLayout({ preview, children, hideFooter = false }: EmailLayoutProps) {
export function EmailLayout({
preview,
children,
hideFooter = false,
showUnsubscribe,
}: EmailLayoutProps) {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
@@ -43,7 +53,7 @@ export function EmailLayout({ preview, children, hideFooter = false }: EmailLayo
</Container>
{/* Footer in gray section */}
{!hideFooter && <EmailFooter baseUrl={baseUrl} />}
{!hideFooter && <EmailFooter baseUrl={baseUrl} showUnsubscribe={showUnsubscribe} />}
</Body>
</Html>
)

View File

@@ -54,6 +54,7 @@ export function BatchInvitationEmail({
return (
<EmailLayout
preview={`You've been invited to join ${organizationName}${hasWorkspaces ? ` and ${workspaceInvitations.length} workspace(s)` : ''}`}
showUnsubscribe={false}
>
<Text style={baseStyles.paragraph}>Hello,</Text>
<Text style={baseStyles.paragraph}>

View File

@@ -36,7 +36,10 @@ export function InvitationEmail({
}
return (
<EmailLayout preview={`You've been invited to join ${organizationName} on ${brand.name}`}>
<EmailLayout
preview={`You've been invited to join ${organizationName} on ${brand.name}`}
showUnsubscribe={false}
>
<Text style={baseStyles.paragraph}>Hello,</Text>
<Text style={baseStyles.paragraph}>
<strong>{inviterName}</strong> invited you to join <strong>{organizationName}</strong> on{' '}

View File

@@ -22,7 +22,10 @@ export function PollingGroupInvitationEmail({
const providerName = provider === 'google-email' ? 'Gmail' : 'Outlook'
return (
<EmailLayout preview={`You've been invited to join ${pollingGroupName} on ${brand.name}`}>
<EmailLayout
preview={`You've been invited to join ${pollingGroupName} on ${brand.name}`}
showUnsubscribe={false}
>
<Text style={baseStyles.paragraph}>Hello,</Text>
<Text style={baseStyles.paragraph}>
<strong>{inviterName}</strong> from <strong>{organizationName}</strong> has invited you to

View File

@@ -41,6 +41,7 @@ export function WorkspaceInvitationEmail({
return (
<EmailLayout
preview={`You've been invited to join the "${workspaceName}" workspace on ${brand.name}!`}
showUnsubscribe={false}
>
<Text style={baseStyles.paragraph}>Hello,</Text>
<Text style={baseStyles.paragraph}>

View File

@@ -73,7 +73,7 @@ export function WorkflowNotificationEmail({
: 'Your workflow completed successfully.'
return (
<EmailLayout preview={previewText}>
<EmailLayout preview={previewText} showUnsubscribe={true}>
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>Hello,</Text>
<Text style={baseStyles.paragraph}>{message}</Text>

View File

@@ -32,7 +32,10 @@ export function HelpConfirmationEmail({
const typeLabel = getTypeLabel(type)
return (
<EmailLayout preview={`Your ${typeLabel.toLowerCase()} has been received`}>
<EmailLayout
preview={`Your ${typeLabel.toLowerCase()} has been received`}
showUnsubscribe={false}
>
<Text style={baseStyles.paragraph}>Hello,</Text>
<Text style={baseStyles.paragraph}>
We've received your <strong>{typeLabel.toLowerCase()}</strong> and will get back to you