CreditBalance.tsx 3.9 KB

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