Skip to content

Commit ab465ee

Browse files
committed
feat(email): lower free tier warning to 80% and add credits exhausted email
1 parent ffec9e9 commit ab465ee

6 files changed

Lines changed: 172 additions & 6 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Link, Section, Text } from '@react-email/components'
2+
import { baseStyles, colors, typography } from '@/components/emails/_styles'
3+
import { EmailLayout } from '@/components/emails/components'
4+
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
5+
import { getBrandConfig } from '@/ee/whitelabeling'
6+
7+
interface CreditsExhaustedEmailProps {
8+
userName?: string
9+
currentUsage: number
10+
limit: number
11+
upgradeLink: string
12+
}
13+
14+
const proFeatures = [
15+
{ label: '6,000 credits/month', desc: 'included' },
16+
{ label: '+50 daily refresh', desc: 'credits per day' },
17+
{ label: '150 runs/min', desc: 'sync executions' },
18+
{ label: '50GB storage', desc: 'for files & assets' },
19+
]
20+
21+
export function CreditsExhaustedEmail({
22+
userName,
23+
currentUsage,
24+
limit,
25+
upgradeLink,
26+
}: CreditsExhaustedEmailProps) {
27+
const brand = getBrandConfig()
28+
29+
return (
30+
<EmailLayout
31+
preview={`You've used all ${dollarsToCredits(limit).toLocaleString()} of your free ${brand.name} credits`}
32+
showUnsubscribe={true}
33+
>
34+
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>
35+
{userName ? `Hi ${userName},` : 'Hi,'}
36+
</Text>
37+
38+
<Text style={baseStyles.paragraph}>
39+
You&apos;ve used all <strong>{dollarsToCredits(currentUsage).toLocaleString()}</strong> of
40+
your free credits on {brand.name}. Your workflows are paused until you upgrade.
41+
</Text>
42+
43+
<Section
44+
style={{
45+
backgroundColor: '#f8faf9',
46+
border: `1px solid ${colors.brandTertiary}20`,
47+
borderRadius: '8px',
48+
padding: '16px 20px',
49+
margin: '16px 0',
50+
}}
51+
>
52+
<Text
53+
style={{
54+
fontSize: '14px',
55+
fontWeight: 600,
56+
color: colors.brandTertiary,
57+
fontFamily: typography.fontFamily,
58+
margin: '0 0 12px 0',
59+
textTransform: 'uppercase' as const,
60+
letterSpacing: '0.5px',
61+
}}
62+
>
63+
Pro includes
64+
</Text>
65+
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
66+
<tbody>
67+
{proFeatures.map((feature, i) => (
68+
<tr key={i}>
69+
<td
70+
style={{
71+
padding: '6px 0',
72+
fontSize: '15px',
73+
fontWeight: 600,
74+
color: colors.textPrimary,
75+
fontFamily: typography.fontFamily,
76+
width: '45%',
77+
}}
78+
>
79+
{feature.label}
80+
</td>
81+
<td
82+
style={{
83+
padding: '6px 0',
84+
fontSize: '14px',
85+
color: colors.textMuted,
86+
fontFamily: typography.fontFamily,
87+
}}
88+
>
89+
{feature.desc}
90+
</td>
91+
</tr>
92+
))}
93+
</tbody>
94+
</table>
95+
</Section>
96+
97+
<Link href={upgradeLink} style={{ textDecoration: 'none' }}>
98+
<Text style={baseStyles.button}>Upgrade to Pro</Text>
99+
</Link>
100+
101+
<div style={baseStyles.divider} />
102+
103+
<Text style={{ ...baseStyles.footerText, textAlign: 'left' }}>
104+
One-time notification when free credits are exhausted.
105+
</Text>
106+
</EmailLayout>
107+
)
108+
}
109+
110+
export default CreditsExhaustedEmail

apps/sim/components/emails/billing/free-tier-upgrade-email.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function FreeTierUpgradeEmail({
105105
<div style={baseStyles.divider} />
106106

107107
<Text style={{ ...baseStyles.footerText, textAlign: 'left' }}>
108-
One-time notification at 90% usage.
108+
One-time notification at 80% usage.
109109
</Text>
110110
</EmailLayout>
111111
)

apps/sim/components/emails/billing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { AbandonedCheckoutEmail } from './abandoned-checkout-email'
22
export { CreditPurchaseEmail } from './credit-purchase-email'
3+
export { CreditsExhaustedEmail } from './credits-exhausted-email'
34
export { EnterpriseSubscriptionEmail } from './enterprise-subscription-email'
45
export { FreeTierUpgradeEmail } from './free-tier-upgrade-email'
56
export { PaymentFailedEmail } from './payment-failed-email'

apps/sim/components/emails/render.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import {
99
AbandonedCheckoutEmail,
1010
CreditPurchaseEmail,
11+
CreditsExhaustedEmail,
1112
EnterpriseSubscriptionEmail,
1213
FreeTierUpgradeEmail,
1314
PaymentFailedEmail,
@@ -173,6 +174,15 @@ export async function renderAbandonedCheckoutEmail(userName?: string): Promise<s
173174
return await render(AbandonedCheckoutEmail({ userName }))
174175
}
175176

177+
export async function renderCreditsExhaustedEmail(params: {
178+
userName?: string
179+
currentUsage: number
180+
limit: number
181+
upgradeLink: string
182+
}): Promise<string> {
183+
return await render(CreditsExhaustedEmail(params))
184+
}
185+
176186
export async function renderCreditPurchaseEmail(params: {
177187
userName?: string
178188
amount: number

apps/sim/components/emails/subjects.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type EmailSubjectType =
1717
| 'plan-welcome-team'
1818
| 'credit-purchase'
1919
| 'abandoned-checkout'
20+
| 'free-tier-exhausted'
2021
| 'onboarding-followup'
2122
| 'welcome'
2223

@@ -59,6 +60,8 @@ export function getEmailSubject(type: EmailSubjectType): string {
5960
return `Credits added to your ${brandName} account`
6061
case 'abandoned-checkout':
6162
return `Quick question`
63+
case 'free-tier-exhausted':
64+
return `You've run out of free credits on ${brandName}`
6265
case 'onboarding-followup':
6366
return `Quick question about ${brandName}`
6467
case 'welcome':

apps/sim/lib/billing/core/usage.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { eq, inArray } from 'drizzle-orm'
55
import {
66
getEmailSubject,
7+
renderCreditsExhaustedEmail,
78
renderFreeTierUpgradeEmail,
89
renderUsageThresholdEmail,
910
} from '@/components/emails'
@@ -716,11 +717,13 @@ export async function maybeSendUsageThresholdEmail(params: {
716717

717718
// Check for 80% threshold (all users)
718719
const crosses80 = params.percentBefore < 80 && params.percentAfter >= 80
719-
// Check for 90% threshold (free users only)
720-
const crosses90 = params.percentBefore < 90 && params.percentAfter >= 90
720+
// Check for 80% threshold (free users only)
721+
const crosses80Free = params.percentBefore < 80 && params.percentAfter >= 80
722+
// Check for 100% threshold (free users only — credits exhausted)
723+
const crosses100 = params.percentBefore < 100 && params.percentAfter >= 100
721724

722725
// Skip if no thresholds crossed
723-
if (!crosses80 && !crosses90) return
726+
if (!crosses80 && !crosses80Free && !crosses100) return
724727

725728
// For 80% threshold email (all users)
726729
if (crosses80) {
@@ -777,8 +780,8 @@ export async function maybeSendUsageThresholdEmail(params: {
777780
}
778781
}
779782

780-
// For 90% threshold email (free users only)
781-
if (crosses90 && isFreeUser) {
783+
// For 80% threshold email (free users only)
784+
if (crosses80Free && isFreeUser) {
782785
const upgradeLink = `${baseUrl}/workspace?billing=upgrade`
783786
const sendFreeTierEmail = async (email: string, name?: string) => {
784787
const prefs = await getEmailPreferences(email)
@@ -818,6 +821,45 @@ export async function maybeSendUsageThresholdEmail(params: {
818821
await sendFreeTierEmail(params.userEmail, params.userName)
819822
}
820823
}
824+
825+
// For 100% threshold email (free users only — credits exhausted)
826+
if (crosses100 && isFreeUser) {
827+
const upgradeLink = `${baseUrl}/workspace?billing=upgrade`
828+
const sendExhaustedEmail = async (email: string, name?: string) => {
829+
const prefs = await getEmailPreferences(email)
830+
if (prefs?.unsubscribeAll || prefs?.unsubscribeNotifications) return
831+
832+
const html = await renderCreditsExhaustedEmail({
833+
userName: name,
834+
currentUsage: params.currentUsageAfter,
835+
limit: params.limit,
836+
upgradeLink,
837+
})
838+
839+
await sendEmail({
840+
to: email,
841+
subject: getEmailSubject('free-tier-exhausted'),
842+
html,
843+
emailType: 'notifications',
844+
})
845+
846+
logger.info('Free tier credits exhausted email sent', {
847+
email,
848+
currentUsage: params.currentUsageAfter,
849+
limit: params.limit,
850+
})
851+
}
852+
853+
if (params.scope === 'user' && params.userId && params.userEmail) {
854+
const rows = await db
855+
.select({ enabled: settings.billingUsageNotificationsEnabled })
856+
.from(settings)
857+
.where(eq(settings.userId, params.userId))
858+
.limit(1)
859+
if (rows.length > 0 && rows[0].enabled === false) return
860+
await sendExhaustedEmail(params.userEmail, params.userName)
861+
}
862+
}
821863
} catch (error) {
822864
logger.error('Failed to send usage threshold email', {
823865
scope: params.scope,

0 commit comments

Comments
 (0)