| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import type {
- StripeAddressElement,
- StripeAddressElementChangeEvent,
- StripeAddressElementOptions,
- } from '@stripe/stripe-js'
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
- import { useForm } from 'react-hook-form'
- import { TaxIdFormValues, TaxIdSchema } from './BillingCustomerDataForm'
- import { TAX_IDS } from './TaxID.constants'
- import { getEffectiveTaxCountry, resolveStoredTaxId, sanitizeTaxIdValue } from './TaxID.utils'
- import type { CustomerAddress, CustomerTaxId } from '@/data/organizations/types'
- type StripeAddressValue = StripeAddressElementChangeEvent['value']
- type StripeAddressValidationState = 'unknown' | 'complete' | 'incomplete'
- type StripeAddressElementValueResult = Awaited<ReturnType<StripeAddressElement['getValue']>>
- interface UseBillingCustomerDataFormProps {
- customerProfile?: {
- address?: CustomerAddress | null
- billing_name?: string
- } | null
- taxId?: CustomerTaxId | null
- onCustomerDataChange: (data: BillingAddressPayload) => Promise<void>
- }
- export type BillingAddressPayload = {
- address: CustomerAddress | undefined
- billing_name: string
- tax_id: CustomerTaxId | null
- }
- type SubmittedBillingFormState = {
- addressValue: StripeAddressValue
- taxIdValues: TaxIdFormValues
- }
- type BillingFormSubmitResult =
- | {
- status: 'error'
- message: string
- }
- | {
- status: 'success'
- submittedState: SubmittedBillingFormState
- }
- export function useBillingCustomerDataForm({
- customerProfile,
- taxId,
- onCustomerDataChange,
- }: UseBillingCustomerDataFormProps) {
- const initialStripeAddressValue: StripeAddressValue = useMemo(
- () => ({
- name: customerProfile?.billing_name ?? '',
- address: {
- line1: customerProfile?.address?.line1 ?? '',
- line2: customerProfile?.address?.line2 ?? '',
- city: customerProfile?.address?.city ?? '',
- state: customerProfile?.address?.state ?? '',
- postal_code: customerProfile?.address?.postal_code ?? '',
- country: customerProfile?.address?.country ?? '',
- },
- }),
- [customerProfile]
- )
- const addressOptions: StripeAddressElementOptions = useMemo(
- () => ({
- mode: 'billing',
- fields: { phone: 'never' },
- autocomplete: {
- apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY!,
- mode: 'google_maps_api' as const,
- },
- display: { name: 'full' as const },
- defaultValues: initialStripeAddressValue,
- }),
- [initialStripeAddressValue]
- )
- const initialTaxIdValues = useMemo(
- () => ({
- tax_id_type: taxId?.type || '',
- tax_id_value: taxId?.value || '',
- tax_id_name: taxId
- ? (resolveStoredTaxId(taxId.type, taxId.country, customerProfile?.address?.country)?.name ??
- '')
- : '',
- }),
- [customerProfile, taxId]
- )
- const form = useForm<TaxIdFormValues>({
- resolver: zodResolver(TaxIdSchema as any),
- defaultValues: initialTaxIdValues,
- })
- const stripeAddressRef = useRef<StripeAddressValue>(initialStripeAddressValue)
- const savedStripeAddressRef = useRef<StripeAddressValue>(initialStripeAddressValue)
- const stripeAddressValidationRef = useRef<StripeAddressValidationState>('unknown')
- const savedTaxIdValuesRef = useRef<TaxIdFormValues>(initialTaxIdValues)
- const [isAddressDirty, setIsAddressDirty] = useState(false)
- const [addressCountry, setAddressCountry] = useState<string | undefined>(
- initialStripeAddressValue.address.country || undefined
- )
- const [resetKey, setResetKey] = useState(0)
- useEffect(() => {
- savedStripeAddressRef.current = initialStripeAddressValue
- savedTaxIdValuesRef.current = initialTaxIdValues
- form.reset(initialTaxIdValues)
- stripeAddressRef.current = initialStripeAddressValue
- stripeAddressValidationRef.current = 'unknown'
- setIsAddressDirty(false)
- setAddressCountry(initialStripeAddressValue.address.country || undefined)
- setResetKey((c) => c + 1)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [initialStripeAddressValue, initialTaxIdValues])
- const onAddressChange = useCallback((evt: StripeAddressElementChangeEvent) => {
- stripeAddressRef.current = evt.value
- stripeAddressValidationRef.current = evt.complete ? 'complete' : 'incomplete'
- setAddressCountry(evt.value.address.country || undefined)
- setIsAddressDirty(!isAddressEqual(evt.value, savedStripeAddressRef.current))
- }, [])
- const applyAddressElementValue = useCallback((result: StripeAddressElementValueResult) => {
- stripeAddressRef.current = result.value
- stripeAddressValidationRef.current = result.complete ? 'complete' : 'incomplete'
- setAddressCountry(result.value.address.country || undefined)
- setIsAddressDirty(!isAddressEqual(result.value, savedStripeAddressRef.current))
- }, [])
- const isDirty = isAddressDirty || form.formState.isDirty
- const syncCurrentState = useCallback(
- (addressValue: StripeAddressValue, taxIdValues: TaxIdFormValues) => {
- form.reset(taxIdValues)
- stripeAddressRef.current = addressValue
- stripeAddressValidationRef.current = 'unknown'
- setIsAddressDirty(false)
- setAddressCountry(addressValue.address.country || undefined)
- },
- [form]
- )
- const handleSubmit = async (): Promise<BillingFormSubmitResult> => {
- const address = stripeAddressRef.current
- const addressWasEdited =
- isAddressDirty || !isAddressEqual(address, savedStripeAddressRef.current)
- if (!address.name?.trim()) {
- return { status: 'error', message: 'Full name is required.' }
- }
- if (!address.address.country?.trim()) {
- return { status: 'error', message: 'Country is required.' }
- }
- if (!address.address.line1?.trim()) {
- return { status: 'error', message: 'Address Line 1 is required.' }
- }
- if (addressWasEdited && stripeAddressValidationRef.current === 'incomplete') {
- return { status: 'error', message: 'Please enter a valid billing address.' }
- }
- const taxIdValues = form.getValues()
- const selectedTaxId = TAX_IDS.find((option) => option.name === taxIdValues.tax_id_name)
- const payload = {
- address: {
- line1: address.address.line1.trim(),
- line2: address.address.line2?.trim() || undefined,
- city: address.address.city.trim(),
- state: address.address.state?.trim() || undefined,
- postal_code: address.address.postal_code.trim(),
- country: address.address.country.trim(),
- },
- billing_name: address.name.trim(),
- tax_id:
- selectedTaxId && taxIdValues.tax_id_type?.length && taxIdValues.tax_id_value?.length
- ? {
- type: taxIdValues.tax_id_type,
- value: sanitizeTaxIdValue({
- value: taxIdValues.tax_id_value,
- name: taxIdValues.tax_id_name,
- }),
- country: getEffectiveTaxCountry(selectedTaxId),
- }
- : null,
- }
- await onCustomerDataChange(payload)
- return {
- status: 'success',
- submittedState: { addressValue: address, taxIdValues },
- }
- }
- const markCurrentValuesAsSaved = (
- addressValue: StripeAddressValue,
- taxIdValues: TaxIdFormValues
- ) => {
- savedStripeAddressRef.current = addressValue
- savedTaxIdValuesRef.current = taxIdValues
- syncCurrentState(addressValue, taxIdValues)
- }
- const handleReset = () => {
- syncCurrentState(savedStripeAddressRef.current, savedTaxIdValuesRef.current)
- setResetKey((c) => c + 1)
- }
- return {
- form,
- handleSubmit,
- handleReset,
- isDirty,
- resetKey,
- onAddressChange,
- applyAddressElementValue,
- markCurrentValuesAsSaved,
- addressCountry,
- addressOptions,
- }
- }
- function isAddressEqual(current: StripeAddressValue, original: StripeAddressValue): boolean {
- return (
- normalize(current.name) === normalize(original.name) &&
- normalize(current.address.line1) === normalize(original.address.line1) &&
- normalize(current.address.line2) === normalize(original.address.line2) &&
- normalize(current.address.city) === normalize(original.address.city) &&
- normalize(current.address.state) === normalize(original.address.state) &&
- normalize(current.address.postal_code) === normalize(original.address.postal_code) &&
- normalize(current.address.country) === normalize(original.address.country)
- )
- }
- function normalize(val: string | null | undefined): string {
- return (val ?? '').trim()
- }
|