Skip to content

Commit 5c6df26

Browse files
fix: prevent delivery method flipping on address change (#1407)
1 parent 5a0ed65 commit 5c6df26

3 files changed

Lines changed: 267 additions & 2 deletions

File tree

.changeset/gentle-moose-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@godaddy/react": patch
3+
---
4+
5+
fix: prevent delivery method flipping on address change

packages/react/src/components/checkout/__tests__/checkout-draft-order-sync.test.tsx

Lines changed: 262 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DeliveryMethods } from '@/components/checkout/delivery/delivery-methods
44
import { checkoutQueryKeys } from '@/components/checkout/utils/query-keys';
55
import {
66
advanceCheckoutDebounce,
7+
buildDraftOrder,
78
buildShippingAddress,
89
clearOperations,
910
fillShippingAddress,
@@ -17,7 +18,26 @@ import {
1718
waitForCheckoutReady,
1819
waitForOperation,
1920
} from './checkout-test-env';
20-
import { getLastUpdateInput } from './checkout-test-fixtures';
21+
import {
22+
getLastConfirmInput,
23+
getLastUpdateInput,
24+
} from './checkout-test-fixtures';
25+
26+
function offlinePaymentMethods() {
27+
return {
28+
card: null as never,
29+
offline: {
30+
processor: 'offline',
31+
checkoutTypes: ['standard'],
32+
},
33+
};
34+
}
35+
36+
async function waitForDeliveryMethodEnabled(name: RegExp) {
37+
await waitFor(() => {
38+
expect(screen.getByRole('radio', { name })).not.toBeDisabled();
39+
});
40+
}
2141

2242
describe('Checkout draft-order field sync', () => {
2343
it('syncs contact email to both shipping and billing', async () => {
@@ -96,6 +116,247 @@ describe('Checkout draft-order field sync', () => {
96116
expect(getOperations('DraftOrderShippingRates')).toHaveLength(0);
97117
});
98118

119+
it('keeps pickup selected when a draft-order refetch still looks like shipping', async () => {
120+
const { user, queryClient, session } = renderCheckout({
121+
draftOrderOverrides: {
122+
lineItems: [{ fulfillmentMode: DeliveryMethods.SHIP }],
123+
},
124+
});
125+
await waitForCheckoutReady();
126+
await waitForDeliveryMethodEnabled(/local pickup/i);
127+
128+
await user.click(screen.getByRole('radio', { name: /local pickup/i }));
129+
await waitFor(() => {
130+
expect(
131+
screen.getByRole('radio', { name: /local pickup/i })
132+
).toBeChecked();
133+
});
134+
135+
queryClient.setQueryData(checkoutQueryKeys.draftOrder(session.id), {
136+
checkoutSession: {
137+
...session,
138+
draftOrder: buildDraftOrder({
139+
lineItems: [{ fulfillmentMode: DeliveryMethods.SHIP }],
140+
}),
141+
},
142+
});
143+
await flushPromises();
144+
145+
await waitFor(() => {
146+
expect(
147+
screen.getByRole('radio', { name: /local pickup/i })
148+
).toBeChecked();
149+
expect(
150+
screen.getByRole('radio', { name: /shipping/i })
151+
).not.toBeChecked();
152+
});
153+
});
154+
155+
it('keeps shipping selected when a stale pickup refetch arrives during delivery-method switching', async () => {
156+
const stalePickupOrder = buildDraftOrder({
157+
lineItems: [{ fulfillmentMode: DeliveryMethods.PICKUP }],
158+
shippingLines: [],
159+
});
160+
const { user, queryClient, session } = renderCheckout({
161+
draftOrder: stalePickupOrder,
162+
});
163+
await waitForCheckoutReady();
164+
await waitForDeliveryMethodEnabled(/shipping/i);
165+
166+
await user.click(screen.getByRole('radio', { name: /shipping/i }));
167+
queryClient.setQueryData(checkoutQueryKeys.draftOrder(session.id), {
168+
checkoutSession: {
169+
...session,
170+
draftOrder: stalePickupOrder,
171+
},
172+
});
173+
await flushPromises();
174+
175+
await waitFor(() => {
176+
expect(screen.getByRole('radio', { name: /shipping/i })).toBeChecked();
177+
expect(
178+
screen.getByRole('radio', { name: /local pickup/i })
179+
).not.toBeChecked();
180+
});
181+
});
182+
183+
it('defaults to pickup when shipping is disabled and the order-derived method is shipping', async () => {
184+
renderCheckout({
185+
draftOrderOverrides: {
186+
lineItems: [{ fulfillmentMode: DeliveryMethods.SHIP }],
187+
},
188+
sessionOverrides: {
189+
enableShipping: false,
190+
enableLocalPickup: true,
191+
},
192+
});
193+
await waitForCheckoutReady();
194+
195+
expect(
196+
screen.queryByRole('radio', { name: /shipping/i })
197+
).not.toBeInTheDocument();
198+
expect(document.body).toHaveTextContent(/local pickup/i);
199+
});
200+
201+
it('defaults to shipping when pickup is disabled and the order-derived method is pickup', async () => {
202+
renderCheckout({
203+
draftOrderOverrides: {
204+
lineItems: [{ fulfillmentMode: DeliveryMethods.PICKUP }],
205+
},
206+
sessionOverrides: {
207+
enableShipping: true,
208+
enableLocalPickup: false,
209+
},
210+
});
211+
await waitForCheckoutReady();
212+
213+
expect(
214+
screen.queryByRole('radio', { name: /local pickup/i })
215+
).not.toBeInTheDocument();
216+
expect(document.body).toHaveTextContent(/shipping/i);
217+
});
218+
219+
it('keeps an explicit delivery selection across mixed-fulfillment refetches', async () => {
220+
const mixedOrder = buildDraftOrder({
221+
lineItems: [
222+
{ id: 'ship-item', fulfillmentMode: DeliveryMethods.SHIP },
223+
{ id: 'pickup-item', fulfillmentMode: DeliveryMethods.PICKUP },
224+
],
225+
});
226+
const { user, queryClient, session } = renderCheckout({
227+
draftOrder: mixedOrder,
228+
});
229+
await waitForCheckoutReady();
230+
await waitForDeliveryMethodEnabled(/local pickup/i);
231+
232+
await user.click(screen.getByRole('radio', { name: /local pickup/i }));
233+
await waitFor(() => {
234+
expect(
235+
screen.getByRole('radio', { name: /local pickup/i })
236+
).toBeChecked();
237+
});
238+
239+
queryClient.setQueryData(checkoutQueryKeys.draftOrder(session.id), {
240+
checkoutSession: {
241+
...session,
242+
draftOrder: mixedOrder,
243+
},
244+
});
245+
await flushPromises();
246+
247+
await waitFor(() => {
248+
expect(
249+
screen.getByRole('radio', { name: /local pickup/i })
250+
).toBeChecked();
251+
expect(
252+
screen.getByRole('radio', { name: /shipping/i })
253+
).not.toBeChecked();
254+
});
255+
});
256+
257+
it('keeps shipping selected when a shipping address sync refetches a prior pickup order', async () => {
258+
const { user } = renderCheckout({
259+
draftOrderOverrides: {
260+
lineItems: [{ fulfillmentMode: DeliveryMethods.PICKUP }],
261+
shippingLines: [],
262+
shipping: {
263+
address: buildShippingAddress({
264+
addressLine1: '',
265+
addressLine2: '',
266+
adminArea1: 'GA',
267+
adminArea2: '',
268+
postalCode: '',
269+
countryCode: 'US',
270+
}),
271+
},
272+
},
273+
});
274+
await waitForCheckoutReady();
275+
await waitForOperation('ApplyCheckoutSessionFulfillmentLocation');
276+
await waitForOperation('CalculateCheckoutSessionTaxes');
277+
await waitForOperation('DraftOrder');
278+
await flushPromises();
279+
clearOperations();
280+
281+
expect(screen.getByRole('radio', { name: /local pickup/i })).toBeChecked();
282+
283+
await waitFor(() => {
284+
expect(
285+
screen.getByRole('radio', { name: /shipping/i })
286+
).not.toBeDisabled();
287+
});
288+
await user.click(screen.getByRole('radio', { name: /shipping/i }));
289+
await waitFor(() => {
290+
expect(screen.getByRole('radio', { name: /shipping/i })).toBeChecked();
291+
});
292+
293+
await typeIntoNamedField(user, 'shippingAddressLine1', '456 Shipping Ln');
294+
await typeIntoNamedField(user, 'shippingAdminArea2', 'Jasper');
295+
await typeIntoNamedField(user, 'shippingPostalCode', '30143');
296+
await advanceCheckoutDebounce();
297+
await waitForOperation('UpdateCheckoutSessionDraftOrder');
298+
await flushPromises();
299+
300+
await waitFor(() => {
301+
expect(screen.getByRole('radio', { name: /shipping/i })).toBeChecked();
302+
expect(
303+
screen.getByRole('radio', { name: /local pickup/i })
304+
).not.toBeChecked();
305+
});
306+
});
307+
308+
it('confirms as shipping after pickup-to-shipping address sync refetches', async () => {
309+
const { user } = renderCheckout({
310+
draftOrderOverrides: {
311+
lineItems: [{ fulfillmentMode: DeliveryMethods.PICKUP }],
312+
shippingLines: [],
313+
shipping: {
314+
firstName: '',
315+
lastName: '',
316+
address: buildShippingAddress({
317+
addressLine1: '',
318+
addressLine2: '',
319+
adminArea1: 'GA',
320+
adminArea2: '',
321+
postalCode: '',
322+
countryCode: 'US',
323+
}),
324+
},
325+
},
326+
sessionOverrides: {
327+
paymentMethods: offlinePaymentMethods(),
328+
},
329+
});
330+
await waitForCheckoutReady();
331+
await waitForOperation('ApplyCheckoutSessionFulfillmentLocation');
332+
await waitForDeliveryMethodEnabled(/shipping/i);
333+
clearOperations();
334+
335+
await user.click(screen.getByRole('radio', { name: /shipping/i }));
336+
await waitFor(() => {
337+
expect(screen.getByRole('radio', { name: /shipping/i })).toBeChecked();
338+
});
339+
340+
await typeIntoNamedField(user, 'shippingFirstName', 'Ship');
341+
await typeIntoNamedField(user, 'shippingLastName', 'Buyer');
342+
await typeIntoNamedField(user, 'shippingAddressLine1', '456 Shipping Ln');
343+
await typeIntoNamedField(user, 'shippingAdminArea2', 'Jasper');
344+
await typeIntoNamedField(user, 'shippingPostalCode', '30143');
345+
await advanceCheckoutDebounce();
346+
await waitForOperation('UpdateCheckoutSessionDraftOrder');
347+
await waitForOperation('ApplyCheckoutSessionShippingMethod');
348+
await flushPromises();
349+
350+
await user.click(
351+
await screen.findByRole('button', { name: /complete your order/i })
352+
);
353+
await waitForOperation('ConfirmCheckoutSession');
354+
355+
expect(getLastConfirmInput()).not.toHaveProperty('fulfillmentLocationId');
356+
expect(getLastConfirmInput()).not.toHaveProperty('fulfillmentStartAt');
357+
expect(getLastConfirmInput()).not.toHaveProperty('fulfillmentEndAt');
358+
});
359+
99360
it('still sends address and recalculates taxes when only an address field changes', async () => {
100361
const { user } = renderCheckout();
101362
await waitForCheckoutReady();

packages/react/src/components/checkout/form/checkout-form.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ interface CheckoutFormProps extends Omit<CheckoutProps, 'session'> {
6767

6868
const ORDER_BACKED_FORM_FIELDS = [
6969
'contactEmail',
70-
'deliveryMethod',
7170
'paymentUseShippingAddress',
7271
'shippingFirstName',
7372
'shippingLastName',

0 commit comments

Comments
 (0)