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)}
- />
-
- I’m purchasing as a business
-
-
-
- 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.
-
-
+ I'm purchasing as an individual
+
+
+
+ I'm purchasing as a business
+
+
+
+
+
+ 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"
+ >
+
+
+ I'm purchasing as an individual
+
+
+
+ I'm purchasing as a business
+
+
+
+
+
+ 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.
-
-
- }
- >
-
-
-
-
- }
- >
- {selectedTaxId
- ? `${selectedTaxId.country} - ${selectedTaxId.name}`
- : 'Select tax ID'}
-
-
-
-
-
-
-
- No tax ID found.
-
- {availableTaxIds.map((option) => (
- {
- onSelectTaxIdType(option.name)
- setShowTaxIDsPopover(false)
- }}
- >
-
- {option.country} - {option.name}
-
- ))}
-
-
-
-
-
-
-
+ {purchasingAs === 'business' && (
+
+
+ )}
)
}
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,
}
}