Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions clients/.changeset/checkout-coalesced-country-tax-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@polar-sh/checkout': patch
---

Fix a debounce coalescing race in `CheckoutForm` that prevented a billing country change from clearing the tax ID when another address field was edited within the 500 ms debounce window. The debounced watcher branches on the last changed field's name, so a country change followed quickly by another address-field edit took the `customer_billing_address.*` branch and sent the new country without `customer_tax_id: null`. The server then re-validated the stale tax ID against the new country and rejected the update with "Invalid tax ID", leaving the form and server out of sync. The tax-ID-clearing side effects now also run in the coalesced branch when the new country differs from the last persisted checkout country.
55 changes: 55 additions & 0 deletions clients/packages/checkout/src/components/CheckoutForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,61 @@ describe('CheckoutForm', () => {
customer_tax_id: null,
})
})

it('clears the tax ID when a country change is coalesced with a line1 edit within the debounce window', async () => {
const update = vi.fn(async () => createCheckout())
const { form } = renderForm(businessCheckout(), update, {
customer_billing_address: { country: 'FR', state: '', line1: '' },
customer_tax_id: 'FR61954506077',
})

await act(async () => {
form.setValue('customer_billing_address.country', 'US')
})
await act(async () => {
form.setValue('customer_billing_address.line1', '1 Market St')
})
await waitForDebounce()

expect(update).toHaveBeenCalledTimes(1)
expect(update).toHaveBeenLastCalledWith(
expect.objectContaining({
customer_billing_address: expect.objectContaining({
country: 'US',
line1: '1 Market St',
}),
customer_tax_id: null,
}),
)
expect(form.getValues('customer_tax_id')).toBe('')
})

it('does not clear the tax ID when only a non-country address field is edited (no country change)', async () => {
const update = vi.fn(async () => createCheckout())
const { form } = renderForm(businessCheckout(), update, {
customer_billing_address: { country: 'FR', state: '', line1: '' },
customer_tax_id: 'FR61954506077',
})

await act(async () => {
form.setValue('customer_billing_address.line1', '1 Market St')
})
await waitForDebounce()

expect(update).toHaveBeenCalledTimes(1)
expect(update).toHaveBeenLastCalledWith(
expect.objectContaining({
customer_billing_address: expect.objectContaining({
country: 'FR',
line1: '1 Market St',
}),
}),
)
expect(update).toHaveBeenLastCalledWith(
expect.not.objectContaining({ customer_tax_id: expect.anything() }),
)
expect(form.getValues('customer_tax_id')).toBe('FR61954506077')
})
})

describe('Stripe checkout form', () => {
Expand Down
15 changes: 14 additions & 1 deletion clients/packages/checkout/src/components/CheckoutForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,26 @@ const BaseCheckoutForm = ({
} else if (name.startsWith('customer_billing_address')) {
const { customer_billing_address: customerBillingAddress } = value
if (customerBillingAddress && customerBillingAddress.country) {
const newCountry = customerBillingAddress.country
payload = {
...payload,
customer_billing_address: {
...customerBillingAddress,
country: customerBillingAddress.country,
country: newCountry,
},
}
// Coalesced edit: a country change followed by another address
// edit within the debounce window lands here with a non-country
// name. Compare against the persisted checkout country (the live
// watched `country` has already advanced to `newCountry`).
if (
newCountry !== checkout.customer_billing_address?.country &&
(checkout.customer_tax_id || value.customer_tax_id)
) {
clearErrors('customer_tax_id')
payload.customer_tax_id = null
resetField('customer_tax_id', { defaultValue: '' })
}
clearErrors('customer_billing_address')
}
}
Expand Down
Loading