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