Skip to content

Commit e29b8cd

Browse files
committed
Simplify subscription plan to use on manage subscription button
1 parent a5589d8 commit e29b8cd

File tree

1 file changed

+24
-213
lines changed

1 file changed

+24
-213
lines changed

web/src/app/profile/components/subscription-section.tsx

Lines changed: 24 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,21 @@
11
'use client'
22

3-
import {
4-
SUBSCRIPTION_DISPLAY_NAME,
5-
SUBSCRIPTION_TIERS,
6-
} from '@codebuff/common/constants/subscription-plans'
7-
8-
import type { SubscriptionTierPrice } from '@codebuff/common/constants/subscription-plans'
9-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3+
import { SUBSCRIPTION_DISPLAY_NAME } from '@codebuff/common/constants/subscription-plans'
4+
import { env } from '@codebuff/common/env'
5+
import { useQuery } from '@tanstack/react-query'
106
import {
117
Zap,
128
Clock,
139
CalendarDays,
14-
Loader2,
1510
AlertTriangle,
16-
ArrowRightLeft,
11+
ExternalLink,
12+
Loader2,
1713
} from 'lucide-react'
1814
import Link from 'next/link'
1915
import { useSession } from 'next-auth/react'
20-
import { useState } from 'react'
2116

2217
import { Button } from '@/components/ui/button'
2318
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
24-
import {
25-
Dialog,
26-
DialogContent,
27-
DialogDescription,
28-
DialogFooter,
29-
DialogHeader,
30-
DialogTitle,
31-
} from '@/components/ui/dialog'
32-
import { toast } from '@/components/ui/use-toast'
3319
import { cn } from '@/lib/utils'
3420

3521
interface SubscriptionApiResponse {
@@ -129,77 +115,15 @@ function ProgressBar({
129115

130116
function SubscriptionActive({
131117
data,
118+
email,
132119
}: {
133120
data: SubscriptionApiResponse
121+
email: string
134122
}) {
135-
const queryClient = useQueryClient()
136-
const [showCancelDialog, setShowCancelDialog] = useState(false)
137-
const [showChangePlanDialog, setShowChangePlanDialog] = useState(false)
138-
139-
const cancelMutation = useMutation({
140-
mutationFn: async () => {
141-
const response = await fetch('/api/stripe/cancel-subscription', {
142-
method: 'POST',
143-
})
144-
if (!response.ok) {
145-
const err = await response.json().catch(() => ({}))
146-
throw new Error(err.error || 'Failed to cancel subscription')
147-
}
148-
return response.json()
149-
},
150-
onSuccess: () => {
151-
setShowCancelDialog(false)
152-
queryClient.invalidateQueries({ queryKey: ['subscription'] })
153-
toast({
154-
title: 'Subscription canceled',
155-
description: `Your ${SUBSCRIPTION_DISPLAY_NAME} subscription will remain active until the end of your billing period.`,
156-
})
157-
},
158-
onError: (error: Error) => {
159-
setShowCancelDialog(false)
160-
toast({
161-
title: 'Error',
162-
description: error.message,
163-
variant: 'destructive',
164-
})
165-
},
166-
})
167-
168-
const changeTierMutation = useMutation({
169-
mutationFn: async (selectedTier: SubscriptionTierPrice) => {
170-
const response = await fetch('/api/stripe/change-subscription-tier', {
171-
method: 'POST',
172-
headers: { 'Content-Type': 'application/json' },
173-
body: JSON.stringify({ tier: selectedTier }),
174-
})
175-
if (!response.ok) {
176-
const err = await response.json().catch(() => ({}))
177-
throw new Error(err.error || 'Failed to change plan')
178-
}
179-
return response.json()
180-
},
181-
onSuccess: () => {
182-
setShowChangePlanDialog(false)
183-
queryClient.invalidateQueries({ queryKey: ['subscription'] })
184-
toast({
185-
title: 'Plan changed',
186-
description: 'Your subscription plan has been updated.',
187-
})
188-
},
189-
onError: (error: Error) => {
190-
setShowChangePlanDialog(false)
191-
toast({
192-
title: 'Error',
193-
description: error.message,
194-
variant: 'destructive',
195-
})
196-
},
197-
})
198-
199123
const { subscription, rateLimit } = data
200124

201125
const isCanceling = subscription?.cancelAtPeriodEnd
202-
const currentTier = (subscription?.tier ?? 200) as SubscriptionTierPrice
126+
const billingPortalUrl = `${env.NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL}?prefilled_email=${encodeURIComponent(email)}`
203127

204128
return (
205129
<Card>
@@ -301,141 +225,26 @@ function SubscriptionActive({
301225
</>
302226
)}
303227

304-
{/* Billing info & cancel */}
228+
{/* Billing info & manage */}
305229
<div className="flex items-center justify-between border-t pt-4">
306230
<p className="text-sm text-muted-foreground">
307231
{isCanceling
308232
? `Cancels ${subscription ? formatDate(subscription.billingPeriodEnd) : ''}`
309233
: `Renews ${subscription ? formatDate(subscription.billingPeriodEnd) : ''}`}
310234
</p>
311-
{!isCanceling && (
312-
<div className="flex items-center gap-2">
313-
<Button
314-
variant="ghost"
315-
size="sm"
316-
className="text-muted-foreground"
317-
onClick={() => setShowChangePlanDialog(true)}
318-
>
319-
<ArrowRightLeft className="mr-1 h-3 w-3" />
320-
Change Plan
321-
</Button>
322-
<Button
323-
variant="ghost"
324-
size="sm"
325-
className="text-muted-foreground hover:text-destructive"
326-
onClick={() => setShowCancelDialog(true)}
327-
>
328-
Cancel Subscription
329-
</Button>
330-
</div>
331-
)}
235+
<Button
236+
variant="ghost"
237+
size="sm"
238+
className="text-muted-foreground"
239+
asChild
240+
>
241+
<a href={billingPortalUrl} target="_blank" rel="noopener noreferrer">
242+
<ExternalLink className="mr-1.5 h-3.5 w-3.5" />
243+
Manage Subscription
244+
</a>
245+
</Button>
332246
</div>
333247
</CardContent>
334-
335-
<Dialog open={showCancelDialog} onOpenChange={setShowCancelDialog}>
336-
<DialogContent>
337-
<DialogHeader>
338-
<DialogTitle>Cancel subscription?</DialogTitle>
339-
<DialogDescription>
340-
Your {SUBSCRIPTION_DISPLAY_NAME} subscription will remain active
341-
until{' '}
342-
{subscription
343-
? formatDate(subscription.billingPeriodEnd)
344-
: 'the end of your billing period'}
345-
. After that, you'll return to the free tier.
346-
</DialogDescription>
347-
</DialogHeader>
348-
<DialogFooter>
349-
<Button
350-
variant="outline"
351-
onClick={() => setShowCancelDialog(false)}
352-
disabled={cancelMutation.isPending}
353-
>
354-
Keep Subscription
355-
</Button>
356-
<Button
357-
variant="destructive"
358-
onClick={() => cancelMutation.mutate()}
359-
disabled={cancelMutation.isPending}
360-
>
361-
{cancelMutation.isPending ? (
362-
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
363-
) : null}
364-
Yes, Cancel
365-
</Button>
366-
</DialogFooter>
367-
</DialogContent>
368-
</Dialog>
369-
370-
<Dialog open={showChangePlanDialog} onOpenChange={setShowChangePlanDialog}>
371-
<DialogContent>
372-
<DialogHeader>
373-
<DialogTitle>Change Plan</DialogTitle>
374-
<DialogDescription>
375-
Select a new plan for your {SUBSCRIPTION_DISPLAY_NAME} subscription. The change takes effect immediately with a prorated charge.
376-
</DialogDescription>
377-
</DialogHeader>
378-
<div className="flex flex-col gap-3 py-2">
379-
{Object.entries(SUBSCRIPTION_TIERS).map(
380-
([key, tier]) => {
381-
const tierPrice = Number(key) as SubscriptionTierPrice
382-
const isCurrent = tierPrice === currentTier
383-
const tierName =
384-
tierPrice === 100
385-
? 'Starter'
386-
: tierPrice === 200
387-
? 'Pro'
388-
: 'Team'
389-
const tierDescription =
390-
tierPrice === 100
391-
? 'Great for individuals getting started.'
392-
: tierPrice === 200
393-
? 'For professionals who need more capacity.'
394-
: 'For power users and teams with heavy workloads.'
395-
return (
396-
<button
397-
key={tierPrice}
398-
disabled={isCurrent || changeTierMutation.isPending}
399-
onClick={() => changeTierMutation.mutate(tierPrice)}
400-
className={cn(
401-
'flex items-center justify-between rounded-lg border p-4 text-left transition-colors',
402-
isCurrent
403-
? 'cursor-default border-indigo-300 bg-indigo-50 dark:border-indigo-700 dark:bg-indigo-900/20'
404-
: 'hover:border-indigo-300 hover:bg-muted dark:hover:border-indigo-700',
405-
)}
406-
>
407-
<div>
408-
<div className="flex items-center gap-2">
409-
<span className="font-semibold">{tierName}</span>
410-
{isCurrent && (
411-
<span className="inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-medium text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400">
412-
Current
413-
</span>
414-
)}
415-
</div>
416-
<p className="mt-0.5 text-sm text-muted-foreground">
417-
{tierDescription}
418-
</p>
419-
</div>
420-
<span className="ml-4 text-lg font-semibold">
421-
${tier.monthlyPrice}/mo
422-
</span>
423-
</button>
424-
)
425-
},
426-
)}
427-
</div>
428-
<DialogFooter>
429-
<Button
430-
variant="outline"
431-
onClick={() => setShowChangePlanDialog(false)}
432-
disabled={changeTierMutation.isPending}
433-
>
434-
Cancel
435-
</Button>
436-
</DialogFooter>
437-
</DialogContent>
438-
</Dialog>
439248
</Card>
440249
)
441250
}
@@ -469,7 +278,7 @@ function SubscriptionCta() {
469278
}
470279

471280
export function SubscriptionSection() {
472-
const { status } = useSession()
281+
const { data: session, status } = useSession()
473282

474283
const { data, isLoading } = useQuery<SubscriptionApiResponse>({
475284
queryKey: ['subscription'],
@@ -500,5 +309,7 @@ export function SubscriptionSection() {
500309
return <SubscriptionCta />
501310
}
502311

503-
return <SubscriptionActive data={data} />
312+
const email = session?.user?.email || ''
313+
314+
return <SubscriptionActive data={data} email={email} />
504315
}

0 commit comments

Comments
 (0)