1+ import { env } from '@codebuff/common/env'
2+ import { useMutation } from '@tanstack/react-query'
3+ import { ExternalLink , Loader2 } from 'lucide-react'
4+
15import { CreditManagementSkeleton } from './CreditManagementSkeleton'
26import { CreditPurchaseSection } from './CreditPurchaseSection'
37
48import { AutoTopupSettings } from '@/components/auto-topup/AutoTopupSettings'
59import { OrgAutoTopupSettings } from '@/components/auto-topup/OrgAutoTopupSettings'
10+ import { Button } from '@/components/ui/button'
11+ import { toast } from '@/components/ui/use-toast'
612
713export interface CreditManagementSectionProps {
814 onPurchase : ( credits : number ) => void
@@ -13,7 +19,7 @@ export interface CreditManagementSectionProps {
1319 organizationId ?: string
1420 isOrganization ?: boolean // Keep for backward compatibility
1521 isLoading ?: boolean
16- billingPortalUrl ?: string
22+ email ?: string
1723}
1824
1925export { CreditManagementSkeleton }
@@ -27,11 +33,40 @@ export function CreditManagementSection({
2733 organizationId,
2834 isOrganization = false ,
2935 isLoading = false ,
30- billingPortalUrl ,
36+ email ,
3137} : CreditManagementSectionProps ) {
3238 // Determine if we're in organization context
3339 const isOrgContext = context === 'organization' || isOrganization
3440
41+ const fallbackPortalUrl = email
42+ ? `${ env . NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL } ?prefilled_email=${ encodeURIComponent ( email ) } `
43+ : env . NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL
44+
45+ const billingPortalMutation = useMutation ( {
46+ mutationFn : async ( ) => {
47+ const res = await fetch ( '/api/user/billing-portal' , {
48+ method : 'POST' ,
49+ } )
50+ if ( ! res . ok ) {
51+ const error = await res . json ( ) . catch ( ( ) => ( { error : 'Failed to open billing portal' } ) )
52+ throw new Error ( error . error || 'Failed to open billing portal' )
53+ }
54+ const data = await res . json ( )
55+ return data . url as string
56+ } ,
57+ onSuccess : ( url ) => {
58+ window . open ( url , '_blank' , 'noopener,noreferrer' )
59+ } ,
60+ onError : ( ) => {
61+ // Fall back to the prefilled email portal URL on error
62+ window . open ( fallbackPortalUrl , '_blank' , 'noopener,noreferrer' )
63+ toast ( {
64+ title : 'Note' ,
65+ description : 'Opened billing portal - you may need to sign in.' ,
66+ } )
67+ } ,
68+ } )
69+
3570 if ( isLoading ) {
3671 return < CreditManagementSkeleton />
3772 }
@@ -41,15 +76,24 @@ export function CreditManagementSection({
4176 < div className = "space-y-8" >
4277 < div className = "flex items-center justify-between" >
4378 < h3 className = "text-2xl font-bold" > Buy Credits</ h3 >
44- { billingPortalUrl && (
45- < a
46- href = { billingPortalUrl }
47- target = "_blank"
48- rel = "noopener noreferrer"
49- className = "text-sm text-primary underline underline-offset-4 hover:text-primary/90"
79+ { /* Only show billing portal button for user context - orgs have their own button */ }
80+ { ! isOrgContext && (
81+ < Button
82+ variant = "link"
83+ size = "sm"
84+ onClick = { ( ) => billingPortalMutation . mutate ( ) }
85+ disabled = { billingPortalMutation . isPending }
86+ className = "text-sm text-primary underline underline-offset-4 hover:text-primary/90 p-0 h-auto"
5087 >
51- Billing Portal →
52- </ a >
88+ { billingPortalMutation . isPending ? (
89+ < >
90+ < Loader2 className = "mr-1 h-3 w-3 animate-spin" />
91+ Opening...
92+ </ >
93+ ) : (
94+ < > Billing Portal < ExternalLink className = "ml-1 h-3 w-3" /> </ >
95+ ) }
96+ </ Button >
5397 ) }
5498 </ div >
5599 < CreditPurchaseSection
0 commit comments