Skip to content

Commit d2eb33a

Browse files
waleedlatif1Sg312
authored andcommitted
feat(billing): add notif for first failed payment, added upgrade email from free, updated providers that supported granular tool control to support them, fixed envvar popover, fixed redirect to wrong workspace after oauth connect (#2015)
* feat(billing): add notif for first failed payment, added upgrade email from free, updated providers that supported granular tool control to support them, fixed envvar popover, fixed redirect to wrong workspace after oauth connect * fix build * ack PR comments
1 parent 01752ef commit d2eb33a

File tree

32 files changed

+888
-236
lines changed

32 files changed

+888
-236
lines changed

apps/sim/app/api/careers/submit/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { render } from '@react-email/components'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4-
import CareersConfirmationEmail from '@/components/emails/careers-confirmation-email'
5-
import CareersSubmissionEmail from '@/components/emails/careers-submission-email'
4+
import CareersConfirmationEmail from '@/components/emails/careers/careers-confirmation-email'
5+
import CareersSubmissionEmail from '@/components/emails/careers/careers-submission-email'
66
import { sendEmail } from '@/lib/email/mailer'
77
import { createLogger } from '@/lib/logs/console/logger'
88
import { generateRequestId } from '@/lib/utils'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,19 +1642,19 @@ export function ToolInput({
16421642
<p className='text-xs'>
16431643
{tool.usageControl === 'auto' && (
16441644
<span>
1645-
<span className='font-medium'>Auto:</span> The model decides when to
1646-
use the tool
1645+
<span className='font-medium' /> The model decides when to use the
1646+
tool
16471647
</span>
16481648
)}
16491649
{tool.usageControl === 'force' && (
16501650
<span>
1651-
<span className='font-medium'>Force:</span> Always use this tool in
1652-
the response
1651+
<span className='font-medium' /> Always use this tool in the
1652+
response
16531653
</span>
16541654
)}
16551655
{tool.usageControl === 'none' && (
16561656
<span>
1657-
<span className='font-medium'>Deny:</span> Never use this tool
1657+
<span className='font-medium' /> Never use this tool
16581658
</span>
16591659
)}
16601660
</p>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function Account(_props: AccountProps) {
2626
const router = useRouter()
2727
const brandConfig = useBrandConfig()
2828

29-
// React Query hooks - with placeholderData to show cached data immediately (no skeleton loading!)
29+
// React Query hooks - with placeholderData to show cached data immediately
3030
const { data: profile } = useUserProfile()
3131
const updateProfile = useUpdateUserProfile()
3232

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function CreatorProfile() {
4141
const { data: session } = useSession()
4242
const userId = session?.user?.id || ''
4343

44-
// React Query hooks - with placeholderData to show cached data immediately (no skeleton loading!)
44+
// React Query hooks - with placeholderData to show cached data immediately
4545
const { data: organizations = [] } = useOrganizations()
4646
const { data: existingProfile } = useCreatorProfile(userId)
4747
const saveProfile = useSaveCreatorProfile()

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

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Check, ChevronDown, ExternalLink, Search } from 'lucide-react'
55
import { useRouter, useSearchParams } from 'next/navigation'
66
import { Button } from '@/components/emcn'
77
import { Input, Label } from '@/components/ui'
8-
import { useSession } from '@/lib/auth-client'
98
import { createLogger } from '@/lib/logs/console/logger'
109
import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth'
1110
import { cn } from '@/lib/utils'
@@ -26,63 +25,38 @@ interface CredentialsProps {
2625
export function Credentials({ onOpenChange, registerCloseHandler }: CredentialsProps) {
2726
const router = useRouter()
2827
const searchParams = useSearchParams()
29-
const { data: session } = useSession()
30-
const userId = session?.user?.id
3128
const pendingServiceRef = useRef<HTMLDivElement>(null)
3229

33-
// React Query hooks - with placeholderData to show cached data immediately (no skeleton loading!)
30+
// React Query hooks - with placeholderData to show cached data immediately
3431
const { data: services = [] } = useOAuthConnections()
3532
const connectService = useConnectOAuthService()
3633
const disconnectService = useDisconnectOAuthService()
3734

3835
// Local UI state
3936
const [searchTerm, setSearchTerm] = useState('')
4037
const [pendingService, setPendingService] = useState<string | null>(null)
41-
const [_pendingScopes, setPendingScopes] = useState<string[]>([])
4238
const [authSuccess, setAuthSuccess] = useState(false)
4339
const [showActionRequired, setShowActionRequired] = useState(false)
4440
const prevConnectedIdsRef = useRef<Set<string>>(new Set())
4541
const connectionAddedRef = useRef<boolean>(false)
4642

47-
// Check for OAuth callback
43+
// Check for OAuth callback - just show success message
4844
useEffect(() => {
4945
const code = searchParams.get('code')
5046
const state = searchParams.get('state')
5147
const error = searchParams.get('error')
5248

53-
// Handle OAuth callback
5449
if (code && state) {
55-
// This is an OAuth callback - try to restore state from localStorage
56-
try {
57-
const stored = localStorage.getItem('pending_oauth_state')
58-
if (stored) {
59-
const oauthState = JSON.parse(stored)
60-
logger.info('OAuth callback with restored state:', oauthState)
61-
62-
// Mark as pending if we have context about what service was being connected
63-
if (oauthState.serviceId) {
64-
setPendingService(oauthState.serviceId)
65-
setShowActionRequired(true)
66-
}
67-
68-
// Clean up the state (one-time use)
69-
localStorage.removeItem('pending_oauth_state')
70-
} else {
71-
logger.warn('OAuth callback but no state found in localStorage')
72-
}
73-
} catch (error) {
74-
logger.error('Error loading OAuth state from localStorage:', error)
75-
localStorage.removeItem('pending_oauth_state') // Clean up corrupted state
76-
}
77-
78-
// Set success flag
50+
logger.info('OAuth callback successful')
7951
setAuthSuccess(true)
8052

81-
// Clear the URL parameters
82-
router.replace('/workspace')
53+
// Clear URL parameters without changing the page
54+
const url = new URL(window.location.href)
55+
url.searchParams.delete('code')
56+
url.searchParams.delete('state')
57+
router.replace(url.pathname + url.search)
8358
} else if (error) {
8459
logger.error('OAuth error:', { error })
85-
router.replace('/workspace')
8660
}
8761
}, [searchParams, router])
8862

@@ -132,6 +106,7 @@ export function Credentials({ onOpenChange, registerCloseHandler }: CredentialsP
132106
scopes: service.scopes,
133107
})
134108

109+
// better-auth will automatically redirect back to this URL after OAuth
135110
await connectService.mutateAsync({
136111
providerId: service.providerId,
137112
callbackURL: window.location.href,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function Files() {
5555
const params = useParams()
5656
const workspaceId = params?.workspaceId as string
5757

58-
// React Query hooks - with placeholderData to show cached data immediately (no skeleton loading!)
58+
// React Query hooks - with placeholderData to show cached data immediately
5959
const { data: files = [] } = useWorkspaceFiles(workspaceId)
6060
const { data: storageInfo } = useStorageInfo(isBillingEnabled)
6161
const uploadFile = useUploadWorkspaceFile()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function General() {
3434
const [isSuperUser, setIsSuperUser] = useState(false)
3535
const [loadingSuperUser, setLoadingSuperUser] = useState(true)
3636

37-
// React Query hooks - with placeholderData to show cached data immediately (no skeleton loading!)
37+
// React Query hooks - with placeholderData to show cached data immediately
3838
const { data: settings, isLoading } = useGeneralSettings()
3939
const updateSetting = useUpdateGeneralSetting()
4040

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const TOOLTIPS = {
1313
}
1414

1515
export function Privacy() {
16-
// React Query hooks - with placeholderData to show cached data immediately (no skeleton loading!)
16+
// React Query hooks - with placeholderData to show cached data immediately
1717
const { data: settings } = useGeneralSettings()
1818
const updateSetting = useUpdateGeneralSetting()
1919

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,15 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
469469
/>
470470
</div>
471471

472+
{/* Enterprise Usage Limit Notice */}
473+
{subscription.isEnterprise && (
474+
<div className='text-center'>
475+
<p className='text-muted-foreground text-xs'>
476+
Contact enterprise for support usage limit changes
477+
</p>
478+
</div>
479+
)}
480+
472481
{/* Cost Breakdown */}
473482
{/* TODO: Re-enable CostBreakdown component in the next billing period
474483
once sufficient copilot cost data has been collected for accurate display.
@@ -554,14 +563,6 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
554563
{/* Billing usage notifications toggle */}
555564
{subscription.isPaid && <BillingUsageNotificationsToggle />}
556565

557-
{subscription.isEnterprise && (
558-
<div className='text-center'>
559-
<p className='text-muted-foreground text-xs'>
560-
Contact enterprise for support usage limit changes
561-
</p>
562-
</div>
563-
)}
564-
565566
{/* Cancel Subscription */}
566567
{permissions.canCancelSubscription && (
567568
<div className='mt-2'>
@@ -631,9 +632,6 @@ function BillingUsageNotificationsToggle() {
631632
const updateSetting = useUpdateGeneralSetting()
632633
const isLoading = updateSetting.isPending
633634

634-
// Settings are automatically loaded by SettingsLoader provider
635-
// No need to load here - Zustand is synced from React Query
636-
637635
return (
638636
<div className='mt-4 flex items-center justify-between'>
639637
<div className='flex flex-col'>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/usage-indicator/usage-indicator.tsx

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
184184
)
185185
}
186186

187-
const handleClick = () => {
187+
const handleClick = async () => {
188188
try {
189189
if (onClick) {
190190
onClick()
@@ -194,7 +194,35 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
194194
const blocked = getBillingStatus(subscriptionData?.data) === 'blocked'
195195
const canUpg = canUpgrade(subscriptionData?.data)
196196

197-
// Open Settings modal to the subscription tab (upgrade UI lives there)
197+
// If blocked, try to open billing portal directly for faster recovery
198+
if (blocked) {
199+
try {
200+
const context = subscription.isTeam || subscription.isEnterprise ? 'organization' : 'user'
201+
const organizationId =
202+
subscription.isTeam || subscription.isEnterprise
203+
? subscriptionData?.data?.organization?.id
204+
: undefined
205+
206+
const response = await fetch('/api/billing/portal', {
207+
method: 'POST',
208+
headers: { 'Content-Type': 'application/json' },
209+
body: JSON.stringify({ context, organizationId }),
210+
})
211+
212+
if (response.ok) {
213+
const { url } = await response.json()
214+
window.open(url, '_blank')
215+
logger.info('Opened billing portal for blocked account', { context, organizationId })
216+
return
217+
}
218+
} catch (portalError) {
219+
logger.warn('Failed to open billing portal, falling back to settings', {
220+
error: portalError,
221+
})
222+
}
223+
}
224+
225+
// Fallback: Open Settings modal to the subscription tab
198226
if (typeof window !== 'undefined') {
199227
window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'subscription' } }))
200228
logger.info('Opened settings to subscription tab', { blocked, canUpgrade: canUpg })
@@ -206,7 +234,9 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
206234

207235
return (
208236
<div
209-
className='group flex flex-shrink-0 cursor-pointer flex-col gap-[8px] border-t px-[13.5px] pt-[8px] pb-[10px]'
237+
className={`group flex flex-shrink-0 cursor-pointer flex-col gap-[8px] border-t px-[13.5px] pt-[8px] pb-[10px] ${
238+
isBlocked ? 'border-red-500/50 bg-red-950/20' : ''
239+
}`}
210240
onClick={handleClick}
211241
onMouseEnter={() => setIsHovered(true)}
212242
onMouseLeave={() => setIsHovered(false)}
@@ -219,8 +249,8 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
219249
<div className='flex items-center gap-[4px]'>
220250
{isBlocked ? (
221251
<>
222-
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>Over</span>
223-
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>limit</span>
252+
<span className='font-medium text-[12px] text-red-400'>Payment</span>
253+
<span className='font-medium text-[12px] text-red-400'>Required</span>
224254
</>
225255
) : (
226256
<>
@@ -238,10 +268,14 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
238268
{showUpgradeButton && (
239269
<Button
240270
variant='ghost'
241-
className='-mx-1 !h-auto !px-1 !py-0 !text-[#F473B7] group-hover:!text-[#F789C4] mt-[-2px] transition-colors duration-100'
271+
className={`-mx-1 !h-auto !px-1 !py-0 mt-[-2px] transition-colors duration-100 ${
272+
isBlocked
273+
? '!text-red-400 group-hover:!text-red-300'
274+
: '!text-[#F473B7] group-hover:!text-[#F789C4]'
275+
}`}
242276
onClick={handleClick}
243277
>
244-
<span className='font-medium text-[12px]'>Upgrade</span>
278+
<span className='font-medium text-[12px]'>{isBlocked ? 'Fix Now' : 'Upgrade'}</span>
245279
</Button>
246280
)}
247281
</div>
@@ -251,7 +285,11 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
251285
{Array.from({ length: pillCount }).map((_, i) => {
252286
const isFilled = i < filledPillsCount
253287

254-
const baseColor = isFilled ? (isAlmostOut ? '#ef4444' : '#34B5FF') : '#414141'
288+
const baseColor = isFilled
289+
? isBlocked || isAlmostOut
290+
? '#ef4444'
291+
: '#34B5FF'
292+
: '#414141'
255293

256294
let backgroundColor = baseColor
257295
let backgroundImage: string | undefined

0 commit comments

Comments
 (0)