diff --git a/.changeset/revert-pickup-customer-names.md b/.changeset/revert-pickup-customer-names.md new file mode 100644 index 00000000..63cebe1c --- /dev/null +++ b/.changeset/revert-pickup-customer-names.md @@ -0,0 +1,5 @@ +--- +'@godaddy/react': patch +--- + +Restore pickup customer name collection to the existing billing fields and remove unsupported pickup handling from Stripe and GoDaddy express checkout. diff --git a/packages/react/src/components/checkout/__tests__/checkout-address.test.tsx b/packages/react/src/components/checkout/__tests__/checkout-address.test.tsx index 58df3626..22c3bbd6 100644 --- a/packages/react/src/components/checkout/__tests__/checkout-address.test.tsx +++ b/packages/react/src/components/checkout/__tests__/checkout-address.test.tsx @@ -263,20 +263,20 @@ describe('Checkout address behavior', () => { }); }); - it('syncs only billing names in onlyNames mode without clearing billing address', async () => { + it('syncs only billing names in onlyNames mode without stale address fields', async () => { const draftOrder = buildDraftOrder({ totals: { - subTotal: { value: 2500, currencyCode: 'USD' }, + subTotal: { value: 0, currencyCode: 'USD' }, discountTotal: { value: 0, currencyCode: 'USD' }, shippingTotal: { value: 0, currencyCode: 'USD' }, taxTotal: { value: 0, currencyCode: 'USD' }, feeTotal: { value: 0, currencyCode: 'USD' }, - total: { value: 2500, currencyCode: 'USD' }, + total: { value: 0, currencyCode: 'USD' }, }, lineItems: [ { fulfillmentMode: 'PICKUP', - unitAmount: { value: 2500, currencyCode: 'USD' }, + unitAmount: { value: 0, currencyCode: 'USD' }, }, ], billing: { @@ -284,9 +284,7 @@ describe('Checkout address behavior', () => { lastName: '', phone: '', email: 'jane@example.com', - address: buildBillingAddress({ - addressLine1: 'Paid Pickup Billing St', - }), + address: buildBillingAddress({ addressLine1: 'Stale Billing St' }), }, }); const { user } = renderCheckout({ @@ -312,7 +310,9 @@ describe('Checkout address behavior', () => { lastName: 'Buyer', }, }); - expect(getLastUpdateInput()?.billing).not.toHaveProperty('address'); + expect(getLastUpdateInput()?.billing ?? {}).not.toMatchObject({ + address: expect.objectContaining({ addressLine1: 'Stale Billing St' }), + }); }); it('does not sync the address until country, state, city, and postal-code are valid', async () => { diff --git a/packages/react/src/components/checkout/__tests__/checkout-draft-order-sync.test.tsx b/packages/react/src/components/checkout/__tests__/checkout-draft-order-sync.test.tsx index f2fe9b17..e62bbeb9 100644 --- a/packages/react/src/components/checkout/__tests__/checkout-draft-order-sync.test.tsx +++ b/packages/react/src/components/checkout/__tests__/checkout-draft-order-sync.test.tsx @@ -563,9 +563,8 @@ describe('Checkout draft-order field sync', () => { await waitForOperation('UpdateCheckoutSessionDraftOrder'); expect(getLastUpdateInput()).toMatchObject({ - billing: { firstName: 'Only', lastName: 'Names' }, + billing: { firstName: 'Only', lastName: 'Names', address: null }, }); - expect(getLastUpdateInput()?.billing).not.toHaveProperty('address'); expect(getLastUpdateInput()?.billing).not.toMatchObject({ addressLine1: expect.anything(), postalCode: expect.anything(), diff --git a/packages/react/src/components/checkout/__tests__/checkout-form-validation.test.tsx b/packages/react/src/components/checkout/__tests__/checkout-form-validation.test.tsx index 74b1afb0..fa377efc 100644 --- a/packages/react/src/components/checkout/__tests__/checkout-form-validation.test.tsx +++ b/packages/react/src/components/checkout/__tests__/checkout-form-validation.test.tsx @@ -2,7 +2,7 @@ import { enUs } from '@godaddy/localizations'; import { screen, waitFor } from '@testing-library/react'; import { useFormContext } from 'react-hook-form'; import { describe, expect, it, vi } from 'vitest'; -import { PaymentProvider } from '@/types'; +import { PaymentMethodType, PaymentProvider } from '@/types'; import { buildDraftOrder, buildLineItem, @@ -42,6 +42,7 @@ function _offlinePaymentMethods() { mercadopago: null, ccavenue: null, offline: { + type: PaymentMethodType.OFFLINE, processor: PaymentProvider.OFFLINE, checkoutTypes: ['standard'], }, @@ -51,6 +52,7 @@ function _offlinePaymentMethods() { function stripeOnlyPaymentMethods() { return { card: { + type: PaymentMethodType.CREDIT_CARD, processor: PaymentProvider.STRIPE, checkoutTypes: ['standard'], }, @@ -161,42 +163,6 @@ describe('Checkout form validation', () => { expect(getOperations('ConfirmCheckoutSession')).toHaveLength(0); }); - it('requires pickup customer names for paid pickup with offline payment', async () => { - const draftOrder = makePaidPickupOrder({ - billing: { - firstName: '', - lastName: '', - address: buildShippingAddress({ addressLine1: '' }), - }, - }); - const { user } = renderCheckout({ - draftOrder, - sessionOverrides: { - draftOrder, - paymentMethods: { - ...stripeOnlyPaymentMethods(), - card: null as never, - offline: { - processor: PaymentProvider.OFFLINE, - checkoutTypes: ['standard'], - }, - }, - enableShipping: false, - enableLocalPickup: true, - enableTaxCollection: false, - }, - }); - await waitForCheckoutReady(); - clearOperations(); - - await user.click(await clickSubmitButton(/complete your order/i)); - - await waitFor(() => { - expect(document.body).toHaveTextContent(enUs.validation.enterFirstName); - }); - expect(getOperations('ConfirmCheckoutSession')).toHaveLength(0); - }); - it('pins current paid pickup card behavior when the billing address line is empty', async () => { const draftOrder = makePaidPickupOrder(); const { user } = renderCheckout({ diff --git a/packages/react/src/components/checkout/__tests__/checkout-free-payment-form.test.tsx b/packages/react/src/components/checkout/__tests__/checkout-free-payment-form.test.tsx index 26b806d5..e418a910 100644 --- a/packages/react/src/components/checkout/__tests__/checkout-free-payment-form.test.tsx +++ b/packages/react/src/components/checkout/__tests__/checkout-free-payment-form.test.tsx @@ -1,4 +1,4 @@ -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import { advanceCheckoutDebounce, @@ -6,17 +6,12 @@ import { buildDraftOrder, buildShippingRates, clearOperations, - getOperationOrder, getOperations, renderCheckout, - typeIntoNamedField, waitForCheckoutReady, waitForOperation, } from './checkout-test-env'; -import { - getLastConfirmInput, - getLastUpdateInput, -} from './checkout-test-fixtures'; +import { getLastConfirmInput } from './checkout-test-fixtures'; function buildFreeDraftOrder( overrides: Parameters[0] = {} @@ -58,7 +53,7 @@ async function submitFreeOrder( } describe('Checkout FreePaymentForm integration', () => { - it('renders pickup customer names in the pickup section for a free pickup order', async () => { + it('renders names-only billing for a free pickup order without a billing address', async () => { const draftOrder = buildFreeDraftOrder({ lineItems: [{ fulfillmentMode: 'PICKUP' }], billing: { @@ -88,9 +83,6 @@ describe('Checkout FreePaymentForm integration', () => { expect(document.querySelector('input[name="billingLastName"]')).toHaveValue( 'Pickup' ); - expect( - document.querySelectorAll('input[name="billingFirstName"]') - ).toHaveLength(1); expect( document.querySelector('input[name="billingAddressLine1"]') ).not.toBeInTheDocument(); @@ -187,49 +179,4 @@ describe('Checkout FreePaymentForm integration', () => { document.querySelector('input[name="shippingAddressLine1"]') ).not.toBeInTheDocument(); }); - - it('persists pickup billing names before free-order confirmation without waiting for debounce', async () => { - const draftOrder = buildFreeDraftOrder({ - lineItems: [{ fulfillmentMode: 'PICKUP' }], - billing: { - firstName: '', - lastName: '', - phone: '', - email: 'jane@example.com', - address: null, - }, - }); - const session = buildCheckoutSession({ - draftOrder, - enableShipping: false, - enableLocalPickup: true, - enableTaxCollection: false, - }); - - const { user } = renderCheckout({ session, draftOrder }); - await waitForCheckoutReady(); - - await typeIntoNamedField(user, 'billingFirstName', 'Immediate'); - await typeIntoNamedField(user, 'billingLastName', 'Pickup'); - - clearOperations(); - await user.click( - await screen.findByRole('button', { name: /complete your free order/i }) - ); - await waitForOperation('ConfirmCheckoutSession'); - - const [updateIdx, confirmIdx] = getOperationOrder([ - 'UpdateCheckoutSessionDraftOrder', - 'ConfirmCheckoutSession', - ]); - expect(updateIdx).toBeGreaterThanOrEqual(0); - expect(confirmIdx).toBeGreaterThan(updateIdx); - expect(getLastUpdateInput()).toMatchObject({ - billing: { - firstName: 'Immediate', - lastName: 'Pickup', - }, - }); - expect(getLastUpdateInput()?.billing).not.toHaveProperty('address'); - }); }); diff --git a/packages/react/src/components/checkout/__tests__/checkout-pickup.test.tsx b/packages/react/src/components/checkout/__tests__/checkout-pickup.test.tsx index c5af9fd6..e33d10a0 100644 --- a/packages/react/src/components/checkout/__tests__/checkout-pickup.test.tsx +++ b/packages/react/src/components/checkout/__tests__/checkout-pickup.test.tsx @@ -10,69 +10,6 @@ import { } from './checkout-test-env'; describe('Checkout pickup behavior', () => { - it('shows customer name fields in the pickup section', async () => { - const { user } = renderCheckout(); - await waitForCheckoutReady(); - - await user.click(screen.getByRole('radio', { name: /local pickup/i })); - await waitForOperation('ApplyCheckoutSessionFulfillmentLocation'); - - expect( - document.querySelector('input[name="billingFirstName"]') - ).toBeInTheDocument(); - expect( - document.querySelector('input[name="billingLastName"]') - ).toBeInTheDocument(); - }); - - it('keeps a single set of name fields for paid pickup with credit-card billing', async () => { - renderCheckout({ - draftOrderOverrides: { - lineItems: [{ fulfillmentMode: 'PICKUP' }], - }, - sessionOverrides: { - enableShipping: false, - enableLocalPickup: true, - enableBillingAddressCollection: true, - }, - }); - await waitForCheckoutReady(); - - expect( - document.querySelectorAll('input[name="billingFirstName"]') - ).toHaveLength(1); - expect( - document.querySelectorAll('input[name="billingLastName"]') - ).toHaveLength(1); - expect( - document.querySelector('input[name="billingAddressLine1"]') - ).toBeInTheDocument(); - }); - - it('keeps a single set of name fields for paid pickup when billing address collection is disabled', async () => { - renderCheckout({ - draftOrderOverrides: { - lineItems: [{ fulfillmentMode: 'PICKUP' }], - }, - sessionOverrides: { - enableShipping: false, - enableLocalPickup: true, - enableBillingAddressCollection: false, - }, - }); - await waitForCheckoutReady(); - - expect( - document.querySelectorAll('input[name="billingFirstName"]') - ).toHaveLength(1); - expect( - document.querySelectorAll('input[name="billingLastName"]') - ).toHaveLength(1); - expect( - document.querySelector('input[name="billingAddressLine1"]') - ).not.toBeInTheDocument(); - }); - it('switches from shipping to pickup and calculates taxes with pickup location', async () => { const { user } = renderCheckout(); await waitForCheckoutReady(); diff --git a/packages/react/src/components/checkout/address/address-form.tsx b/packages/react/src/components/checkout/address/address-form.tsx index 41e08a68..73cc8894 100644 --- a/packages/react/src/components/checkout/address/address-form.tsx +++ b/packages/react/src/components/checkout/address/address-form.tsx @@ -54,10 +54,8 @@ import type { Address } from '@/types'; interface AddressFormProps { sectionKey: string; - /** When true, only show first name and last name fields (used for pickup orders) */ + /** When true, only show first name and last name fields (used for free pickup orders) */ onlyNames?: boolean; - /** When true, hide first/last name fields (names collected elsewhere, e.g. pickup section) */ - hideNames?: boolean; } export function mapAutocompleteAddressFields(selectedAddress?: Address) { @@ -75,10 +73,7 @@ export function mapAutocompleteAddressFields(selectedAddress?: Address) { export function AddressForm({ sectionKey, onlyNames = false, - hideNames = false, }: AddressFormProps) { - const showNames = onlyNames || !hideNames; - const showAddressFields = !onlyNames; const form = useFormContext(); const { session } = useCheckoutContext(); const { t } = useGoDaddyContext(); @@ -169,8 +164,6 @@ export function AddressForm({ !!lastName?.trim() && debouncedContact === serializedContact; - // Names-only sync must omit `address`. Sending `address: null` clears any - // billing address already collected for paid pickup (Payment section). useDraftOrderFieldSync({ key: 'name', data: contact, @@ -182,6 +175,7 @@ export function AddressForm({ const fields = { firstName: data.firstName.trim(), lastName: data.lastName.trim(), + address: null, }; return mapAddressFieldsToInput( @@ -395,7 +389,7 @@ export function AddressForm({ return (
- {showAddressFields && ( + {!onlyNames && ( )} - {showNames ? ( -
- ( - - - {t.shipping.firstName} - - - - - - - )} - /> - ( - - {t.shipping.lastName} - - - - - - )} - /> -
- ) : null} +
+ ( + + {t.shipping.firstName} + + + + + + )} + /> + ( + + {t.shipping.lastName} + + + + + + )} + /> +
- {showAddressFields ? ( + {!onlyNames && ( <> - ) : null} + )}
); } diff --git a/packages/react/src/components/checkout/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx index ea5e8369..e3efb6ff 100644 --- a/packages/react/src/components/checkout/checkout.tsx +++ b/packages/react/src/components/checkout/checkout.tsx @@ -282,12 +282,12 @@ export function Checkout(props: CheckoutProps) { } } - // Billing address validation - only required when billing is separate from shipping. - // Offline pickup (pay in store / $0 order) only requires customer names. - const isOfflinePayment = data.paymentMethod === PaymentMethodType.OFFLINE; + // Billing address validation - only required if not using shipping address OR pickup + // BUT skip for free orders (paymentMethod === 'offline') + const isFreeOrder = data.paymentMethod === PaymentMethodType.OFFLINE; const isPickup = data.deliveryMethod === DeliveryMethods.PICKUP; const isShipping = data.deliveryMethod === DeliveryMethods.SHIP; - const isOfflinePickup = isOfflinePayment && isPickup; + const isFreePickup = isFreeOrder && isPickup; // Billing is separate from shipping when there is no shipping address // to copy from. `mapOrderToFormValues` canonicalizes deliveryMethod @@ -297,32 +297,18 @@ export function Checkout(props: CheckoutProps) { const billingIsSeparateFromShipping = !isShipping || !data.paymentUseShippingAddress; - const billingNameFields = [ - { key: 'billingFirstName', message: t.validation.enterFirstName }, - { key: 'billingLastName', message: t.validation.enterLastName }, - ]; - - // Pickup orders always require customer name (collected in Pickup section). - if (isPickup) { - for (const { key, message } of billingNameFields) { - if (!String(data[key as keyof typeof data] ?? '').trim()) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message, - path: [key], - }); - } - } - } - const requireBillingNamesOnly = - !isPickup && - !enableBillingAddressCollection && - billingIsSeparateFromShipping; + (!enableBillingAddressCollection && billingIsSeparateFromShipping) || + isFreePickup; if (requireBillingNamesOnly) { - for (const { key, message } of billingNameFields) { - if (!String(data[key as keyof typeof data] ?? '').trim()) { + const nameFields = [ + { key: 'billingFirstName', message: t.validation.enterFirstName }, + { key: 'billingLastName', message: t.validation.enterLastName }, + ]; + + for (const { key, message } of nameFields) { + if (!data[key as keyof typeof data]) { ctx.addIssue({ code: z.ZodIssueCode.custom, message, @@ -334,7 +320,7 @@ export function Checkout(props: CheckoutProps) { const requireBillingAddress = enableBillingAddressCollection && - !isOfflinePickup && + !isFreePickup && billingIsSeparateFromShipping; if (requireBillingAddress) { diff --git a/packages/react/src/components/checkout/form/custom-form-provider.tsx b/packages/react/src/components/checkout/form/custom-form-provider.tsx index dc2699b6..7e0ead41 100644 --- a/packages/react/src/components/checkout/form/custom-form-provider.tsx +++ b/packages/react/src/components/checkout/form/custom-form-provider.tsx @@ -56,15 +56,15 @@ export function CustomFormProvider< values.paymentUseShippingAddress as unknown as boolean; const isPickup = deliveryMethod === DeliveryMethods.PICKUP; const isShipping = deliveryMethod === DeliveryMethods.SHIP; - const isOfflinePayment = paymentMethod === PaymentMethodType.OFFLINE; - const isOfflinePickup = isOfflinePayment && isPickup; + const isFreeOrder = paymentMethod === PaymentMethodType.OFFLINE; + const isFreePickup = isFreeOrder && isPickup; // Get all field names and filter based on conditions const allFieldNames = Object.keys(values); let fieldNames = [...allFieldNames] as Array>; - /* Offline pickup only validates billing name fields among billing inputs */ - if (isOfflinePickup) { + /* For free pickup orders, only validate billingFirstName and billingLastName */ + if (isFreePickup) { fieldNames = fieldNames.filter( fieldName => !fieldName.startsWith('billing') || @@ -90,7 +90,7 @@ export function CustomFormProvider< // Trigger validation only on the filtered fields if any condition is true, // otherwise trigger on all fields - if (paymentUseShippingAddress || isPickup || isOfflinePayment) { + if (paymentUseShippingAddress || isPickup || isFreeOrder) { result = await methods.trigger(fieldNames, triggerOptions); } else { result = await methods.trigger(undefined, triggerOptions); diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx index cf73b25e..5d2ddd19 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx @@ -1,6 +1,4 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; -import type { CheckoutFormData } from '@/components/checkout/checkout'; import { useCheckoutContext } from '@/components/checkout/checkout'; import { useGetPriceAdjustments } from '@/components/checkout/discount/utils/use-get-price-adjustments'; import { @@ -33,8 +31,6 @@ import { useConvertMajorToMinorUnits, useFormatCurrency, } from '@/components/checkout/utils/format-currency'; -import { useSyncPickupBillingNames } from '@/components/checkout/utils/use-sync-pickup-billing-names'; -import { validatePickupPrerequisites } from '@/components/checkout/utils/use-validate-pickup-prerequisites'; import { Skeleton } from '@/components/ui/skeleton'; import { useGoDaddyContext } from '@/godaddy-provider'; import { GraphQLErrorWithCodes } from '@/lib/graphql-with-errors'; @@ -49,12 +45,10 @@ import type { CalculatedAdjustments, CalculatedTaxes } from '@/types'; export function ExpressCheckoutButton() { const formatCurrency = useFormatCurrency(); const convertMajorToMinorUnits = useConvertMajorToMinorUnits(); - const form = useFormContext(); const { session, setCheckoutErrors, isConfirmingCheckout } = useCheckoutContext(); const isPaymentDisabled = useIsPaymentDisabled(); const { isPoyntLoaded } = useLoadPoyntCollect(); - const syncPickupBillingNames = useSyncPickupBillingNames(); const isDisabled = isConfirmingCheckout || isPaymentDisabled; const { godaddyPaymentsConfig } = useCheckoutContext(); @@ -100,9 +94,6 @@ export function ExpressCheckoutButton() { // Use refs to store current coupon state to avoid stale closures in event handlers const appliedCouponCodeRef = useRef(null); const calculatedAdjustmentsRef = useRef(null); - const pickupBillingNamesSyncRef = useRef | null>(null); - const syncPickupBillingNamesRef = useRef(syncPickupBillingNames); - syncPickupBillingNamesRef.current = syncPickupBillingNames; const calculateGodaddyExpressTaxes = useCallback( async ({ @@ -195,13 +186,6 @@ export function ExpressCheckoutButton() { return; } - const pickupValid = await validatePickupPrerequisites(form, session); - if (!pickupValid) { - return; - } - - pickupBillingNamesSyncRef.current = syncPickupBillingNames(); - // Read from refs to get current values (avoid stale closure) const currentCouponCode = appliedCouponCodeRef.current; const currentAdjustments = calculatedAdjustmentsRef.current; @@ -312,9 +296,6 @@ export function ExpressCheckoutButton() { totals, formatCurrency, isDisabled, - form, - session, - syncPickupBillingNames, ] ); @@ -993,10 +974,6 @@ export function ExpressCheckoutButton() { }; try { - await (pickupBillingNamesSyncRef.current ?? - syncPickupBillingNamesRef.current()); - pickupBillingNamesSyncRef.current = null; - await confirmCheckout.mutateAsync(checkoutBody); event.complete(); diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/express/stripe.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/express/stripe.tsx index ba39c04f..03fb32fc 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/express/stripe.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/express/stripe.tsx @@ -9,8 +9,6 @@ import type { StripeExpressCheckoutElementShippingRateChangeEvent, } from '@stripe/stripe-js'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; -import type { CheckoutFormData } from '@/components/checkout/checkout'; import { useCheckoutContext } from '@/components/checkout/checkout'; import { useGetPriceAdjustments } from '@/components/checkout/discount/utils/use-get-price-adjustments'; import { @@ -23,8 +21,6 @@ import { useStripePaymentIntent } from '@/components/checkout/payment/utils/use- import { filterAndSortShippingMethods } from '@/components/checkout/shipping/utils/filter-shipping-methods'; import { useGetShippingMethodByAddress } from '@/components/checkout/shipping/utils/use-get-shipping-methods'; import { useGetTaxes } from '@/components/checkout/taxes/utils/use-get-taxes'; -import { useSyncPickupBillingNames } from '@/components/checkout/utils/use-sync-pickup-billing-names'; -import { validatePickupPrerequisites } from '@/components/checkout/utils/use-validate-pickup-prerequisites'; import { Skeleton } from '@/components/ui/skeleton'; import { useGoDaddyContext } from '@/godaddy-provider'; @@ -46,12 +42,10 @@ interface StripePartialAddress { export function StripeExpressCheckoutForm() { const { t } = useGoDaddyContext(); - const form = useFormContext(); const { session, setCheckoutErrors, isConfirmingCheckout } = useCheckoutContext(); const elements = useElements(); const isPaymentDisabled = useIsPaymentDisabled(); - const syncPickupBillingNames = useSyncPickupBillingNames(); const { handleSubmit } = useStripeCheckout({ mode: 'express', }); @@ -88,7 +82,6 @@ export function StripeExpressCheckoutForm() { // Use refs for values needed in event handlers to avoid stale closures const appliedCouponCodeRef = useRef(null); const calculatedAdjustmentsRef = useRef(null); - const pickupBillingNamesSyncRef = useRef | null>(null); // Extract discount codes from draft order for comparison (stable string) const draftOrderDiscountCodes = useMemo(() => { @@ -376,21 +369,13 @@ export function StripeExpressCheckoutForm() { // Handle click event - configure initial details const handleClick = useCallback( - async (event: StripeExpressCheckoutElementClickEvent) => { + (event: StripeExpressCheckoutElementClickEvent) => { // Reject if payment is disabled if (isDisabled) { event.reject(); return; } - const pickupValid = await validatePickupPrerequisites(form, session); - if (!pickupValid) { - event.reject(); - return; - } - - pickupBillingNamesSyncRef.current = syncPickupBillingNames(); - // Track click if (event.expressPaymentType === 'apple_pay') { track({ @@ -419,14 +404,7 @@ export function StripeExpressCheckoutForm() { lineItems: buildLineItems({ discountAmount }), }); }, - [ - buildLineItems, - setCheckoutErrors, - isDisabled, - form, - session, - syncPickupBillingNames, - ] + [buildLineItems, setCheckoutErrors, isDisabled] ); // Handle shipping address change @@ -587,9 +565,6 @@ export function StripeExpressCheckoutForm() { const handleConfirm = useCallback( async (event: StripeExpressCheckoutElementConfirmEvent) => { try { - await (pickupBillingNamesSyncRef.current ?? syncPickupBillingNames()); - pickupBillingNamesSyncRef.current = null; - // Find the selected shipping method from our stored shipping methods const selectedShippingMethod = shippingMethods?.find( method => @@ -640,7 +615,6 @@ export function StripeExpressCheckoutForm() { }, [ handleSubmit, - syncPickupBillingNames, t.errors.errorProcessingPayment, shippingMethods, selectedShippingRate, @@ -652,7 +626,6 @@ export function StripeExpressCheckoutForm() { // Handle cancel event const handleCancel = useCallback(() => { // Reset state when payment sheet is dismissed - pickupBillingNamesSyncRef.current = null; setCalculatedTaxes(null); setShippingMethods(null); setSelectedShippingRate(null); diff --git a/packages/react/src/components/checkout/payment/free-payment-form.tsx b/packages/react/src/components/checkout/payment/free-payment-form.tsx index ee2fb3d4..ac26e230 100644 --- a/packages/react/src/components/checkout/payment/free-payment-form.tsx +++ b/packages/react/src/components/checkout/payment/free-payment-form.tsx @@ -1,7 +1,9 @@ import { LoaderCircle } from 'lucide-react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; +import { AddressForm } from '@/components/checkout/address/address-form'; import { useCheckoutContext } from '@/components/checkout/checkout'; +import { DeliveryMethods } from '@/components/checkout/delivery/delivery-methods'; import { PaymentProvider, useConfirmCheckout, @@ -20,6 +22,9 @@ export function FreePaymentForm() { const form = useFormContext(); const confirmCheckout = useConfirmCheckout(); + const deliveryMethod = form.watch('deliveryMethod'); + const isPickup = deliveryMethod === DeliveryMethods.PICKUP; + const handleSubmit = React.useCallback(async () => { const valid = await form.trigger(); if (!valid) { @@ -43,20 +48,16 @@ export function FreePaymentForm() { } }, [form, confirmCheckout.mutateAsync, setCheckoutErrors]); - if (isConfirmingCheckout) { - return ( - - ); - } - - return ( + const submitButton = isConfirmingCheckout ? ( + + ) : ( ); + + // For pickup orders, show name fields + if (isPickup) { + return ( +
+ + {submitButton} +
+ ); + } + + return submitButton; } diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index 5c8d4392..d3f7c026 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -308,7 +308,6 @@ export function PaymentForm( const billingIsSeparateFromShipping = !isShipping || !useShippingAddress; const shouldShowBillingNamesOnly = - !isPickup && !isPaymentMethodWithInlineBilling && session?.enableBillingAddressCollection === false && billingIsSeparateFromShipping; @@ -570,7 +569,6 @@ export function PaymentForm( ) : null} diff --git a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx index 7be07d49..44e352f3 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/ach/godaddy.tsx @@ -34,7 +34,6 @@ export function GoDaddyACHForm() { const useShippingAddress = form.watch('paymentUseShippingAddress'); const deliveryMethod = form.watch('deliveryMethod'); const isShipping = deliveryMethod === DeliveryMethods.SHIP; - const isPickup = deliveryMethod === DeliveryMethods.PICKUP; // Billing is separate from shipping when there is no shipping address to // copy from. `mapOrderToFormValues` canonicalizes deliveryMethod against @@ -45,7 +44,6 @@ export function GoDaddyACHForm() { const billingIsSeparateFromShipping = !isShipping || !useShippingAddress; const shouldShowBillingNamesOnly = - !isPickup && paymentMethod === PaymentMethodType.ACH && session?.enableBillingAddressCollection === false && billingIsSeparateFromShipping; @@ -278,7 +276,6 @@ export function GoDaddyACHForm() { ) : null} diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx index 11d3c75e..25a8f2a0 100644 --- a/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/container.tsx @@ -18,7 +18,6 @@ export function CreditCardContainer({ children }: { children?: ReactNode }) { const useShippingAddress = form.watch('paymentUseShippingAddress'); const deliveryMethod = form.watch('deliveryMethod'); const isShipping = deliveryMethod === DeliveryMethods.SHIP; - const isPickup = deliveryMethod === DeliveryMethods.PICKUP; // Billing is separate from shipping when there is no shipping address to // copy from. `mapOrderToFormValues` canonicalizes deliveryMethod against @@ -29,7 +28,6 @@ export function CreditCardContainer({ children }: { children?: ReactNode }) { const billingIsSeparateFromShipping = !isShipping || !useShippingAddress; const shouldShowBillingNamesOnly = - !isPickup && paymentMethod === PaymentMethodType.CREDIT_CARD && session?.enableBillingAddressCollection === false && billingIsSeparateFromShipping; @@ -74,7 +72,6 @@ export function CreditCardContainer({ children }: { children?: ReactNode }) { ) : null} diff --git a/packages/react/src/components/checkout/payment/utils/use-flush-checkout-sync.ts b/packages/react/src/components/checkout/payment/utils/use-flush-checkout-sync.ts index f10359d4..35235cd9 100644 --- a/packages/react/src/components/checkout/payment/utils/use-flush-checkout-sync.ts +++ b/packages/react/src/components/checkout/payment/utils/use-flush-checkout-sync.ts @@ -1,17 +1,11 @@ import { useQueryClient } from '@tanstack/react-query'; import * as React from 'react'; -import { useFormContext } from 'react-hook-form'; -import { - type CheckoutFormData, - useCheckoutContext, -} from '@/components/checkout/checkout'; +import { useCheckoutContext } from '@/components/checkout/checkout'; import { useDraftOrderSyncQueue } from '@/components/checkout/order/draft-order-sync-provider'; -import { useDraftOrder } from '@/components/checkout/order/use-draft-order'; import { checkoutMutationKeys, checkoutQueryKeys, } from '@/components/checkout/utils/query-keys'; -import { getPickupBillingNamesPatch } from '@/components/checkout/utils/sync-pickup-billing-names'; const DEFAULT_TIMEOUT_MS = 10_000; const POLL_INTERVAL_MS = 50; @@ -28,22 +22,12 @@ function delay(ms: number) { export function useFlushCheckoutSync() { const queryClient = useQueryClient(); - const form = useFormContext(); const { session, setCheckoutErrors } = useCheckoutContext(); - const { data: draftOrder } = useDraftOrder(); - const { enqueueDraftOrderPatch, flushDraftOrderSync } = - useDraftOrderSyncQueue(); + const { flushDraftOrderSync } = useDraftOrderSyncQueue(); return React.useCallback( async (options: FlushCheckoutSyncOptions = {}) => { try { - const pickupNamesPatch = getPickupBillingNamesPatch(form, draftOrder); - if (pickupNamesPatch) { - enqueueDraftOrderPatch(pickupNamesPatch, { - fieldNames: ['billingFirstName', 'billingLastName'], - }); - } - await flushDraftOrderSync(); const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; @@ -104,14 +88,6 @@ export function useFlushCheckoutSync() { throw error; } }, - [ - draftOrder, - enqueueDraftOrderPatch, - flushDraftOrderSync, - form, - queryClient, - session, - setCheckoutErrors, - ] + [flushDraftOrderSync, queryClient, session, setCheckoutErrors] ); } diff --git a/packages/react/src/components/checkout/pickup/local-pickup.tsx b/packages/react/src/components/checkout/pickup/local-pickup.tsx index d6772929..f004771a 100644 --- a/packages/react/src/components/checkout/pickup/local-pickup.tsx +++ b/packages/react/src/components/checkout/pickup/local-pickup.tsx @@ -3,7 +3,6 @@ import { format as formatTz, toZonedTime } from 'date-fns-tz'; import { CalendarIcon, ChevronDown, Clock, MapPin, Store } from 'lucide-react'; import React, { useCallback, useEffect, useState } from 'react'; import { useFormContext } from 'react-hook-form'; -import { AddressForm } from '@/components/checkout/address/address-form'; import { useCheckoutContext } from '@/components/checkout/checkout'; import { DeliveryMethods } from '@/components/checkout/delivery/delivery-methods'; import { useApplyFulfillmentLocation } from '@/components/checkout/delivery/utils/use-apply-fulfillment-location'; @@ -441,8 +440,6 @@ export function LocalPickupForm({ return (
- - -): UseFormReturn { - return { - getValues: vi.fn(() => values), - } as unknown as UseFormReturn; -} - -describe('getPickupBillingNamesPatch', () => { - it('returns null when delivery method is not pickup', () => { - const form = mockForm({ - deliveryMethod: DeliveryMethods.SHIP, - billingFirstName: 'Jane', - billingLastName: 'Doe', - }); - - expect(getPickupBillingNamesPatch(form, null)).toBeNull(); - }); - - it('returns null when pickup names are incomplete', () => { - const form = mockForm({ - deliveryMethod: DeliveryMethods.PICKUP, - billingFirstName: 'Jane', - billingLastName: ' ', - }); - - expect(getPickupBillingNamesPatch(form, null)).toBeNull(); - }); - - it('returns null when form names already match the draft order', () => { - const form = mockForm({ - deliveryMethod: DeliveryMethods.PICKUP, - billingFirstName: 'Jane', - billingLastName: 'Doe', - }); - const draftOrder = { - billing: { firstName: 'Jane', lastName: 'Doe' }, - } as DraftOrder; - - expect(getPickupBillingNamesPatch(form, draftOrder)).toBeNull(); - }); - - it('returns a names-only billing patch without address', () => { - const form = mockForm({ - deliveryMethod: DeliveryMethods.PICKUP, - billingFirstName: ' Jane ', - billingLastName: ' Doe ', - }); - const draftOrder = { - billing: { - firstName: '', - lastName: '', - address: { - addressLine1: 'Paid Pickup Billing St', - }, - }, - } as DraftOrder; - - expect(getPickupBillingNamesPatch(form, draftOrder)).toEqual({ - billing: { - firstName: 'Jane', - lastName: 'Doe', - }, - }); - }); -}); diff --git a/packages/react/src/components/checkout/utils/sync-pickup-billing-names.ts b/packages/react/src/components/checkout/utils/sync-pickup-billing-names.ts deleted file mode 100644 index c103433e..00000000 --- a/packages/react/src/components/checkout/utils/sync-pickup-billing-names.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { UseFormReturn } from 'react-hook-form'; -import type { CheckoutFormData } from '@/components/checkout/checkout'; -import { DeliveryMethods } from '@/components/checkout/delivery/delivery-methods'; -import type { DraftOrderPatch } from '@/components/checkout/order/draft-order-sync-provider'; - -type DraftOrderBillingNames = { - billing?: { - firstName?: string | null; - lastName?: string | null; - } | null; -} | null; - -/** - * Build a names-only billing patch for pickup when the form has names that are - * not yet on the draft order. Omits `address` so paid-pickup billing addresses - * are preserved. - */ -export function getPickupBillingNamesPatch( - form: UseFormReturn, - draftOrder?: DraftOrderBillingNames -): DraftOrderPatch | null { - const values = form.getValues(); - if (values.deliveryMethod !== DeliveryMethods.PICKUP) { - return null; - } - - const firstName = String(values.billingFirstName ?? '').trim(); - const lastName = String(values.billingLastName ?? '').trim(); - if (!firstName || !lastName) { - return null; - } - - if ( - (draftOrder?.billing?.firstName || '') === firstName && - (draftOrder?.billing?.lastName || '') === lastName - ) { - return null; - } - - return { - billing: { - firstName, - lastName, - }, - }; -} diff --git a/packages/react/src/components/checkout/utils/use-sync-pickup-billing-names.ts b/packages/react/src/components/checkout/utils/use-sync-pickup-billing-names.ts deleted file mode 100644 index 20e696ac..00000000 --- a/packages/react/src/components/checkout/utils/use-sync-pickup-billing-names.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useCallback } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { type CheckoutFormData } from '@/components/checkout/checkout'; -import { useDraftOrderSyncQueue } from '@/components/checkout/order/draft-order-sync-provider'; -import { useDraftOrder } from '@/components/checkout/order/use-draft-order'; -import { getPickupBillingNamesPatch } from '@/components/checkout/utils/sync-pickup-billing-names'; - -/** - * Queue current pickup billing names immediately (bypass AddressForm debounce) - * and drain the draft-order sync queue. Call before confirm / express submit. - */ -export function useSyncPickupBillingNames() { - const form = useFormContext(); - const { data: draftOrder } = useDraftOrder(); - const { enqueueDraftOrderPatch, flushDraftOrderSync } = - useDraftOrderSyncQueue(); - - return useCallback(async () => { - const patch = getPickupBillingNamesPatch(form, draftOrder); - if (!patch) { - return; - } - - enqueueDraftOrderPatch(patch, { - fieldNames: ['billingFirstName', 'billingLastName'], - }); - await flushDraftOrderSync(); - }, [enqueueDraftOrderPatch, flushDraftOrderSync, form, draftOrder]); -} diff --git a/packages/react/src/components/checkout/utils/use-validate-pickup-prerequisites.ts b/packages/react/src/components/checkout/utils/use-validate-pickup-prerequisites.ts deleted file mode 100644 index 76a41e75..00000000 --- a/packages/react/src/components/checkout/utils/use-validate-pickup-prerequisites.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useCallback } from 'react'; -import { - type FieldPath, - type UseFormReturn, - useFormContext, -} from 'react-hook-form'; -import type { CheckoutFormData } from '@/components/checkout/checkout'; -import { DeliveryMethods } from '@/components/checkout/delivery/delivery-methods'; -import type { CheckoutSession } from '@/types'; - -export function getPickupPrerequisiteFields( - values: Pick< - CheckoutFormData, - 'deliveryMethod' | 'pickupLocationId' | 'pickupDate' | 'pickupTime' - >, - session?: CheckoutSession | null -): Array> { - if (values.deliveryMethod !== DeliveryMethods.PICKUP) { - return []; - } - - const fields: Array> = [ - 'contactEmail', - 'billingFirstName', - 'billingLastName', - 'pickupLocationId', - ]; - - const location = session?.locations?.find( - loc => loc.id === values.pickupLocationId - ); - const storeHours = location?.operatingHours ?? session?.defaultOperatingHours; - - if (storeHours?.pickupWindowInDays !== 0) { - fields.push('pickupDate', 'pickupTime'); - } - - return fields; -} - -export async function validatePickupPrerequisites( - form: UseFormReturn, - session?: CheckoutSession | null -): Promise { - const values = form.getValues(); - const fields = getPickupPrerequisiteFields(values, session); - - if (fields.length === 0) { - return true; - } - - return form.trigger(fields); -} - -export function useValidatePickupPrerequisites( - session?: CheckoutSession | null -) { - const form = useFormContext(); - - return useCallback(async () => { - return validatePickupPrerequisites(form, session); - }, [form, session]); -}