CreditBalance.tsx102 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import { useInView } from 'react-intersection-observer'
4
5import { CreditCodeRedemption } from './CreditCodeRedemption'
6import { CreditTopUp } from './CreditTopUp'
7import {
8 ScaffoldSection,
9 ScaffoldSectionContent,
10 ScaffoldSectionDetail,
11} from '@/components/layouts/Scaffold'
12import AlertError from '@/components/ui/AlertError'
13import { FormPanel } from '@/components/ui/Forms/FormPanel'
14import { FormSection, FormSectionContent } from '@/components/ui/Forms/FormSection'
15import NoPermission from '@/components/ui/NoPermission'
16import { useOrgBalanceQuery } from '@/data/subscriptions/org-balance-query'
17import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
18
19const CreditBalance = () => {
20 const { slug } = useParams()
21
22 const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = useAsyncCheckPermissions(
23 PermissionAction.BILLING_READ,
24 'stripe.subscriptions'
25 )
26 const { ref, inView } = useInView({ triggerOnce: true })
27
28 // Endpoint is expensive, so we only load it if it's in view
29 const {
30 data: balanceData,
31 error: balanceError,
32 isPending: isBalanceLoading,
33 isError: isBalanceError,
34 isSuccess: isBalanceSuccess,
35 } = useOrgBalanceQuery({ orgSlug: slug }, { enabled: canReadSubscriptions && inView })
36
37 const combinedCreditBalanceCents = balanceData?.total_balance_cents ?? 0
38 const combinedCreditBalance = combinedCreditBalanceCents / 100
39 const hasCredits = combinedCreditBalanceCents > 0
40 const hasDebt = combinedCreditBalanceCents < 0
41 const balance = Math.abs(combinedCreditBalance).toFixed(2)
42
43 return (
44 <ScaffoldSection ref={ref}>
45 <ScaffoldSectionDetail>
46 <div className="sticky space-y-2 top-12 pr-3">
47 <div className="flex items-center space-x-2">
48 <p className="text-foreground text-base m-0">Credit Balance</p>
49 </div>
50 <p className="text-sm text-foreground-light m-0">
51 Credits will be applied to future invoices, before charging your payment method. This
52 balance includes purchased credits and any prorated credits from plan changes.
53 </p>
54 <p className="text-sm text-foreground-light m-0">
55 If your credits run out, your default payment method will be charged.
56 </p>
57 </div>
58 </ScaffoldSectionDetail>
59 <ScaffoldSectionContent>
60 {isPermissionsLoaded && !canReadSubscriptions ? (
61 <NoPermission resourceText="view this organization's credits" />
62 ) : (
63 <FormPanel
64 footer={
65 balanceData?.billing_via_partner ? undefined : (
66 <div className="flex justify-end items-center py-4 px-8 gap-x-2">
67 <CreditCodeRedemption slug={slug} />
68 <CreditTopUp slug={slug} />
69 </div>
70 )
71 }
72 >
73 <FormSection>
74 <FormSectionContent fullWidth loading={isBalanceLoading}>
75 {isBalanceError && (
76 <AlertError
77 subject="Failed to retrieve organization customer profile"
78 error={balanceError}
79 />
80 )}
81
82 {isBalanceSuccess && (
83 <div className="flex w-full justify-between items-center">
84 <span>Balance</span>
85 <div className="flex items-center space-x-1">
86 {hasDebt && <h4 className="opacity-50">-</h4>}
87 <h4 className="opacity-50">$</h4>
88 <h1 className="relative">{balance}</h1>
89 {hasCredits && <h4 className="opacity-50">/credits</h4>}
90 </div>
91 </div>
92 )}
93 </FormSectionContent>
94 </FormSection>
95 </FormPanel>
96 )}
97 </ScaffoldSectionContent>
98 </ScaffoldSection>
99 )
100}
101
102export default CreditBalance