Skip to content

Commit ec3ab92

Browse files
Vnext-81889 | | Name fields for in-person Pickup (#1409)
* VNEXT-81889 | Name fields for IN person Pickup * chore: add changeset for pickup customer names * test: select pickup before asserting name fields in checkout The default checkout session starts on shipping, so pickup name inputs only render after choosing local pickup. Co-authored-by: Cursor <cursoragent@cursor.com> * PR comments fixed and Test cases updated * chore: remove redundant pickup sync comments Co-authored-by: Cursor <cursoragent@cursor.com> * fix: avoid Stripe express click timeout and apply Biome format Defer pickup name draft sync until confirm so click resolve stays within Stripe's 1s deadline, and format requireBillingNamesOnly for Biome. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: hide duplicate pickup names when billing address collection is off Exclude pickup from shouldShowBillingNamesOnly on card/ACH so onlyNames does not override hideNames, and cover that configuration in tests. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: keep GoDaddy express wallet open within user gesture Start pickup name sync without awaiting before Apple Pay / Google Pay / Paze, and await it in payment_authorized before confirmation. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent eccc991 commit ec3ab92

21 files changed

Lines changed: 574 additions & 111 deletions
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+
Require customer first and last name for in-person pickup orders. Names are collected in the Pickup section, synced to order billing, and validated for card, offline, free, and express checkout paths.

packages/react/src/components/checkout/__tests__/checkout-address.test.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -263,28 +263,30 @@ describe('Checkout address behavior', () => {
263263
});
264264
});
265265

266-
it('syncs only billing names in onlyNames mode without stale address fields', async () => {
266+
it('syncs only billing names in onlyNames mode without clearing billing address', async () => {
267267
const draftOrder = buildDraftOrder({
268268
totals: {
269-
subTotal: { value: 0, currencyCode: 'USD' },
269+
subTotal: { value: 2500, currencyCode: 'USD' },
270270
discountTotal: { value: 0, currencyCode: 'USD' },
271271
shippingTotal: { value: 0, currencyCode: 'USD' },
272272
taxTotal: { value: 0, currencyCode: 'USD' },
273273
feeTotal: { value: 0, currencyCode: 'USD' },
274-
total: { value: 0, currencyCode: 'USD' },
274+
total: { value: 2500, currencyCode: 'USD' },
275275
},
276276
lineItems: [
277277
{
278278
fulfillmentMode: 'PICKUP',
279-
unitAmount: { value: 0, currencyCode: 'USD' },
279+
unitAmount: { value: 2500, currencyCode: 'USD' },
280280
},
281281
],
282282
billing: {
283283
firstName: '',
284284
lastName: '',
285285
phone: '',
286286
email: 'jane@example.com',
287-
address: buildBillingAddress({ addressLine1: 'Stale Billing St' }),
287+
address: buildBillingAddress({
288+
addressLine1: 'Paid Pickup Billing St',
289+
}),
288290
},
289291
});
290292
const { user } = renderCheckout({
@@ -310,9 +312,7 @@ describe('Checkout address behavior', () => {
310312
lastName: 'Buyer',
311313
},
312314
});
313-
expect(getLastUpdateInput()?.billing ?? {}).not.toMatchObject({
314-
address: expect.objectContaining({ addressLine1: 'Stale Billing St' }),
315-
});
315+
expect(getLastUpdateInput()?.billing).not.toHaveProperty('address');
316316
});
317317

318318
it('does not sync the address until country, state, city, and postal-code are valid', async () => {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,8 +563,9 @@ describe('Checkout draft-order field sync', () => {
563563
await waitForOperation('UpdateCheckoutSessionDraftOrder');
564564

565565
expect(getLastUpdateInput()).toMatchObject({
566-
billing: { firstName: 'Only', lastName: 'Names', address: null },
566+
billing: { firstName: 'Only', lastName: 'Names' },
567567
});
568+
expect(getLastUpdateInput()?.billing).not.toHaveProperty('address');
568569
expect(getLastUpdateInput()?.billing).not.toMatchObject({
569570
addressLine1: expect.anything(),
570571
postalCode: expect.anything(),

packages/react/src/components/checkout/__tests__/checkout-form-validation.test.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { enUs } from '@godaddy/localizations';
22
import { screen, waitFor } from '@testing-library/react';
33
import { useFormContext } from 'react-hook-form';
44
import { describe, expect, it, vi } from 'vitest';
5-
import { PaymentMethodType, PaymentProvider } from '@/types';
5+
import { PaymentProvider } from '@/types';
66
import {
77
buildDraftOrder,
88
buildLineItem,
@@ -42,7 +42,6 @@ function _offlinePaymentMethods() {
4242
mercadopago: null,
4343
ccavenue: null,
4444
offline: {
45-
type: PaymentMethodType.OFFLINE,
4645
processor: PaymentProvider.OFFLINE,
4746
checkoutTypes: ['standard'],
4847
},
@@ -52,7 +51,6 @@ function _offlinePaymentMethods() {
5251
function stripeOnlyPaymentMethods() {
5352
return {
5453
card: {
55-
type: PaymentMethodType.CREDIT_CARD,
5654
processor: PaymentProvider.STRIPE,
5755
checkoutTypes: ['standard'],
5856
},
@@ -163,6 +161,42 @@ describe('Checkout form validation', () => {
163161
expect(getOperations('ConfirmCheckoutSession')).toHaveLength(0);
164162
});
165163

164+
it('requires pickup customer names for paid pickup with offline payment', async () => {
165+
const draftOrder = makePaidPickupOrder({
166+
billing: {
167+
firstName: '',
168+
lastName: '',
169+
address: buildShippingAddress({ addressLine1: '' }),
170+
},
171+
});
172+
const { user } = renderCheckout({
173+
draftOrder,
174+
sessionOverrides: {
175+
draftOrder,
176+
paymentMethods: {
177+
...stripeOnlyPaymentMethods(),
178+
card: null as never,
179+
offline: {
180+
processor: PaymentProvider.OFFLINE,
181+
checkoutTypes: ['standard'],
182+
},
183+
},
184+
enableShipping: false,
185+
enableLocalPickup: true,
186+
enableTaxCollection: false,
187+
},
188+
});
189+
await waitForCheckoutReady();
190+
clearOperations();
191+
192+
await user.click(await clickSubmitButton(/complete your order/i));
193+
194+
await waitFor(() => {
195+
expect(document.body).toHaveTextContent(enUs.validation.enterFirstName);
196+
});
197+
expect(getOperations('ConfirmCheckoutSession')).toHaveLength(0);
198+
});
199+
166200
it('pins current paid pickup card behavior when the billing address line is empty', async () => {
167201
const draftOrder = makePaidPickupOrder();
168202
const { user } = renderCheckout({

packages/react/src/components/checkout/__tests__/checkout-free-payment-form.test.tsx

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
import { screen, waitFor } from '@testing-library/react';
1+
import { screen } from '@testing-library/react';
22
import { describe, expect, it } from 'vitest';
33
import {
44
advanceCheckoutDebounce,
55
buildCheckoutSession,
66
buildDraftOrder,
77
buildShippingRates,
88
clearOperations,
9+
getOperationOrder,
910
getOperations,
1011
renderCheckout,
12+
typeIntoNamedField,
1113
waitForCheckoutReady,
1214
waitForOperation,
1315
} from './checkout-test-env';
14-
import { getLastConfirmInput } from './checkout-test-fixtures';
16+
import {
17+
getLastConfirmInput,
18+
getLastUpdateInput,
19+
} from './checkout-test-fixtures';
1520

1621
function buildFreeDraftOrder(
1722
overrides: Parameters<typeof buildDraftOrder>[0] = {}
@@ -53,7 +58,7 @@ async function submitFreeOrder(
5358
}
5459

5560
describe('Checkout FreePaymentForm integration', () => {
56-
it('renders names-only billing for a free pickup order without a billing address', async () => {
61+
it('renders pickup customer names in the pickup section for a free pickup order', async () => {
5762
const draftOrder = buildFreeDraftOrder({
5863
lineItems: [{ fulfillmentMode: 'PICKUP' }],
5964
billing: {
@@ -83,6 +88,9 @@ describe('Checkout FreePaymentForm integration', () => {
8388
expect(document.querySelector('input[name="billingLastName"]')).toHaveValue(
8489
'Pickup'
8590
);
91+
expect(
92+
document.querySelectorAll('input[name="billingFirstName"]')
93+
).toHaveLength(1);
8694
expect(
8795
document.querySelector('input[name="billingAddressLine1"]')
8896
).not.toBeInTheDocument();
@@ -179,4 +187,49 @@ describe('Checkout FreePaymentForm integration', () => {
179187
document.querySelector('input[name="shippingAddressLine1"]')
180188
).not.toBeInTheDocument();
181189
});
190+
191+
it('persists pickup billing names before free-order confirmation without waiting for debounce', async () => {
192+
const draftOrder = buildFreeDraftOrder({
193+
lineItems: [{ fulfillmentMode: 'PICKUP' }],
194+
billing: {
195+
firstName: '',
196+
lastName: '',
197+
phone: '',
198+
email: 'jane@example.com',
199+
address: null,
200+
},
201+
});
202+
const session = buildCheckoutSession({
203+
draftOrder,
204+
enableShipping: false,
205+
enableLocalPickup: true,
206+
enableTaxCollection: false,
207+
});
208+
209+
const { user } = renderCheckout({ session, draftOrder });
210+
await waitForCheckoutReady();
211+
212+
await typeIntoNamedField(user, 'billingFirstName', 'Immediate');
213+
await typeIntoNamedField(user, 'billingLastName', 'Pickup');
214+
215+
clearOperations();
216+
await user.click(
217+
await screen.findByRole('button', { name: /complete your free order/i })
218+
);
219+
await waitForOperation('ConfirmCheckoutSession');
220+
221+
const [updateIdx, confirmIdx] = getOperationOrder([
222+
'UpdateCheckoutSessionDraftOrder',
223+
'ConfirmCheckoutSession',
224+
]);
225+
expect(updateIdx).toBeGreaterThanOrEqual(0);
226+
expect(confirmIdx).toBeGreaterThan(updateIdx);
227+
expect(getLastUpdateInput()).toMatchObject({
228+
billing: {
229+
firstName: 'Immediate',
230+
lastName: 'Pickup',
231+
},
232+
});
233+
expect(getLastUpdateInput()?.billing).not.toHaveProperty('address');
234+
});
182235
});

packages/react/src/components/checkout/__tests__/checkout-pickup.test.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,69 @@ import {
1010
} from './checkout-test-env';
1111

1212
describe('Checkout pickup behavior', () => {
13+
it('shows customer name fields in the pickup section', async () => {
14+
const { user } = renderCheckout();
15+
await waitForCheckoutReady();
16+
17+
await user.click(screen.getByRole('radio', { name: /local pickup/i }));
18+
await waitForOperation('ApplyCheckoutSessionFulfillmentLocation');
19+
20+
expect(
21+
document.querySelector('input[name="billingFirstName"]')
22+
).toBeInTheDocument();
23+
expect(
24+
document.querySelector('input[name="billingLastName"]')
25+
).toBeInTheDocument();
26+
});
27+
28+
it('keeps a single set of name fields for paid pickup with credit-card billing', async () => {
29+
renderCheckout({
30+
draftOrderOverrides: {
31+
lineItems: [{ fulfillmentMode: 'PICKUP' }],
32+
},
33+
sessionOverrides: {
34+
enableShipping: false,
35+
enableLocalPickup: true,
36+
enableBillingAddressCollection: true,
37+
},
38+
});
39+
await waitForCheckoutReady();
40+
41+
expect(
42+
document.querySelectorAll('input[name="billingFirstName"]')
43+
).toHaveLength(1);
44+
expect(
45+
document.querySelectorAll('input[name="billingLastName"]')
46+
).toHaveLength(1);
47+
expect(
48+
document.querySelector('input[name="billingAddressLine1"]')
49+
).toBeInTheDocument();
50+
});
51+
52+
it('keeps a single set of name fields for paid pickup when billing address collection is disabled', async () => {
53+
renderCheckout({
54+
draftOrderOverrides: {
55+
lineItems: [{ fulfillmentMode: 'PICKUP' }],
56+
},
57+
sessionOverrides: {
58+
enableShipping: false,
59+
enableLocalPickup: true,
60+
enableBillingAddressCollection: false,
61+
},
62+
});
63+
await waitForCheckoutReady();
64+
65+
expect(
66+
document.querySelectorAll('input[name="billingFirstName"]')
67+
).toHaveLength(1);
68+
expect(
69+
document.querySelectorAll('input[name="billingLastName"]')
70+
).toHaveLength(1);
71+
expect(
72+
document.querySelector('input[name="billingAddressLine1"]')
73+
).not.toBeInTheDocument();
74+
});
75+
1376
it('switches from shipping to pickup and calculates taxes with pickup location', async () => {
1477
const { user } = renderCheckout();
1578
await waitForCheckoutReady();

0 commit comments

Comments
 (0)