Skip to content

Commit 7f1ff7f

Browse files
fix(billing): should allow restoring subscription (#1728)
* fix(already-cancelled-sub): UI should allow restoring subscription * restore functionality fixed * fix
1 parent 9b2490c commit 7f1ff7f

File tree

5 files changed

+70
-57
lines changed

5 files changed

+70
-57
lines changed

apps/sim/app/api/billing/portal/route.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { db } from '@sim/db'
22
import { subscription as subscriptionTable, user } from '@sim/db/schema'
3-
import { and, eq } from 'drizzle-orm'
3+
import { and, eq, or } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { getSession } from '@/lib/auth'
66
import { requireStripeClient } from '@/lib/billing/stripe-client'
@@ -38,7 +38,10 @@ export async function POST(request: NextRequest) {
3838
.where(
3939
and(
4040
eq(subscriptionTable.referenceId, organizationId),
41-
eq(subscriptionTable.status, 'active')
41+
or(
42+
eq(subscriptionTable.status, 'active'),
43+
eq(subscriptionTable.cancelAtPeriodEnd, true)
44+
)
4245
)
4346
)
4447
.limit(1)

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

Lines changed: 51 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
AlertDialogTitle,
1313
} from '@/components/ui/alert-dialog'
1414
import { Button } from '@/components/ui/button'
15-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
1615
import { useSession, useSubscription } from '@/lib/auth-client'
1716
import { createLogger } from '@/lib/logs/console/logger'
1817
import { getBaseUrl } from '@/lib/urls/utils'
@@ -30,6 +29,7 @@ interface CancelSubscriptionProps {
3029
}
3130
subscriptionData?: {
3231
periodEnd?: Date | null
32+
cancelAtPeriodEnd?: boolean
3333
}
3434
}
3535

@@ -127,35 +127,48 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
127127
const subscriptionStatus = getSubscriptionStatus()
128128
const activeOrgId = activeOrganization?.id
129129

130-
// For team/enterprise plans, get the subscription ID from organization store
131-
if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) {
132-
const orgSubscription = useOrganizationStore.getState().subscriptionData
130+
if (isCancelAtPeriodEnd) {
131+
if (!betterAuthSubscription.restore) {
132+
throw new Error('Subscription restore not available')
133+
}
134+
135+
let referenceId: string
136+
let subscriptionId: string | undefined
137+
138+
if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) {
139+
const orgSubscription = useOrganizationStore.getState().subscriptionData
140+
referenceId = activeOrgId
141+
subscriptionId = orgSubscription?.id
142+
} else {
143+
// For personal subscriptions, use user ID and let better-auth find the subscription
144+
referenceId = session.user.id
145+
subscriptionId = undefined
146+
}
147+
148+
logger.info('Restoring subscription', { referenceId, subscriptionId })
133149

134-
if (orgSubscription?.id && orgSubscription?.cancelAtPeriodEnd) {
135-
// Restore the organization subscription
136-
if (!betterAuthSubscription.restore) {
137-
throw new Error('Subscription restore not available')
138-
}
139-
140-
const result = await betterAuthSubscription.restore({
141-
referenceId: activeOrgId,
142-
subscriptionId: orgSubscription.id,
143-
})
144-
logger.info('Organization subscription restored successfully', result)
150+
// Build restore params - only include subscriptionId if we have one (team/enterprise)
151+
const restoreParams: any = { referenceId }
152+
if (subscriptionId) {
153+
restoreParams.subscriptionId = subscriptionId
145154
}
155+
156+
const result = await betterAuthSubscription.restore(restoreParams)
157+
158+
logger.info('Subscription restored successfully', result)
146159
}
147160

148-
// Refresh state and close
149161
await refresh()
150162
if (activeOrgId) {
151163
await loadOrganizationSubscription(activeOrgId)
152164
await refreshOrganization().catch(() => {})
153165
}
166+
154167
setIsDialogOpen(false)
155168
} catch (error) {
156-
const errorMessage = error instanceof Error ? error.message : 'Failed to keep subscription'
169+
const errorMessage = error instanceof Error ? error.message : 'Failed to restore subscription'
157170
setError(errorMessage)
158-
logger.error('Failed to keep subscription', { error })
171+
logger.error('Failed to restore subscription', { error })
159172
} finally {
160173
setIsLoading(false)
161174
}
@@ -190,19 +203,15 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
190203
const periodEndDate = getPeriodEndDate()
191204

192205
// Check if subscription is set to cancel at period end
193-
const isCancelAtPeriodEnd = (() => {
194-
const subscriptionStatus = getSubscriptionStatus()
195-
if (subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) {
196-
return useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd === true
197-
}
198-
return false
199-
})()
206+
const isCancelAtPeriodEnd = subscriptionData?.cancelAtPeriodEnd === true
200207

201208
return (
202209
<>
203210
<div className='flex items-center justify-between'>
204211
<div>
205-
<span className='font-medium text-sm'>Manage Subscription</span>
212+
<span className='font-medium text-sm'>
213+
{isCancelAtPeriodEnd ? 'Restore Subscription' : 'Manage Subscription'}
214+
</span>
206215
{isCancelAtPeriodEnd && (
207216
<p className='mt-1 text-muted-foreground text-xs'>
208217
You'll keep access until {formatDate(periodEndDate)}
@@ -217,22 +226,24 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
217226
'h-8 rounded-[8px] font-medium text-xs transition-all duration-200',
218227
error
219228
? 'border-red-500 text-red-500 dark:border-red-500 dark:text-red-500'
220-
: 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500'
229+
: isCancelAtPeriodEnd
230+
? 'text-muted-foreground hover:border-green-500 hover:bg-green-500 hover:text-white dark:hover:border-green-500 dark:hover:bg-green-500'
231+
: 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500'
221232
)}
222233
>
223-
{error ? 'Error' : 'Manage'}
234+
{error ? 'Error' : isCancelAtPeriodEnd ? 'Restore' : 'Manage'}
224235
</Button>
225236
</div>
226237

227238
<AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
228239
<AlertDialogContent>
229240
<AlertDialogHeader>
230241
<AlertDialogTitle>
231-
{isCancelAtPeriodEnd ? 'Manage' : 'Cancel'} {subscription.plan} subscription?
242+
{isCancelAtPeriodEnd ? 'Restore' : 'Cancel'} {subscription.plan} subscription?
232243
</AlertDialogTitle>
233244
<AlertDialogDescription>
234245
{isCancelAtPeriodEnd
235-
? 'Your subscription is set to cancel at the end of the billing period. You can reactivate it or manage other settings.'
246+
? 'Your subscription is set to cancel at the end of the billing period. Would you like to keep your subscription active?'
236247
: `You'll be redirected to Stripe to manage your subscription. You'll keep access until ${formatDate(
237248
periodEndDate
238249
)}, then downgrade to free plan.`}{' '}
@@ -260,38 +271,23 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
260271
<AlertDialogFooter className='flex'>
261272
<AlertDialogCancel
262273
className='h-9 w-full rounded-[8px]'
263-
onClick={handleKeep}
274+
onClick={isCancelAtPeriodEnd ? () => setIsDialogOpen(false) : handleKeep}
264275
disabled={isLoading}
265276
>
266-
Keep Subscription
277+
{isCancelAtPeriodEnd ? 'Cancel' : 'Keep Subscription'}
267278
</AlertDialogCancel>
268279

269280
{(() => {
270281
const subscriptionStatus = getSubscriptionStatus()
271-
if (
272-
subscriptionStatus.isPaid &&
273-
(activeOrganization?.id
274-
? useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd
275-
: false)
276-
) {
282+
if (subscriptionStatus.isPaid && isCancelAtPeriodEnd) {
277283
return (
278-
<TooltipProvider delayDuration={0}>
279-
<Tooltip>
280-
<TooltipTrigger asChild>
281-
<div className='w-full'>
282-
<AlertDialogAction
283-
disabled
284-
className='h-9 w-full cursor-not-allowed rounded-[8px] bg-muted text-muted-foreground opacity-50'
285-
>
286-
Continue
287-
</AlertDialogAction>
288-
</div>
289-
</TooltipTrigger>
290-
<TooltipContent side='top'>
291-
<p>Subscription will be cancelled at end of billing period</p>
292-
</TooltipContent>
293-
</Tooltip>
294-
</TooltipProvider>
284+
<AlertDialogAction
285+
onClick={handleKeep}
286+
className='h-9 w-full rounded-[8px] bg-green-500 text-white transition-all duration-200 hover:bg-green-600 dark:bg-green-500 dark:hover:bg-green-600'
287+
disabled={isLoading}
288+
>
289+
{isLoading ? 'Restoring...' : 'Restore Subscription'}
290+
</AlertDialogAction>
295291
)
296292
}
297293
return (

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
523523
}}
524524
subscriptionData={{
525525
periodEnd: subscriptionData?.periodEnd || null,
526+
cancelAtPeriodEnd: subscriptionData?.cancelAtPeriodEnd,
526527
}}
527528
/>
528529
</div>

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export async function getSimplifiedBillingSummary(
220220
metadata: any
221221
stripeSubscriptionId: string | null
222222
periodEnd: Date | string | null
223+
cancelAtPeriodEnd?: boolean
223224
// Usage details
224225
usage: {
225226
current: number
@@ -318,6 +319,7 @@ export async function getSimplifiedBillingSummary(
318319
metadata: subscription.metadata || null,
319320
stripeSubscriptionId: subscription.stripeSubscriptionId || null,
320321
periodEnd: subscription.periodEnd || null,
322+
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd || undefined,
321323
// Usage details
322324
usage: {
323325
current: usageData.currentUsage,
@@ -393,6 +395,7 @@ export async function getSimplifiedBillingSummary(
393395
metadata: subscription?.metadata || null,
394396
stripeSubscriptionId: subscription?.stripeSubscriptionId || null,
395397
periodEnd: subscription?.periodEnd || null,
398+
cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd || undefined,
396399
// Usage details
397400
usage: {
398401
current: currentUsage,
@@ -450,5 +453,14 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') {
450453
lastPeriodCost: 0,
451454
daysRemaining: 0,
452455
},
456+
...(type === 'organization' && {
457+
organizationData: {
458+
seatCount: 0,
459+
memberCount: 0,
460+
totalBasePrice: 0,
461+
totalCurrentUsage: 0,
462+
totalOverage: 0,
463+
},
464+
}),
453465
}
454466
}

apps/sim/stores/subscription/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface SubscriptionData {
2929
metadata: any | null
3030
stripeSubscriptionId: string | null
3131
periodEnd: Date | null
32+
cancelAtPeriodEnd?: boolean
3233
usage: UsageData
3334
billingBlocked?: boolean
3435
}

0 commit comments

Comments
 (0)