11"use client" ;
22
3+ import { useMutation } from "@tanstack/react-query" ;
4+ import { toast } from "sonner" ;
35import type {
46 BillingBillingPortalAction ,
57 BillingPortalOptions ,
68 RedirectBillingCheckoutAction ,
79 RedirectCheckoutOptions ,
810} from "../actions/billing" ;
11+ import { cn } from "../lib/utils" ;
12+ import { Spinner } from "./ui/Spinner/Spinner" ;
913import { Button , type ButtonProps } from "./ui/button" ;
1014
1115type CheckoutButtonProps = Omit < RedirectCheckoutOptions , "redirectUrl" > &
1216 ButtonProps & {
13- redirectPath : string ;
1417 redirectToCheckout : RedirectBillingCheckoutAction ;
1518 } ;
1619
@@ -19,60 +22,108 @@ export function CheckoutButton({
1922 teamSlug,
2023 sku,
2124 metadata,
22- redirectPath,
2325 children,
2426 redirectToCheckout,
2527 ...restProps
2628} : CheckoutButtonProps ) {
29+ const redirectMutation = useMutation ( {
30+ mutationFn : async ( ) => {
31+ return redirectToCheckout ( {
32+ teamSlug,
33+ sku,
34+ metadata,
35+ redirectUrl : getAbsoluteUrl ( "/stripe-redirect" ) ,
36+ } ) ;
37+ } ,
38+ } ) ;
39+
2740 return (
2841 < Button
2942 { ...restProps }
43+ className = { cn ( restProps . className , "gap-2" ) }
44+ disabled = { redirectMutation . isPending || restProps . disabled }
3045 onClick = { async ( e ) => {
3146 onClick ?.( e ) ;
32- await redirectToCheckout ( {
33- teamSlug,
34- sku,
35- metadata,
36- redirectUrl : getRedirectUrl ( redirectPath ) ,
47+ redirectMutation . mutate ( undefined , {
48+ onSuccess : ( res ) => {
49+ if ( ! res . url ) {
50+ toast . error ( "Failed to open checkout page" ) ;
51+ return ;
52+ }
53+
54+ const tab = window . open ( res . url , "_blank" ) ;
55+
56+ if ( ! tab ) {
57+ toast . error ( "Failed to open checkout page" ) ;
58+ return ;
59+ }
60+ } ,
61+ onError : ( ) => {
62+ toast . error ( "Failed to open checkout page" ) ;
63+ } ,
3764 } ) ;
3865 } }
3966 >
67+ { redirectMutation . isPending && < Spinner className = "size-4" /> }
4068 { children }
4169 </ Button >
4270 ) ;
4371}
4472
4573type BillingPortalButtonProps = Omit < BillingPortalOptions , "redirectUrl" > &
4674 ButtonProps & {
47- redirectPath : string ;
4875 redirectToBillingPortal : BillingBillingPortalAction ;
4976 } ;
5077
5178export function BillingPortalButton ( {
5279 onClick,
5380 teamSlug,
54- redirectPath,
5581 children,
5682 redirectToBillingPortal,
5783 ...restProps
5884} : BillingPortalButtonProps ) {
85+ const redirectMutation = useMutation ( {
86+ mutationFn : async ( ) => {
87+ return redirectToBillingPortal ( {
88+ teamSlug,
89+ redirectUrl : getAbsoluteUrl ( "/stripe-redirect" ) ,
90+ } ) ;
91+ } ,
92+ } ) ;
93+
5994 return (
6095 < Button
6196 { ...restProps }
97+ className = { cn ( restProps . className , "gap-2" ) }
98+ disabled = { redirectMutation . isPending || restProps . disabled }
6299 onClick = { async ( e ) => {
63100 onClick ?.( e ) ;
64- await redirectToBillingPortal ( {
65- teamSlug,
66- redirectUrl : getRedirectUrl ( redirectPath ) ,
101+ redirectMutation . mutate ( undefined , {
102+ onSuccess ( res ) {
103+ if ( ! res . url ) {
104+ toast . error ( "Failed to open billing portal" ) ;
105+ return ;
106+ }
107+
108+ const tab = window . open ( res . url , "_blank" ) ;
109+ if ( ! tab ) {
110+ toast . error ( "Failed to open billing portal" ) ;
111+ return ;
112+ }
113+ } ,
114+ onError : ( ) => {
115+ toast . error ( "Failed to open billing portal" ) ;
116+ } ,
67117 } ) ;
68118 } }
69119 >
120+ { redirectMutation . isPending && < Spinner className = "size-4" /> }
70121 { children }
71122 </ Button >
72123 ) ;
73124}
74125
75- function getRedirectUrl ( path : string ) {
126+ function getAbsoluteUrl ( path : string ) {
76127 const url = new URL ( window . location . origin ) ;
77128 url . pathname = path ;
78129 return url . toString ( ) ;
0 commit comments