Files
supabase/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts
T
Kevin Grüneberg 740eeb8504 feat: mandatory address input for paying customers (#37337)
- Mandatory address input when adding a new payment method
- Removed the global HCaptcha store that wasn't used consistently and would sometimes block payment method changes
- Remove the custom billing address data & tax id form on org creation and plan upgrades in favour of Stripe's `AddressElement`
- Unify usage of the Stripe payment elements into a single component
- Customers can mark "Purchasing as a business" and will then be able to put down a tax id
- Adjusted billing address form to have better labels + tax id is filtered down to selected country
- Adjusted Stripe Elements styling to use floating labels (otherwise very hard to use with address element) + additional styling changes
- New flag to filter out payment methods that do not have an address for org upgrades and credit top ups, this will be enforced a few days after rolling this out
- Added Google Maps Places API integration for address auto-completion via Stripe AddressElement
- Upgraded Stripe dependencies
- Slight adjustments to styling of plan upgrade modal
2025-07-23 18:16:10 +08:00

133 lines
4.5 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { BillingCustomerDataFormValues, BillingCustomerDataSchema } from './BillingCustomerDataForm'
import { TAX_IDS } from './TaxID.constants'
import { sanitizeTaxIdValue } from './TaxID.utils'
import { components } from 'api-types'
import type { CustomerAddress } from 'data/organizations/types'
export type FormCustomerData = {
address: CustomerAddress | undefined
billing_name: string
tax_id: components['schemas']['TaxIdResponse']['tax_id'] | null
}
interface UseBillingAddressFormProps {
initialCustomerData?: Partial<BillingCustomerDataFormValues> | null
onCustomerDataChange: (data: FormCustomerData) => void
}
export function useBillingCustomerDataForm({
initialCustomerData,
onCustomerDataChange,
}: UseBillingAddressFormProps) {
const form = useForm<BillingCustomerDataFormValues>({
resolver: zodResolver(BillingCustomerDataSchema),
defaultValues: {
billing_name: initialCustomerData?.billing_name || '',
line1: initialCustomerData?.line1 || '',
line2: initialCustomerData?.line2 || '',
city: initialCustomerData?.city || '',
state: initialCustomerData?.state || '',
postal_code: initialCustomerData?.postal_code || '',
country: initialCustomerData?.country || '',
tax_id_type: initialCustomerData?.tax_id_type || '',
tax_id_value: initialCustomerData?.tax_id_value || '',
tax_id_name: initialCustomerData?.tax_id_name || '',
},
})
// Update form when initialAddress changes
useEffect(() => {
if (initialCustomerData) {
form.reset({
billing_name: initialCustomerData.billing_name || '',
line1: initialCustomerData.line1 || '',
line2: initialCustomerData.line2 || '',
city: initialCustomerData.city || '',
state: initialCustomerData.state || '',
postal_code: initialCustomerData.postal_code || '',
country: initialCustomerData.country || '',
tax_id_type: initialCustomerData?.tax_id_type || '',
tax_id_value: initialCustomerData?.tax_id_value || '',
tax_id_name: initialCustomerData?.tax_id_name || '',
})
}
}, [initialCustomerData])
const handleSubmit = async (values: BillingCustomerDataFormValues) => {
const trimmedValues = Object.entries(values).reduce((acc, [key, value]) => {
acc[key as keyof BillingCustomerDataFormValues] =
typeof value === 'string' ? value.trim() : value
return acc
}, {} as BillingCustomerDataFormValues)
const addressPayload = !trimmedValues.line1 ? null : trimmedValues
const selectedTaxId = TAX_IDS.find((option) => option.name === values.tax_id_name)
onCustomerDataChange({
address: addressPayload
? {
line1: trimmedValues.line1!,
line2: trimmedValues.line2,
city: trimmedValues.city,
state: trimmedValues.state,
postal_code: trimmedValues.postal_code,
country: trimmedValues.country!,
}
: undefined,
billing_name: trimmedValues.billing_name,
tax_id:
selectedTaxId && values.tax_id_type?.length && values.tax_id_value?.length
? {
type: values.tax_id_type,
value: sanitizeTaxIdValue({
value: values.tax_id_value,
name: form.getValues().tax_id_name,
}),
country: selectedTaxId?.countryIso2,
}
: null,
})
}
const handleReset = () => {
if (initialCustomerData) {
form.reset({
billing_name: initialCustomerData.billing_name || '',
line1: initialCustomerData.line1 || '',
line2: initialCustomerData.line2 || '',
city: initialCustomerData.city || '',
state: initialCustomerData.state || '',
postal_code: initialCustomerData.postal_code || '',
country: initialCustomerData.country || '',
tax_id_type: initialCustomerData?.tax_id_type || '',
tax_id_value: initialCustomerData?.tax_id_value || '',
tax_id_name: initialCustomerData?.tax_id_name || '',
})
} else {
form.reset({
line1: '',
line2: '',
city: '',
state: '',
postal_code: '',
country: '',
tax_id_type: '',
tax_id_value: '',
tax_id_name: '',
})
}
}
return {
form,
handleSubmit,
handleReset,
isDirty: form.formState.isDirty,
}
}