PaymentMethods.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/constants'
  2. import { useParams } from 'common'
  3. import { CreditCardIcon, ExternalLink, Plus } from 'lucide-react'
  4. import { useState } from 'react'
  5. import { toast } from 'sonner'
  6. import { Button } from 'ui'
  7. import { Admonition } from 'ui-patterns/admonition'
  8. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  9. import ChangePaymentMethodModal from './ChangePaymentMethodModal'
  10. import CreditCard from './CreditCard'
  11. import DeletePaymentMethodModal from './DeletePaymentMethodModal'
  12. import AddNewPaymentMethodModal from '@/components/interfaces/Billing/Payment/AddNewPaymentMethodModal'
  13. import { SupportLink } from '@/components/interfaces/Support/SupportLink'
  14. import {
  15. ScaffoldSection,
  16. ScaffoldSectionContent,
  17. ScaffoldSectionDetail,
  18. } from '@/components/layouts/Scaffold'
  19. import AlertError from '@/components/ui/AlertError'
  20. import { FormPanel } from '@/components/ui/Forms/FormPanel'
  21. import { FormSection, FormSectionContent } from '@/components/ui/Forms/FormSection'
  22. import NoPermission from '@/components/ui/NoPermission'
  23. import PartnerManagedResource from '@/components/ui/PartnerManagedResource'
  24. import { isPartnerBillingOrganization } from '@/data/organizations/managed-by-utils'
  25. import { useOrganizationPaymentMethodsQuery } from '@/data/organizations/organization-payment-methods-query'
  26. import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
  27. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  28. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  29. import { MANAGED_BY } from '@/lib/constants/infrastructure'
  30. import { getURL } from '@/lib/helpers'
  31. const PaymentMethods = () => {
  32. const { slug } = useParams()
  33. const { data: selectedOrganization } = useSelectedOrganizationQuery()
  34. const [selectedMethodForUse, setSelectedMethodForUse] = useState<any>()
  35. const [selectedMethodToDelete, setSelectedMethodToDelete] = useState<any>()
  36. const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(false)
  37. const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: slug })
  38. const {
  39. data: paymentMethods,
  40. error,
  41. isPending: isLoading,
  42. isError,
  43. isSuccess,
  44. } = useOrganizationPaymentMethodsQuery({ slug })
  45. const { can: canReadPaymentMethods, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
  46. PermissionAction.BILLING_READ,
  47. 'stripe.payment_methods'
  48. )
  49. const { can: canUpdatePaymentMethods } = useAsyncCheckPermissions(
  50. PermissionAction.BILLING_WRITE,
  51. 'stripe.payment_methods'
  52. )
  53. const isPartnerBilledOrganization = isPartnerBillingOrganization(
  54. selectedOrganization?.billing_partner
  55. )
  56. const isStripeManagedOrganization =
  57. selectedOrganization?.managed_by === MANAGED_BY.STRIPE_PROJECTS
  58. return (
  59. <>
  60. <ScaffoldSection>
  61. <ScaffoldSectionDetail>
  62. <div className="sticky space-y-2 top-12">
  63. <p className="text-foreground text-base m-0">Payment Methods</p>
  64. <p className="text-sm text-foreground-light mb-2 pr-4 m-0">
  65. {isStripeManagedOrganization
  66. ? 'Billing for this organisation is handled through Stripe Projects.'
  67. : 'Payments for your subscription are made using the default card.'}
  68. </p>
  69. </div>
  70. </ScaffoldSectionDetail>
  71. <ScaffoldSectionContent>
  72. {selectedOrganization && isPartnerBilledOrganization ? (
  73. <PartnerManagedResource
  74. managedBy={selectedOrganization?.managed_by}
  75. resource="Payment Methods"
  76. cta={{
  77. installationId: selectedOrganization?.partner_id,
  78. }}
  79. />
  80. ) : isPermissionsLoaded && !canReadPaymentMethods ? (
  81. <NoPermission resourceText="view this organization's payment methods" />
  82. ) : (
  83. <>
  84. {isLoading && (
  85. <div className="space-y-2">
  86. <ShimmeringLoader />
  87. <ShimmeringLoader className="w-3/4" />
  88. <ShimmeringLoader className="w-1/2" />
  89. </div>
  90. )}
  91. {isError && (
  92. <AlertError subject="Failed to retrieve payment methods" error={error as any} />
  93. )}
  94. {isSuccess && (
  95. <>
  96. {subscription?.payment_method_type === 'invoice' && (
  97. <Admonition
  98. type="note"
  99. layout="horizontal"
  100. title="Payment is currently by invoice"
  101. description={
  102. isStripeManagedOrganization
  103. ? 'You get a monthly invoice and payment link via email. Manage payment methods through Stripe Projects.'
  104. : 'You get a monthly invoice and payment link via email. To change your payment method, please contact us via our support form.'
  105. }
  106. actions={
  107. isStripeManagedOrganization ? (
  108. <Button
  109. asChild
  110. key="stripe-projects-billing-docs"
  111. type="default"
  112. iconRight={<ExternalLink size={14} />}
  113. >
  114. <a
  115. href="https://docs.stripe.com/projects#manage-billing"
  116. target="_blank"
  117. rel="noopener noreferrer"
  118. >
  119. View Stripe Projects docs
  120. </a>
  121. </Button>
  122. ) : (
  123. <Button asChild key="payment-method-support" type="default">
  124. <SupportLink
  125. queryParams={{
  126. category: SupportCategories.BILLING,
  127. subject: 'Request to change payment method',
  128. }}
  129. >
  130. Contact support
  131. </SupportLink>
  132. </Button>
  133. )
  134. }
  135. />
  136. )}
  137. <FormPanel
  138. footer={
  139. !isStripeManagedOrganization ? (
  140. <div className="flex items-center justify-between py-4 px-8">
  141. {!canUpdatePaymentMethods ? (
  142. <p className="text-sm text-foreground-light">
  143. You need additional permissions to manage payment methods
  144. </p>
  145. ) : (
  146. <div />
  147. )}
  148. <Button
  149. type="default"
  150. icon={<Plus />}
  151. disabled={!canUpdatePaymentMethods}
  152. onClick={() => setShowAddPaymentMethodModal(true)}
  153. >
  154. Add new card
  155. </Button>
  156. </div>
  157. ) : undefined
  158. }
  159. >
  160. <FormSection>
  161. <FormSectionContent fullWidth loading={false}>
  162. {(paymentMethods?.data?.length ?? 0) === 0 ? (
  163. <div className="flex items-center gap-2 opacity-50">
  164. <CreditCardIcon size={16} strokeWidth={1.5} />
  165. <p className="text-sm">No payment methods</p>
  166. </div>
  167. ) : (
  168. <div className="space-y-3">
  169. {paymentMethods?.data?.map((paymentMethod) => (
  170. <CreditCard
  171. key={paymentMethod.id}
  172. paymentMethod={paymentMethod}
  173. canUpdatePaymentMethods={canUpdatePaymentMethods}
  174. paymentMethodType={subscription?.payment_method_type}
  175. setSelectedMethodForUse={setSelectedMethodForUse}
  176. setSelectedMethodToDelete={setSelectedMethodToDelete}
  177. paymentMethodCount={paymentMethods?.data.length ?? 0}
  178. subscriptionPlan={subscription?.plan.id}
  179. />
  180. ))}
  181. </div>
  182. )}
  183. </FormSectionContent>
  184. </FormSection>
  185. </FormPanel>
  186. </>
  187. )}
  188. </>
  189. )}
  190. </ScaffoldSectionContent>
  191. </ScaffoldSection>
  192. <AddNewPaymentMethodModal
  193. visible={showAddPaymentMethodModal}
  194. returnUrl={`${getURL()}/org/${slug}/billing`}
  195. onCancel={() => setShowAddPaymentMethodModal(false)}
  196. onConfirm={() => {
  197. setShowAddPaymentMethodModal(false)
  198. toast.success('Successfully added new payment method')
  199. }}
  200. />
  201. <ChangePaymentMethodModal
  202. selectedPaymentMethod={selectedMethodForUse}
  203. onClose={() => setSelectedMethodForUse(undefined)}
  204. />
  205. <DeletePaymentMethodModal
  206. selectedPaymentMethod={selectedMethodToDelete}
  207. onClose={() => setSelectedMethodToDelete(undefined)}
  208. />
  209. </>
  210. )
  211. }
  212. export default PaymentMethods