diff --git a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx index b453da92d71..42862a4b25f 100644 --- a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx @@ -30,9 +30,6 @@ const BillingSettings = () => { const [paymentMethods, setPaymentMethods] = useState(null) const [isLoadingPaymentMethods, setIsLoadingPaymentMethods] = useState(false) - const [taxIds, setTaxIds] = useState(null) - const [isLoadingTaxIds, setIsLoadingTaxIds] = useState(false) - const defaultPaymentMethod = customer?.invoice_settings?.default_payment_method ?? '' const customerBalance = customer && customer.balance ? customer.balance / 100 : 0 const isCredit = customerBalance < 0 @@ -79,26 +76,9 @@ const BillingSettings = () => { } } - const getTaxIds = async () => { - try { - setIsLoadingTaxIds(true) - const { data: taxIds, error } = await get(`${API_URL}/organizations/${slug}/tax-ids`) - if (error) throw error - setTaxIds(taxIds) - setIsLoadingTaxIds(false) - } catch (error: any) { - ui.setNotification({ - error, - category: 'error', - message: `Failed to get organization tax IDs: ${error.message}`, - }) - } - } - useEffect(() => { getCustomerProfile() getPaymentMethods() - getTaxIds() }, [slug]) return ( @@ -124,11 +104,7 @@ const BillingSettings = () => { address={customer?.address ?? {}} onAddressUpdated={(address: any) => setCustomer({ ...customer, address })} /> - setTaxIds(ids)} - /> + ) } diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx index 3566af3da31..ea707e420a1 100644 --- a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx @@ -1,54 +1,92 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useParams } from 'common' import { isEqual } from 'lodash' import { useEffect, useState } from 'react' import { Button, IconPlus, IconX, Input, Listbox } from 'ui' +import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import Panel from 'components/ui/Panel' -import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' -import { delete_, post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { TaxId, useOrganizationTaxIDsQuery } from 'data/organizations/organization-tax-ids-query' +import { + TaxIdValue, + useOrganizationTaxIDsUpdateMutation, +} from 'data/organizations/organization-tax-ids-update-mutation' +import { useCheckPermissions, useStore } from 'hooks' import { uuidv4 } from 'lib/helpers' import { StripeTaxId, TAX_IDS } from './TaxID.constants' import { sanitizeTaxID } from './TaxID.utils' -export interface TaxIDProps { - loading: boolean - taxIds: any[] - onTaxIdsUpdated: (taxIds: any) => void -} - // Stripe recommends to delete tax ids and create new ones to update // https://stripe.com/docs/billing/customer/tax-ids -const TaxID = ({ loading, taxIds, onTaxIdsUpdated }: TaxIDProps) => { +const TaxID = () => { const { ui } = useStore() - const selectedOrganization = useSelectedOrganization() - const slug = selectedOrganization?.slug ?? '' + const { slug } = useParams() - const [isSaving, setIsSaving] = useState(false) - const [errors, setErrors] = useState([]) - const [taxIdValues, setTaxIdValues] = useState(taxIds) - const formattedTaxIds: StripeTaxId[] = taxIds.map((taxId: StripeTaxId) => { - return { - id: taxId.id, - type: taxId.type, - value: taxId.value, - name: - taxId.type === 'eu_vat' - ? `${taxId.country} VAT` - : TAX_IDS.find((option) => option.code === taxId.type)?.name ?? '', - } + const { + data: taxIds, + error, + isLoading, + isSuccess, + isError, + } = useOrganizationTaxIDsQuery({ slug }) + + const { mutate: updateTaxIDs, isLoading: isUpdating } = useOrganizationTaxIDsUpdateMutation({ + onSuccess: (res) => { + const { created, errors } = res + setErrors(errors?.map((taxId) => taxId.id) ?? []) + const updatedTaxIds = + created?.map((x) => { + return { + id: x.id, + type: x.type, + value: x.value, + name: + x.type === 'eu_vat' + ? `${x.country} VAT` + : TAX_IDS.find((option) => option.code === x.type)?.name ?? '', + } + }) ?? [] + setTaxIdValues(updatedTaxIds) + + if (errors !== undefined && errors.length > 0) { + errors.forEach((taxId: any) => { + ui.setNotification({ category: 'error', message: taxId.result.error.message }) + }) + } else { + ui.setNotification({ category: 'success', message: 'Successfully updated tax IDs' }) + } + }, }) + const [errors, setErrors] = useState([]) + const [taxIdValues, setTaxIdValues] = useState([]) + + const formattedTaxIds = + taxIds?.map((taxId: TaxId) => { + return { + id: taxId.id, + type: taxId.type, + value: taxId.value, + name: + taxId.type === 'eu_vat' + ? `${taxId.country} VAT` + : TAX_IDS.find((option) => option.code === taxId.type)?.name ?? '', + } + }) ?? [] + useEffect(() => { - if (taxIdValues.length === 0) { - setTaxIdValues(formattedTaxIds) - } else { - const erroredTaxIds = taxIdValues.filter((taxId: any) => errors.includes(taxId.id)) - setTaxIdValues(formattedTaxIds.concat(erroredTaxIds)) + if (isSuccess) { + if (taxIdValues.length === 0) { + setTaxIdValues(formattedTaxIds) + } else { + const erroredTaxIds = taxIdValues.filter((taxId: any) => errors.includes(taxId.id)) + setTaxIdValues(formattedTaxIds.concat(erroredTaxIds)) + } } - }, [taxIds]) + }, [isSuccess]) const hasChanges = !isEqual(taxIdValues, formattedTaxIds) const canReadTaxIds = useCheckPermissions(PermissionAction.BILLING_READ, 'stripe.tax_ids') @@ -87,47 +125,11 @@ const TaxID = ({ loading, taxIds, onTaxIdsUpdated }: TaxIDProps) => { } const onSaveTaxIds = async () => { - // To make things simple we delete all existing ones and create new ones from this session - setIsSaving(true) - try { - const deletedIds = await Promise.all( - taxIds.map(async (taxId: StripeTaxId) => { - return await delete_(`${API_URL}/organizations/${slug}/tax-ids`, { id: taxId.id }) - }) - ) + if (!slug) return console.error('Slug is required') + if (taxIds === undefined) return console.error('Tax IDs are required') - const newIds = await Promise.all( - taxIdValues.map(async (taxId: StripeTaxId) => { - const sanitizedID = sanitizeTaxID(taxId) - const result = await post(`${API_URL}/organizations/${slug}/tax-ids`, { - type: sanitizedID.type, - value: sanitizedID.value, - }) - return { id: sanitizedID.id, result } - }) - ) - const taxIdsWithErrors = newIds.filter((taxId: any) => { - if (taxId.result.error) return taxId - }) - setErrors(taxIdsWithErrors.map((taxId: any) => taxId.id)) - - if (taxIdsWithErrors.length > 0) { - taxIdsWithErrors.forEach((taxId: any) => { - ui.setNotification({ category: 'error', message: taxId.result.error.message }) - }) - } else { - ui.setNotification({ category: 'success', message: 'Successfully updated tax IDs' }) - } - const idsCreated = newIds - .filter((taxId: any) => !taxId.result.error) - .map((taxId: any) => taxId.result) - - onTaxIdsUpdated(idsCreated) - } catch (error: any) { - ui.setNotification({ category: 'error', message: 'Failed to save tax IDs' }) - } finally { - setIsSaving(false) - } + const newIds = taxIdValues.map((x) => sanitizeTaxID(x)) + updateTaxIDs({ slug, existingIds: taxIds, newIds }) } return ( @@ -146,9 +148,9 @@ const TaxID = ({ loading, taxIds, onTaxIdsUpdated }: TaxIDProps) => { ) : ( {!canUpdateTaxIds ? (

@@ -161,7 +163,7 @@ const TaxID = ({ loading, taxIds, onTaxIdsUpdated }: TaxIDProps) => {