diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx index cd4037b16a5..d60e21b9480 100644 --- a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx @@ -18,7 +18,6 @@ import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, - Checkbox, cn, Command, CommandEmpty, @@ -34,6 +33,8 @@ import { Popover, PopoverContent, PopoverTrigger, + RadioGroup, + RadioGroupItem, Tooltip, TooltipContent, TooltipTrigger, @@ -144,7 +145,11 @@ export const NewPaymentMethodElement = forwardRef( const taxIdValue = rawTaxIdValue?.trim() ?? '' const selectedTaxId = TAX_IDS.find((option) => option.name === tax_id_name) - const [purchasingAsBusiness, setPurchasingAsBusiness] = useState(currentTaxId != null) + const [purchasingAs, setPurchasingAs] = useState<'individual' | 'business'>( + currentTaxId != null ? 'business' : 'individual' + ) + const purchasingAsBusiness = purchasingAs === 'business' + const [stripeAddress, setStripeAddress] = useState< StripeAddressElementChangeEvent['value'] | undefined >(undefined) @@ -286,6 +291,7 @@ export const NewPaymentMethodElement = forwardRef( apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY!, mode: 'google_maps_api', }, + display: { name: purchasingAsBusiness ? 'organization' : 'full' }, // Use live form state (stripeAddress) so the address survives remounts triggered // by the purchasingAsBusiness toggle (which changes the key prop). Without this, @@ -341,28 +347,41 @@ export const NewPaymentMethodElement = forwardRef( {fullyLoaded && (
- setPurchasingAsBusiness(!purchasingAsBusiness)} - /> - - - - setPurchasingAs(v === 'business' ? 'business' : 'individual')} + className="text-sm" + > +
+ - - - Check this only if you need a tax ID (e.g. US EIN, VAT, GST) on your invoice. You’ll - be asked to enter it, and it’ll appear on a compliant business invoice. If you don’t - have a tax ID, or don’t need one shown, leave this unchecked. You’ll still receive a - receipt. - - + +
+
+ + + + + + + + Purchasing as a business allows you to add a tax ID (e.g. US EIN, VAT, GST) to + your account, which will then show up on your invoice. + + +
+
)} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx index 98c584f1744..762cf2f1aaf 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx @@ -79,6 +79,8 @@ export const BillingCustomerData = () => { markCurrentValuesAsSaved, addressCountry, addressOptions, + purchasingAs, + setPurchasingAs, } = useBillingCustomerDataForm({ customerProfile, taxId, @@ -221,6 +223,8 @@ export const BillingCustomerData = () => { addressElementRef.current = element }} addressCountry={addressCountry} + purchasingAs={purchasingAs} + setPurchasingAs={setPurchasingAs} /> {!canUpdateBillingCustomerData && ( diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx index e992caf60f0..37219ffc9ba 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx @@ -4,7 +4,7 @@ import type { StripeAddressElementChangeEvent, StripeAddressElementOptions, } from '@stripe/stripe-js' -import { Check, ChevronsUpDown, Info, X } from 'lucide-react' +import { Check, ChevronsUpDown, HelpCircle, Info, X } from 'lucide-react' import { useEffect, useId, useMemo, useRef, useState } from 'react' import { UseFormReturn } from 'react-hook-form' import { @@ -23,6 +23,8 @@ import { Popover, PopoverContent, PopoverTrigger, + RadioGroup, + RadioGroupItem, Tooltip, TooltipContent, TooltipTrigger, @@ -31,6 +33,8 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { z } from 'zod' import { TAX_IDS } from './TaxID.constants' +import { PurchasingAs } from './useBillingCustomerDataForm' +import { CustomerTaxId } from '@/data/organizations/types' interface BillingCustomerDataFormProps { form: UseFormReturn @@ -40,7 +44,10 @@ interface BillingCustomerDataFormProps { resetKey: number onAddressChange: (evt: StripeAddressElementChangeEvent) => void onAddressReady?: (element: StripeAddressElement) => void + purchasingAs: PurchasingAs + setPurchasingAs: (as: PurchasingAs) => void addressCountry?: string + taxId?: CustomerTaxId } export const TaxIdSchema = z.object({ @@ -60,6 +67,8 @@ export const BillingCustomerDataForm = ({ onAddressChange, onAddressReady, addressCountry, + purchasingAs, + setPurchasingAs, }: BillingCustomerDataFormProps) => { const [showTaxIDsPopover, setShowTaxIDsPopover] = useState(false) const taxIdListboxId = useId() @@ -106,6 +115,38 @@ export const BillingCustomerDataForm = ({ return (
+ setPurchasingAs(v === 'business' ? 'business' : 'individual')} + className="text-sm" + > +
+ + +
+
+ + + + + + + + Purchasing as a business allows you to add a tax ID (e.g. US EIN, VAT, GST) to your + account, which will then show up on your invoice. + + +
+
+
}
-
- ( - - - - - - If you are an individual, no need to add a Tax ID. If you are a business below - your country's income threshold and don't have a Tax ID, you can leave this - blank. Taxes will be added to your invoice according to your country's tax laws - in the near future. - - - } - > - - - - - - - - - - - No tax ID found. - - {availableTaxIds.map((option) => ( - { - onSelectTaxIdType(option.name) - setShowTaxIDsPopover(false) - }} - > - - {option.country} - {option.name} - - ))} - - - - - - - + {purchasingAs === 'business' && ( +
+ ( + + + + + + If you are an individual, no need to add a Tax ID. If you are a business below + your country's income threshold and don't have a Tax ID, you can leave this + blank. Taxes will be added to your invoice according to your country's tax + laws in the near future. + + + } + > + + + + + + + + + + + No tax ID found. + + {availableTaxIds.map((option) => ( + { + onSelectTaxIdType(option.name) + setShowTaxIDsPopover(false) + }} + > + + {option.country} - {option.name} + + ))} + + + + + + + + )} + /> + + {selectedTaxId && ( +
+ ( + + + + + + )} + /> + +
)} - /> - - {selectedTaxId && ( -
- ( - - - - - - )} - /> - -
- )} -
+
+ )}
) } diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts index d552eff33b3..da148817586 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts @@ -31,6 +31,8 @@ export type BillingAddressPayload = { tax_id: CustomerTaxId | null } +export type PurchasingAs = 'business' | 'individual' + type SubmittedBillingFormState = { addressValue: StripeAddressValue taxIdValues: TaxIdFormValues @@ -66,6 +68,14 @@ export function useBillingCustomerDataForm({ [customerProfile] ) + const [purchasingAs, setPurchasingAs] = useState( + taxId != null ? 'business' : 'individual' + ) + + useEffect(() => { + setPurchasingAs(taxId ? 'business' : 'individual') + }, [taxId]) + const addressOptions: StripeAddressElementOptions = useMemo( () => ({ mode: 'billing', @@ -74,10 +84,10 @@ export function useBillingCustomerDataForm({ apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY!, mode: 'google_maps_api' as const, }, - display: { name: 'full' as const }, + display: { name: purchasingAs === 'business' ? 'organization' : 'full' }, defaultValues: initialStripeAddressValue, }), - [initialStripeAddressValue] + [initialStripeAddressValue, purchasingAs] ) const initialTaxIdValues = useMemo( @@ -119,6 +129,10 @@ export function useBillingCustomerDataForm({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialStripeAddressValue, initialTaxIdValues]) + useEffect(() => { + setResetKey((c) => c + 1) + }, [purchasingAs]) + const onAddressChange = useCallback((evt: StripeAddressElementChangeEvent) => { stripeAddressRef.current = evt.value stripeAddressValidationRef.current = evt.complete ? 'complete' : 'incomplete' @@ -133,7 +147,8 @@ export function useBillingCustomerDataForm({ setIsAddressDirty(!isAddressEqual(result.value, savedStripeAddressRef.current)) }, []) - const isDirty = isAddressDirty || form.formState.isDirty + const savedTaxIdButPurchasingAsIndividual = taxId && purchasingAs === 'individual' + const isDirty = isAddressDirty || form.formState.isDirty || savedTaxIdButPurchasingAsIndividual const syncCurrentState = useCallback( (addressValue: StripeAddressValue, taxIdValues: TaxIdFormValues) => { @@ -179,7 +194,10 @@ export function useBillingCustomerDataForm({ }, billing_name: address.name.trim(), tax_id: - selectedTaxId && taxIdValues.tax_id_type?.length && taxIdValue.length + selectedTaxId && + taxIdValues.tax_id_type?.length && + taxIdValue.length && + purchasingAs === 'business' ? { type: taxIdValues.tax_id_type, value: sanitizeTaxIdValue({ @@ -224,6 +242,8 @@ export function useBillingCustomerDataForm({ markCurrentValuesAsSaved, addressCountry, addressOptions, + purchasingAs, + setPurchasingAs, } }