1+ /* eslint-disable prettier/prettier */
2+
3+ /* eslint-disable react/jsx-no-undef */
14"use client" ;
25
36import { useSession } from "next-auth/react" ;
@@ -22,6 +25,9 @@ import { showToast } from "@calcom/ui/components/toast";
2225
2326import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton" ;
2427
28+ /* eslint-disable prettier/prettier */
29+ /* eslint-disable react/jsx-no-undef */
30+
2531type MonthOption = {
2632 value : string ;
2733 label : string ;
@@ -62,6 +68,8 @@ export default function BillingCredits() {
6268 const [ isDownloading , setIsDownloading ] = useState ( false ) ;
6369 const utils = trpc . useUtils ( ) ;
6470
71+ const [ showAutoRechargeModal , setShowAutoRechargeModal ] = useState ( false ) ;
72+
6573 const {
6674 register,
6775 handleSubmit,
@@ -91,6 +99,11 @@ export default function BillingCredits() {
9199 { enabled : shouldRender }
92100 ) ;
93101
102+ const { data : autoRechargeData } = trpc . viewer . credits . getAutoRechargeSettings . useQuery (
103+ { teamId } ,
104+ { enabled : shouldRender }
105+ ) ;
106+
94107 if ( ! shouldRender ) return null ;
95108
96109 const buyCreditsMutation = trpc . viewer . credits . buyCredits . useMutation ( {
@@ -104,6 +117,17 @@ export default function BillingCredits() {
104117 } ,
105118 } ) ;
106119
120+ const updateAutoRechargeMutation = trpc . viewer . credits . updateAutoRechargeSettings . useMutation ( {
121+ onSuccess : ( ) => {
122+ showToast ( t ( "auto_recharge_settings_updated" ) , "success" ) ;
123+ setShowAutoRechargeModal ( false ) ;
124+ utils . viewer . credits . getAutoRechargeSettings . invalidate ( { teamId } ) ;
125+ } ,
126+ onError : ( ) => {
127+ showToast ( t ( "auto_recharge_settings_failed" ) , "error" ) ;
128+ } ,
129+ } ) ;
130+
107131 const handleDownload = async ( ) => {
108132 setIsDownloading ( true ) ;
109133 try {
@@ -132,11 +156,22 @@ export default function BillingCredits() {
132156 buyCreditsMutation . mutate ( { quantity : data . quantity , teamId } ) ;
133157 } ;
134158
159+ const handleAutoRechargeSubmit = ( data : { enabled : boolean ; threshold : number ; amount : number } ) => {
160+ updateAutoRechargeMutation . mutate ( {
161+ teamId,
162+ enabled : data . enabled ,
163+ threshold : data . threshold ,
164+ amount : data . amount ,
165+ } ) ;
166+ } ;
167+
135168 const teamCreditsPercentageUsed =
136169 creditsData . credits . totalMonthlyCredits > 0
137170 ? ( creditsData . credits . totalRemainingMonthlyCredits / creditsData . credits . totalMonthlyCredits ) * 100
138171 : 0 ;
139172
173+ const autoRechargeSettings = autoRechargeData ?. settings ;
174+
140175 return (
141176 < div className = "border-subtle mt-8 space-y-6 rounded-lg border px-6 py-6 pb-6 text-sm sm:space-y-8" >
142177 < div >
@@ -186,6 +221,38 @@ export default function BillingCredits() {
186221 { creditsData . credits . totalMonthlyCredits ? t ( "additional_credits" ) : t ( "available_credits" ) }
187222 </ Label >
188223 < div className = "mt-2 text-sm" > { creditsData . credits . additionalCredits } </ div >
224+
225+ { /* Auto-recharge section */ }
226+ < div className = "-mx-6 mb-6 mt-6" >
227+ < hr className = "border-subtle mb-3 mt-3" />
228+ </ div >
229+ < div className = "mb-4 flex items-center justify-between" >
230+ < div >
231+ < Label > { t ( "auto_recharge" ) } </ Label >
232+ < p className = "text-subtle mt-1 text-sm" >
233+ { autoRechargeSettings ?. enabled
234+ ? t ( "auto_recharge_enabled_description" , {
235+ threshold : autoRechargeSettings ?. threshold ,
236+ amount : autoRechargeSettings ?. amount ,
237+ } )
238+ : t ( "auto_recharge_disabled_description" ) }
239+ </ p >
240+ { autoRechargeSettings ?. lastAutoRechargeAt && (
241+ < p className = "text-subtle mt-1 text-sm" >
242+ { t ( "last_auto_recharged_at" , {
243+ date : dayjs ( autoRechargeSettings . lastAutoRechargeAt ) . format ( "MMM D, YYYY HH:mm" ) ,
244+ } ) }
245+ </ p >
246+ ) }
247+ </ div >
248+ < Button
249+ color = "secondary"
250+ onClick = { ( ) => setShowAutoRechargeModal ( true ) }
251+ data-testid = "configure-auto-recharge" >
252+ { autoRechargeSettings ?. enabled ? t ( "edit" ) : t ( "setup" ) }
253+ </ Button >
254+ </ div >
255+
189256 < div className = "-mx-6 mb-6 mt-6" >
190257 < hr className = "border-subtle mb-3 mt-3" />
191258 </ div >
@@ -243,6 +310,124 @@ export default function BillingCredits() {
243310 </ div >
244311 </ div >
245312 </ div >
313+
314+ { /* Auto-recharge modal */ }
315+ { showAutoRechargeModal && (
316+ < AutoRechargeModal
317+ defaultValues = { autoRechargeSettings }
318+ onSubmit = { handleAutoRechargeSubmit }
319+ onCancel = { ( ) => setShowAutoRechargeModal ( false ) }
320+ isLoading = { updateAutoRechargeMutation . isLoading }
321+ />
322+ ) }
246323 </ div >
247324 ) ;
248325}
326+
327+ function AutoRechargeModal ( {
328+ defaultValues,
329+ onSubmit,
330+ onCancel,
331+ isLoading,
332+ } : {
333+ defaultValues ?: {
334+ enabled : boolean ;
335+ threshold : number ;
336+ amount : number ;
337+ } ;
338+ onSubmit : ( data : { enabled : boolean ; threshold : number ; amount : number } ) => void ;
339+ onCancel : ( ) => void ;
340+ isLoading : boolean ;
341+ } ) {
342+ const { t } = useLocale ( ) ;
343+ const {
344+ register,
345+ handleSubmit,
346+ watch,
347+ formState : { errors } ,
348+ } = useForm < {
349+ enabled : boolean ;
350+ threshold : number ;
351+ amount : number ;
352+ } > ( {
353+ defaultValues : {
354+ enabled : defaultValues ?. enabled ?? false ,
355+ threshold : defaultValues ?. threshold ?? 50 ,
356+ amount : defaultValues ?. amount ?? 100 ,
357+ } ,
358+ } ) ;
359+
360+ const enabled = watch ( "enabled" ) ;
361+
362+ return (
363+ < Dialog open onOpenChange = { onCancel } >
364+ < DialogContent >
365+ < DialogHeader >
366+ < DialogTitle > { t ( "auto_recharge_settings" ) } </ DialogTitle >
367+ < DialogDescription > { t ( "auto_recharge_description" ) } </ DialogDescription >
368+ </ DialogHeader >
369+ < form onSubmit = { handleSubmit ( onSubmit ) } >
370+ < div className = "space-y-4 py-4" >
371+ < div className = "flex items-center" >
372+ < Switch
373+ { ...register ( "enabled" ) }
374+ defaultChecked = { defaultValues ?. enabled }
375+ id = "auto-recharge-toggle"
376+ />
377+ < Label className = "ml-2" htmlFor = "auto-recharge-toggle" >
378+ { t ( "enable_auto_recharge" ) }
379+ </ Label >
380+ </ div >
381+
382+ { enabled && (
383+ < >
384+ < div >
385+ < Label htmlFor = "threshold" > { t ( "recharge_threshold" ) } </ Label >
386+ < TextField
387+ id = "threshold"
388+ type = "number"
389+ { ...register ( "threshold" , {
390+ required : t ( "error_required_field" ) ,
391+ min : { value : 10 , message : t ( "minimum_threshold" ) } ,
392+ valueAsNumber : true ,
393+ } ) }
394+ placeholder = "50"
395+ />
396+ { errors . threshold && (
397+ < InputError message = { errors . threshold . message ?? t ( "invalid_input" ) } />
398+ ) }
399+ < p className = "text-subtle mt-1 text-sm" > { t ( "threshold_description" ) } </ p >
400+ </ div >
401+
402+ < div >
403+ < Label htmlFor = "amount" > { t ( "recharge_amount" ) } </ Label >
404+ < TextField
405+ id = "amount"
406+ type = "number"
407+ { ...register ( "amount" , {
408+ required : t ( "error_required_field" ) ,
409+ min : { value : 50 , message : t ( "minimum_amount" ) } ,
410+ valueAsNumber : true ,
411+ } ) }
412+ placeholder = "100"
413+ />
414+ { errors . amount && < InputError message = { errors . amount . message ?? t ( "invalid_input" ) } /> }
415+ < p className = "text-subtle mt-1 text-sm" > { t ( "amount_description" ) } </ p >
416+ </ div >
417+ </ >
418+ ) }
419+ </ div >
420+
421+ < DialogFooter >
422+ < Button type = "button" color = "secondary" onClick = { onCancel } >
423+ { t ( "cancel" ) }
424+ </ Button >
425+ < Button type = "submit" loading = { isLoading } >
426+ { t ( "save" ) }
427+ </ Button >
428+ </ DialogFooter >
429+ </ form >
430+ </ DialogContent >
431+ </ Dialog >
432+ ) ;
433+ }
0 commit comments