From 45599fc5d3f6b19d0f2bad0c357eb7393bace348 Mon Sep 17 00:00:00 2001 From: "detail-app[bot]" <180357370+detail-app[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:53:58 +0000 Subject: [PATCH] fix(checkout): clear tax ID when country change coalesces with another address edit --- .../checkout-coalesced-country-tax-id.md | 5 ++ .../src/components/CheckoutForm.test.tsx | 55 +++++++++++++++++++ .../checkout/src/components/CheckoutForm.tsx | 15 ++++- 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 clients/.changeset/checkout-coalesced-country-tax-id.md diff --git a/clients/.changeset/checkout-coalesced-country-tax-id.md b/clients/.changeset/checkout-coalesced-country-tax-id.md new file mode 100644 index 00000000000..9c2aae56985 --- /dev/null +++ b/clients/.changeset/checkout-coalesced-country-tax-id.md @@ -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. diff --git a/clients/packages/checkout/src/components/CheckoutForm.test.tsx b/clients/packages/checkout/src/components/CheckoutForm.test.tsx index b4a03e11983..8c9348ab34f 100644 --- a/clients/packages/checkout/src/components/CheckoutForm.test.tsx +++ b/clients/packages/checkout/src/components/CheckoutForm.test.tsx @@ -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', () => { diff --git a/clients/packages/checkout/src/components/CheckoutForm.tsx b/clients/packages/checkout/src/components/CheckoutForm.tsx index 6f1572a27f0..3b59e3a5c42 100644 --- a/clients/packages/checkout/src/components/CheckoutForm.tsx +++ b/clients/packages/checkout/src/components/CheckoutForm.tsx @@ -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') } }