| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- import { AddressElement } from '@stripe/react-stripe-js'
- import type {
- StripeAddressElement,
- StripeAddressElementChangeEvent,
- StripeAddressElementOptions,
- } from '@stripe/stripe-js'
- import { Check, ChevronsUpDown, Info, X } from 'lucide-react'
- import { useEffect, useId, useMemo, useRef, useState } from 'react'
- import { UseFormReturn } from 'react-hook-form'
- import {
- Button,
- cn,
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- FormControl,
- FormField,
- FormMessage,
- Input,
- Popover,
- PopoverContent,
- PopoverTrigger,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
- } from 'ui'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { z } from 'zod'
- import { TAX_IDS } from './TaxID.constants'
- interface BillingCustomerDataFormProps {
- form: UseFormReturn<TaxIdFormValues>
- disabled?: boolean
- className?: string
- addressOptions: StripeAddressElementOptions
- resetKey: number
- onAddressChange: (evt: StripeAddressElementChangeEvent) => void
- onAddressReady?: (element: StripeAddressElement) => void
- addressCountry?: string
- }
- export const TaxIdSchema = z.object({
- tax_id_type: z.string(),
- tax_id_value: z.string(),
- tax_id_name: z.string(),
- })
- export type TaxIdFormValues = z.infer<typeof TaxIdSchema>
- export const BillingCustomerDataForm = ({
- form,
- disabled = false,
- className,
- addressOptions,
- resetKey,
- onAddressChange,
- onAddressReady,
- addressCountry,
- }: BillingCustomerDataFormProps) => {
- const [showTaxIDsPopover, setShowTaxIDsPopover] = useState(false)
- const taxIdListboxId = useId()
- const onSelectTaxIdType = (name: string) => {
- const selectedTaxIdOption = TAX_IDS.find((option) => option.name === name)
- if (!selectedTaxIdOption) return
- form.setValue('tax_id_type', selectedTaxIdOption.type, { shouldDirty: true })
- form.setValue('tax_id_value', '', { shouldDirty: true })
- form.setValue('tax_id_name', name, { shouldDirty: true })
- }
- const onRemoveTaxId = () => {
- form.setValue('tax_id_name', '', { shouldDirty: true })
- form.setValue('tax_id_type', '', { shouldDirty: true })
- form.setValue('tax_id_value', '', { shouldDirty: true })
- }
- const { tax_id_name } = form.watch()
- const selectedTaxId = TAX_IDS.find((option) => option.name === tax_id_name)
- const availableTaxIds = useMemo(() => {
- return TAX_IDS.filter((taxId) => !addressCountry || taxId.countryIso2 === addressCountry).sort(
- (a, b) => a.country.localeCompare(b.country)
- )
- }, [addressCountry])
- // Clear tax ID fields when the billing country changes so a stale tax ID
- // from the previous country doesn't persist under the new one.
- const prevCountryRef = useRef(addressCountry)
- useEffect(() => {
- if (!addressCountry) return
- const isCountryChange =
- prevCountryRef.current !== undefined && prevCountryRef.current !== addressCountry
- prevCountryRef.current = addressCountry
- if (isCountryChange) {
- form.setValue('tax_id_type', '', { shouldDirty: true })
- form.setValue('tax_id_value', '', { shouldDirty: true })
- form.setValue('tax_id_name', '', { shouldDirty: true })
- }
- }, [addressCountry, form])
- return (
- <div className={cn('flex flex-col space-y-4', className)}>
- <div className={cn('relative', disabled && 'opacity-50')}>
- <AddressElement
- key={`billing-address-${resetKey}`}
- options={addressOptions}
- onChange={onAddressChange}
- onReady={onAddressReady}
- />
- {disabled && <div className="absolute inset-0 z-10 cursor-not-allowed" />}
- </div>
- <div className={cn('grid grid-cols-2 gap-x-6 w-full items-end', disabled && 'opacity-50')}>
- <FormField
- name="tax_id_name"
- control={form.control}
- render={() => (
- <FormItemLayout
- hideMessage
- layout="vertical"
- label="Business Tax ID"
- afterLabel={
- <Tooltip>
- <TooltipTrigger asChild>
- <Info size={14} className="text-foreground-lighter cursor-pointer" />
- </TooltipTrigger>
- <TooltipContent side="right" className="max-w-xs text-left">
- 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.
- </TooltipContent>
- </Tooltip>
- }
- >
- <Popover open={showTaxIDsPopover} onOpenChange={setShowTaxIDsPopover}>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- type="default"
- role="combobox"
- size="medium"
- disabled={disabled}
- aria-expanded={showTaxIDsPopover}
- aria-controls={taxIdListboxId}
- className={cn(
- 'w-full justify-between h-[34px] pr-2',
- !selectedTaxId && 'text-muted'
- )}
- iconRight={
- <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" strokeWidth={1.5} />
- }
- >
- {selectedTaxId
- ? `${selectedTaxId.country} - ${selectedTaxId.name}`
- : 'Select tax ID'}
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent
- id={taxIdListboxId}
- sameWidthAsTrigger
- className="p-0"
- align="start"
- >
- <Command>
- <CommandInput placeholder="Search tax ID..." />
- <CommandList>
- <CommandEmpty>No tax ID found.</CommandEmpty>
- <CommandGroup>
- {availableTaxIds.map((option) => (
- <CommandItem
- key={option.name}
- value={`${option.country} - ${option.name}`}
- onSelect={() => {
- onSelectTaxIdType(option.name)
- setShowTaxIDsPopover(false)
- }}
- >
- <Check
- className={cn(
- 'mr-2 h-4 w-4',
- selectedTaxId?.name === option.name ? 'opacity-100' : 'opacity-0'
- )}
- />
- {option.country} - {option.name}
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItemLayout>
- )}
- />
- {selectedTaxId && (
- <div className="flex items-center space-x-2 [&>div]:w-full">
- <FormField
- name="tax_id_value"
- control={form.control}
- render={({ field }) => (
- <FormItemLayout hideMessage>
- <FormControl>
- <Input
- {...field}
- disabled={disabled}
- placeholder={selectedTaxId?.placeholder}
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- <Button
- type="text"
- className="px-1"
- icon={<X />}
- disabled={disabled}
- onClick={() => onRemoveTaxId()}
- />
- </div>
- )}
- </div>
- </div>
- )
- }
|