import { Elements } from '@stripe/react-stripe-js' import { loadStripe, StripeAddressElement, StripeElementsOptions } from '@stripe/stripe-js' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { useTheme } from 'next-themes' import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { Button, Card, CardFooter, Form } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { BillingCustomerDataForm } from './BillingCustomerDataForm' import { useBillingCustomerDataForm } from './useBillingCustomerDataForm' import { getAddressElementAppearanceOptions, STRIPE_ELEMENT_FONTS, } from '@/components/interfaces/Billing/Payment/Payment.utils' import { ScaffoldSection, ScaffoldSectionContent, ScaffoldSectionDetail, } from '@/components/layouts/Scaffold' import AlertError from '@/components/ui/AlertError' import NoPermission from '@/components/ui/NoPermission' import PartnerManagedResource from '@/components/ui/PartnerManagedResource' import { organizationKeys } from '@/data/organizations/keys' import { isPartnerBillingOrganization } from '@/data/organizations/managed-by-utils' import { useOrganizationCustomerProfileQuery } from '@/data/organizations/organization-customer-profile-query' import { useOrganizationCustomerProfileUpdateMutation } from '@/data/organizations/organization-customer-profile-update-mutation' import { useOrganizationTaxIdQuery } from '@/data/organizations/organization-tax-id-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { STRIPE_PUBLIC_KEY } from '@/lib/constants' const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) export const BillingCustomerData = () => { const { slug } = useParams() const queryClient = useQueryClient() const { resolvedTheme } = useTheme() const { data: selectedOrganization } = useSelectedOrganizationQuery() const { can: canReadBillingCustomerData, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(PermissionAction.BILLING_READ, 'stripe.customer') const { can: canUpdateBillingCustomerData } = useAsyncCheckPermissions( PermissionAction.BILLING_WRITE, 'stripe.customer' ) const { data: customerProfile, error, isPending: isLoading, isSuccess, } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingCustomerData }) const { data: taxId, error: errorLoadingTaxId, isPending: isLoadingTaxId, isSuccess: loadedTaxId, } = useOrganizationTaxIdQuery({ slug }) const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation({ onError: () => {}, }) const [isSubmitting, setIsSubmitting] = useState(false) const addressElementRef = useRef(null) const { form, handleSubmit, handleReset, isDirty, resetKey, onAddressChange, applyAddressElementValue, markCurrentValuesAsSaved, addressCountry, addressOptions, } = useBillingCustomerDataForm({ customerProfile, taxId, onCustomerDataChange: async (data) => { setIsSubmitting(true) try { await updateCustomerProfile({ slug, address: data.address, billing_name: data.billing_name, tax_id: data.tax_id, }) toast.success('Successfully updated billing data') queryClient.setQueriesData( { queryKey: organizationKeys.list(), exact: true }, (prev) => { if (!prev) return prev return prev.map((org) => org.slug === slug ? { ...org, ...(data.address !== undefined ? { organization_missing_address: false } : {}), ...(data.tax_id !== undefined ? { organization_missing_tax_id: data.tax_id == null } : {}), } : org ) } ) } catch (error) { toast.error( `Failed updating billing data: ${error instanceof Error ? error.message : 'Unknown error'}` ) throw error } finally { setIsSubmitting(false) } }, }) useEffect(() => { addressElementRef.current = null }, [resetKey]) const onFormSubmit = async (e: React.FormEvent) => { e.preventDefault() try { if (addressElementRef.current) { const addressResult = await addressElementRef.current.getValue() applyAddressElementValue(addressResult) } const result = await handleSubmit() if (result.status === 'error') { toast.error(result.message) return } markCurrentValuesAsSaved( result.submittedState.addressValue, result.submittedState.taxIdValues ) } catch { // Save failure toasts are handled inside onCustomerDataChange. } } const isSubmitDisabled = !isDirty || !canUpdateBillingCustomerData || isSubmitting const stripeElementsOptions: StripeElementsOptions = useMemo( () => ({ mode: 'setup', currency: 'usd', appearance: getAddressElementAppearanceOptions(resolvedTheme), fonts: STRIPE_ELEMENT_FONTS, }) as any, [resolvedTheme] ) const isPartnerBilledOrganization = isPartnerBillingOrganization( selectedOrganization?.billing_partner ) return (

Billing Address & Tax ID

Changes will be reflected in every upcoming invoice, past invoices are not affected

A Tax ID is only required for registered businesses.

{selectedOrganization && isPartnerBilledOrganization ? ( ) : isPermissionsLoaded && !canReadBillingCustomerData ? ( ) : ( <> {(isLoading || isLoadingTaxId) && (
)} {(error || errorLoadingTaxId) && ( )} {isSuccess && loadedTaxId && (
{ addressElementRef.current = element }} addressCountry={addressCountry} /> {!canUpdateBillingCustomerData && ( You need additional permissions to manage this organization's billing address )}
)} )}
) }