Skip to content

Commit 07e7040

Browse files
authored
feat(notifications): added notifications for usage thresholds, overages, and welcome emails (#1266)
* feat(notifications): added notifications for usage thresholds, overages, and welcome emails * cleanup * updated logo, ack PR comments * ran migrations
1 parent 07ba174 commit 07e7040

File tree

24 files changed

+6680
-31
lines changed

24 files changed

+6680
-31
lines changed

apps/sim/app/api/users/me/settings/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const SettingsSchema = z.object({
2424
unsubscribeNotifications: z.boolean().optional(),
2525
})
2626
.optional(),
27+
billingUsageNotificationsEnabled: z.boolean().optional(),
2728
})
2829

2930
// Default settings values
@@ -35,6 +36,7 @@ const defaultSettings = {
3536
consoleExpandedByDefault: true,
3637
telemetryEnabled: true,
3738
emailPreferences: {},
39+
billingUsageNotificationsEnabled: true,
3840
}
3941

4042
export async function GET() {
@@ -68,6 +70,7 @@ export async function GET() {
6870
consoleExpandedByDefault: userSettings.consoleExpandedByDefault,
6971
telemetryEnabled: userSettings.telemetryEnabled,
7072
emailPreferences: userSettings.emailPreferences ?? {},
73+
billingUsageNotificationsEnabled: userSettings.billingUsageNotificationsEnabled ?? true,
7174
},
7275
},
7376
{ status: 200 }

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use client'
2-
32
import { useCallback, useEffect, useRef, useState } from 'react'
4-
import { Skeleton } from '@/components/ui'
3+
import { Skeleton, Switch } from '@/components/ui'
54
import { useSession } from '@/lib/auth-client'
65
import { useSubscriptionUpgrade } from '@/lib/subscription/upgrade'
76
import { cn } from '@/lib/utils'
@@ -500,6 +499,9 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
500499
</div>
501500
)}
502501

502+
{/* Billing usage notifications toggle */}
503+
{subscription.isPaid && <BillingUsageNotificationsToggle />}
504+
503505
{subscription.isEnterprise && (
504506
<div className='text-center'>
505507
<p className='text-muted-foreground text-xs'>
@@ -527,3 +529,42 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
527529
</div>
528530
)
529531
}
532+
533+
function BillingUsageNotificationsToggle() {
534+
const [enabled, setEnabled] = useState<boolean | null>(null)
535+
536+
useEffect(() => {
537+
let isMounted = true
538+
const load = async () => {
539+
const res = await fetch('/api/users/me/settings')
540+
const json = await res.json()
541+
const current = json?.data?.billingUsageNotificationsEnabled
542+
if (isMounted) setEnabled(current !== false)
543+
}
544+
load()
545+
return () => {
546+
isMounted = false
547+
}
548+
}, [])
549+
550+
const update = async (next: boolean) => {
551+
setEnabled(next)
552+
await fetch('/api/users/me/settings', {
553+
method: 'PATCH',
554+
headers: { 'Content-Type': 'application/json' },
555+
body: JSON.stringify({ billingUsageNotificationsEnabled: next }),
556+
})
557+
}
558+
559+
if (enabled === null) return null
560+
561+
return (
562+
<div className='mt-4 flex items-center justify-between'>
563+
<div className='flex flex-col'>
564+
<span className='font-medium text-sm'>Usage notifications</span>
565+
<span className='text-muted-foreground text-xs'>Email me when I reach 80% usage</span>
566+
</div>
567+
<Switch checked={enabled} onCheckedChange={(v: boolean) => update(v)} />
568+
</div>
569+
)
570+
}

apps/sim/components/emails/batch-invitation-email.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
} from '@react-email/components'
1414
import { getBrandConfig } from '@/lib/branding/branding'
1515
import { env } from '@/lib/env'
16-
import { getAssetUrl } from '@/lib/utils'
1716
import { baseStyles } from './base-styles'
1817
import EmailFooter from './footer'
1918

@@ -80,7 +79,7 @@ export const BatchInvitationEmail = ({
8079
<Row>
8180
<Column style={{ textAlign: 'center' }}>
8281
<Img
83-
src={brand.logoUrl || getAssetUrl('static/sim.png')}
82+
src={brand.logoUrl || '/logo/reverse/text/medium.png'}
8483
width='114'
8584
alt={brand.name}
8685
style={{

apps/sim/components/emails/enterprise-subscription-email.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
import { format } from 'date-fns'
1515
import { getBrandConfig } from '@/lib/branding/branding'
1616
import { env } from '@/lib/env'
17-
import { getAssetUrl } from '@/lib/utils'
1817
import { baseStyles } from './base-styles'
1918
import EmailFooter from './footer'
2019

@@ -45,7 +44,7 @@ export const EnterpriseSubscriptionEmail = ({
4544
<Row>
4645
<Column style={{ textAlign: 'center' }}>
4746
<Img
48-
src={brand.logoUrl || getAssetUrl('static/sim.png')}
47+
src={brand.logoUrl || '/logo/reverse/text/medium.png'}
4948
width='114'
5049
alt={brand.name}
5150
style={{
@@ -94,7 +93,7 @@ export const EnterpriseSubscriptionEmail = ({
9493
</Text>
9594

9695
<Text style={baseStyles.paragraph}>
97-
Welcome to Sim Enterprise!
96+
Best regards,
9897
<br />
9998
The Sim Team
10099
</Text>

apps/sim/components/emails/help-confirmation-email.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
import { format } from 'date-fns'
1414
import { getBrandConfig } from '@/lib/branding/branding'
1515
import { env } from '@/lib/env'
16-
import { getAssetUrl } from '@/lib/utils'
1716
import { baseStyles } from './base-styles'
1817
import EmailFooter from './footer'
1918

@@ -60,7 +59,7 @@ export const HelpConfirmationEmail = ({
6059
<Row>
6160
<Column style={{ textAlign: 'center' }}>
6261
<Img
63-
src={brand.logoUrl || getAssetUrl('static/sim.png')}
62+
src={brand.logoUrl || '/logo/reverse/text/medium.png'}
6463
width='114'
6564
alt={brand.name}
6665
style={{

apps/sim/components/emails/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export { default as EmailFooter } from './footer'
55
export { HelpConfirmationEmail } from './help-confirmation-email'
66
export { InvitationEmail } from './invitation-email'
77
export { OTPVerificationEmail } from './otp-verification-email'
8+
export { PlanWelcomeEmail } from './plan-welcome-email'
89
export * from './render-email'
910
export { ResetPasswordEmail } from './reset-password-email'
11+
export { UsageThresholdEmail } from './usage-threshold-email'
1012
export { WorkspaceInvitationEmail } from './workspace-invitation'

apps/sim/components/emails/invitation-email.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { format } from 'date-fns'
1515
import { getBrandConfig } from '@/lib/branding/branding'
1616
import { env } from '@/lib/env'
1717
import { createLogger } from '@/lib/logs/console/logger'
18-
import { getAssetUrl } from '@/lib/utils'
1918
import { baseStyles } from './base-styles'
2019
import EmailFooter from './footer'
2120

@@ -66,7 +65,7 @@ export const InvitationEmail = ({
6665
<Row>
6766
<Column style={{ textAlign: 'center' }}>
6867
<Img
69-
src={brand.logoUrl || getAssetUrl('static/sim.png')}
68+
src={brand.logoUrl || '/logo/reverse/text/medium.png'}
7069
width='114'
7170
alt={brand.name}
7271
style={{

apps/sim/components/emails/otp-verification-email.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
} from '@react-email/components'
1313
import { getBrandConfig } from '@/lib/branding/branding'
1414
import { env } from '@/lib/env'
15-
import { getAssetUrl } from '@/lib/utils'
1615
import { baseStyles } from './base-styles'
1716
import EmailFooter from './footer'
1817

@@ -72,7 +71,7 @@ export const OTPVerificationEmail = ({
7271
<Row>
7372
<Column style={{ textAlign: 'center' }}>
7473
<Img
75-
src={brand.logoUrl || getAssetUrl('static/sim.png')}
74+
src={brand.logoUrl || '/logo/reverse/text/medium.png'}
7675
width='114'
7776
alt={brand.name}
7877
style={{
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {
2+
Body,
3+
Column,
4+
Container,
5+
Head,
6+
Hr,
7+
Html,
8+
Img,
9+
Link,
10+
Preview,
11+
Row,
12+
Section,
13+
Text,
14+
} from '@react-email/components'
15+
import EmailFooter from '@/components/emails/footer'
16+
import { getBrandConfig } from '@/lib/branding/branding'
17+
import { env } from '@/lib/env'
18+
import { baseStyles } from './base-styles'
19+
20+
interface PlanWelcomeEmailProps {
21+
planName: 'Pro' | 'Team'
22+
userName?: string
23+
loginLink?: string
24+
createdDate?: Date
25+
}
26+
27+
export function PlanWelcomeEmail({
28+
planName,
29+
userName,
30+
loginLink,
31+
createdDate = new Date(),
32+
}: PlanWelcomeEmailProps) {
33+
const brand = getBrandConfig()
34+
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
35+
const cta = loginLink || `${baseUrl}/login`
36+
37+
const previewText = `${brand.name}: Your ${planName} plan is active`
38+
39+
return (
40+
<Html>
41+
<Head />
42+
<Preview>{previewText}</Preview>
43+
<Body style={baseStyles.main}>
44+
<Container style={baseStyles.container}>
45+
<Section style={{ padding: '30px 0', textAlign: 'center' }}>
46+
<Row>
47+
<Column style={{ textAlign: 'center' }}>
48+
<Img
49+
src={brand.logoUrl || '/logo/reverse/text/medium.png'}
50+
width='114'
51+
alt={brand.name}
52+
style={{
53+
margin: '0 auto',
54+
}}
55+
/>
56+
</Column>
57+
</Row>
58+
</Section>
59+
60+
<Section style={baseStyles.sectionsBorders}>
61+
<Row>
62+
<Column style={baseStyles.sectionBorder} />
63+
<Column style={baseStyles.sectionCenter} />
64+
<Column style={baseStyles.sectionBorder} />
65+
</Row>
66+
</Section>
67+
68+
<Section style={baseStyles.content}>
69+
<Text style={{ ...baseStyles.paragraph, marginTop: 0 }}>
70+
{userName ? `Hi ${userName},` : 'Hi,'}
71+
</Text>
72+
<Text style={baseStyles.paragraph}>
73+
Welcome to the <strong>{planName}</strong> plan on {brand.name}. You're all set to
74+
build, test, and scale your agentic workflows.
75+
</Text>
76+
77+
<Link href={cta} style={{ textDecoration: 'none' }}>
78+
<Text style={baseStyles.button}>Open {brand.name}</Text>
79+
</Link>
80+
81+
<Text style={baseStyles.paragraph}>
82+
Want to discuss your plan or get personalized help getting started?{' '}
83+
<Link href='https://cal.com/waleedlatif/15min' style={baseStyles.link}>
84+
Schedule a 15-minute call
85+
</Link>{' '}
86+
with our team.
87+
</Text>
88+
89+
<Hr />
90+
91+
<Text style={baseStyles.paragraph}>
92+
Need to invite teammates, adjust usage limits, or manage billing? You can do that from
93+
Settings → Subscription.
94+
</Text>
95+
96+
<Text style={baseStyles.paragraph}>
97+
Best regards,
98+
<br />
99+
The Sim Team
100+
</Text>
101+
102+
<Text style={{ ...baseStyles.paragraph, fontSize: '12px', color: '#666' }}>
103+
Sent on {createdDate.toLocaleDateString()}
104+
</Text>
105+
</Section>
106+
</Container>
107+
<EmailFooter baseUrl={baseUrl} />
108+
</Body>
109+
</Html>
110+
)
111+
}
112+
113+
export default PlanWelcomeEmail

apps/sim/components/emails/render-email.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
HelpConfirmationEmail,
66
InvitationEmail,
77
OTPVerificationEmail,
8+
PlanWelcomeEmail,
89
ResetPasswordEmail,
10+
UsageThresholdEmail,
911
} from '@/components/emails'
1012
import { getBrandConfig } from '@/lib/branding/branding'
1113

@@ -100,6 +102,27 @@ export async function renderEnterpriseSubscriptionEmail(
100102
)
101103
}
102104

105+
export async function renderUsageThresholdEmail(params: {
106+
userName?: string
107+
planName: string
108+
percentUsed: number
109+
currentUsage: number
110+
limit: number
111+
ctaLink: string
112+
}): Promise<string> {
113+
return await render(
114+
UsageThresholdEmail({
115+
userName: params.userName,
116+
planName: params.planName,
117+
percentUsed: params.percentUsed,
118+
currentUsage: params.currentUsage,
119+
limit: params.limit,
120+
ctaLink: params.ctaLink,
121+
updatedDate: new Date(),
122+
})
123+
)
124+
}
125+
103126
export function getEmailSubject(
104127
type:
105128
| 'sign-in'
@@ -110,6 +133,9 @@ export function getEmailSubject(
110133
| 'batch-invitation'
111134
| 'help-confirmation'
112135
| 'enterprise-subscription'
136+
| 'usage-threshold'
137+
| 'plan-welcome-pro'
138+
| 'plan-welcome-team'
113139
): string {
114140
const brandName = getBrandConfig().name
115141

@@ -130,7 +156,28 @@ export function getEmailSubject(
130156
return 'Your request has been received'
131157
case 'enterprise-subscription':
132158
return `Your Enterprise Plan is now active on ${brandName}`
159+
case 'usage-threshold':
160+
return `You're nearing your monthly budget on ${brandName}`
161+
case 'plan-welcome-pro':
162+
return `Your Pro plan is now active on ${brandName}`
163+
case 'plan-welcome-team':
164+
return `Your Team plan is now active on ${brandName}`
133165
default:
134166
return brandName
135167
}
136168
}
169+
170+
export async function renderPlanWelcomeEmail(params: {
171+
planName: 'Pro' | 'Team'
172+
userName?: string
173+
loginLink?: string
174+
}): Promise<string> {
175+
return await render(
176+
PlanWelcomeEmail({
177+
planName: params.planName,
178+
userName: params.userName,
179+
loginLink: params.loginLink,
180+
createdDate: new Date(),
181+
})
182+
)
183+
}

0 commit comments

Comments
 (0)