PaymentMethods.tsx222 lines · main
| 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 | |
| 10 | import ChangePaymentMethodModal from './ChangePaymentMethodModal' |
| 11 | import CreditCard from './CreditCard' |
| 12 | import DeletePaymentMethodModal from './DeletePaymentMethodModal' |
| 13 | import AddNewPaymentMethodModal from '@/components/interfaces/Billing/Payment/AddNewPaymentMethodModal' |
| 14 | import { SupportLink } from '@/components/interfaces/Support/SupportLink' |
| 15 | import { |
| 16 | ScaffoldSection, |
| 17 | ScaffoldSectionContent, |
| 18 | ScaffoldSectionDetail, |
| 19 | } from '@/components/layouts/Scaffold' |
| 20 | import AlertError from '@/components/ui/AlertError' |
| 21 | import { FormPanel } from '@/components/ui/Forms/FormPanel' |
| 22 | import { FormSection, FormSectionContent } from '@/components/ui/Forms/FormSection' |
| 23 | import NoPermission from '@/components/ui/NoPermission' |
| 24 | import PartnerManagedResource from '@/components/ui/PartnerManagedResource' |
| 25 | import { isPartnerBillingOrganization } from '@/data/organizations/managed-by-utils' |
| 26 | import { useOrganizationPaymentMethodsQuery } from '@/data/organizations/organization-payment-methods-query' |
| 27 | import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' |
| 28 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 29 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 30 | import { MANAGED_BY } from '@/lib/constants/infrastructure' |
| 31 | import { getURL } from '@/lib/helpers' |
| 32 | |
| 33 | const 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 | |
| 222 | export default PaymentMethods |