@@ -55,9 +55,33 @@ function SubscriptionActive({ data, email }: { data: ActiveSubscriptionResponse;
5555 const { subscription, rateLimit, fallbackToALaCarte } = data
5656 const isCanceling = subscription . cancelAtPeriodEnd
5757 const fallbackPortalUrl = `${ env . NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL } ?prefilled_email=${ encodeURIComponent ( email ) } `
58- const billingPortalUrl = data . billingPortalUrl ?? fallbackPortalUrl
5958 const queryClient = useQueryClient ( )
6059
60+ const billingPortalMutation = useMutation ( {
61+ mutationFn : async ( ) => {
62+ const res = await fetch ( '/api/user/billing-portal' , {
63+ method : 'POST' ,
64+ } )
65+ if ( ! res . ok ) {
66+ const error = await res . json ( ) . catch ( ( ) => ( { error : 'Failed to open billing portal' } ) )
67+ throw new Error ( error . error || 'Failed to open billing portal' )
68+ }
69+ const data = await res . json ( )
70+ return data . url as string
71+ } ,
72+ onSuccess : ( url ) => {
73+ window . open ( url , '_blank' , 'noopener,noreferrer' )
74+ } ,
75+ onError : ( err : Error ) => {
76+ // Fall back to the prefilled email portal URL on error
77+ window . open ( fallbackPortalUrl , '_blank' , 'noopener,noreferrer' )
78+ toast ( {
79+ title : 'Note' ,
80+ description : 'Opened billing portal - you may need to sign in.' ,
81+ } )
82+ } ,
83+ } )
84+
6185 const updatePreferenceMutation = useMutation ( {
6286 mutationFn : async ( newValue : boolean ) => {
6387 const res = await fetch ( '/api/user/preferences' , {
@@ -116,15 +140,23 @@ function SubscriptionActive({ data, email }: { data: ActiveSubscriptionResponse;
116140 </ span >
117141 ) }
118142 </ CardTitle >
119- < a
120- href = { billingPortalUrl }
121- target = "_blank"
122- rel = "noopener noreferrer"
123- className = "text-sm text-muted-foreground hover:text-foreground flex items-center gap-1"
143+ < button
144+ onClick = { ( ) => billingPortalMutation . mutate ( ) }
145+ disabled = { billingPortalMutation . isPending }
146+ className = "text-sm text-muted-foreground hover:text-foreground flex items-center gap-1 disabled:opacity-50"
124147 >
125- Manage
126- < ExternalLink className = "h-3.5 w-3.5" />
127- </ a >
148+ { billingPortalMutation . isPending ? (
149+ < >
150+ < Loader2 className = "h-3.5 w-3.5 animate-spin" />
151+ Opening...
152+ </ >
153+ ) : (
154+ < >
155+ Manage
156+ < ExternalLink className = "h-3.5 w-3.5" />
157+ </ >
158+ ) }
159+ </ button >
128160 </ div >
129161 </ CardHeader >
130162 < CardContent className = "space-y-5" >
0 commit comments