From 0c66a654c0cb47a04cf083f8a944ed0236573008 Mon Sep 17 00:00:00 2001 From: leamotta Date: Fri, 31 Jul 2026 09:38:58 +0200 Subject: [PATCH] surface error for invalid single address --- .../Order2SavedAddressOptions.tsx | 22 +++++- .../Order2SavedAddressOptions.jest.tsx | 78 +++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/Apps/Order2/Routes/Checkout/Components/FulfillmentDetailsStep/SavedAddressOptions/Order2SavedAddressOptions.tsx b/src/Apps/Order2/Routes/Checkout/Components/FulfillmentDetailsStep/SavedAddressOptions/Order2SavedAddressOptions.tsx index c211a70a767..e22b2d8dacd 100644 --- a/src/Apps/Order2/Routes/Checkout/Components/FulfillmentDetailsStep/SavedAddressOptions/Order2SavedAddressOptions.tsx +++ b/src/Apps/Order2/Routes/Checkout/Components/FulfillmentDetailsStep/SavedAddressOptions/Order2SavedAddressOptions.tsx @@ -153,18 +153,31 @@ export const SavedAddressOptions = ({ } }, [isStepActive]) + const loneInvalidAddress = + savedAddresses.length === 1 && initialSelectedAddress?.isValid === false + ? initialSelectedAddress + : null + // Auto-open edit form for the single saved address if it has missing fields // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally runs once on mount useEffect(() => { - if (savedAddresses.length !== 1 || !initialSelectedAddress) return - if (!initialSelectedAddress.isValid) { - setUserAddressMode({ mode: "edit", address: initialSelectedAddress }) + if (loneInvalidAddress) { + setUserAddressMode({ mode: "edit", address: loneInvalidAddress }) } }, []) // Reactively set/clear error banner based on selected address validity and shippability useEffect(() => { - if (!selectedAddress || userAddressMode || !isStepActive) return + if (!selectedAddress || !isStepActive) return + + // Skip while the user is adding or editing an address — except for the lone + // invalid address above, whose form is open precisely because of this error. + // In the offer flow the step isn't active on mount, so this effect first + // runs once the form is already open; skipping would drop the error (and + // its `errorMessageViewed` event) entirely. + const isEditingLoneInvalidAddress = + userAddressMode?.mode === "edit" && !!loneInvalidAddress + if (userAddressMode && !isEditingLoneInvalidAddress) return if (!selectedAddress.isShippable && !isOffer) { setSectionErrorMessage({ @@ -189,6 +202,7 @@ export const SavedAddressOptions = ({ }, [ selectedAddress, userAddressMode, + loneInvalidAddress, isOffer, setSectionErrorMessage, shippingOriginRegion, diff --git a/src/Apps/Order2/Routes/Checkout/Components/FulfillmentDetailsStep/SavedAddressOptions/__tests__/Order2SavedAddressOptions.jest.tsx b/src/Apps/Order2/Routes/Checkout/Components/FulfillmentDetailsStep/SavedAddressOptions/__tests__/Order2SavedAddressOptions.jest.tsx index 3435860280a..5ed7a48691a 100644 --- a/src/Apps/Order2/Routes/Checkout/Components/FulfillmentDetailsStep/SavedAddressOptions/__tests__/Order2SavedAddressOptions.jest.tsx +++ b/src/Apps/Order2/Routes/Checkout/Components/FulfillmentDetailsStep/SavedAddressOptions/__tests__/Order2SavedAddressOptions.jest.tsx @@ -709,6 +709,84 @@ describe("SavedAddressOptions", () => { }) }) + it("surfaces the error banner for the auto-opened edit form once the step becomes active", async () => { + const mockSingleInvalidAddress: ProcessedUserAddress = { + ...mockUSAddress1, + isValid: false, + address: { ...mockUSAddress1.address, city: "" }, + } + + const contextWith = ({ + state, + userAddressMode, + }: { + state: CheckoutStepState + userAddressMode: { mode: "edit"; address: ProcessedUserAddress } | null + }) => { + return { + ...mockCheckoutContext, + isOffer: true, + userAddressMode, + steps: [{ name: CheckoutStepName.FULFILLMENT_DETAILS, state }], + } as any + } + + const props = { + savedAddresses: [mockSingleInvalidAddress], + initialSelectedAddress: mockSingleInvalidAddress, + } + + // Offer flow: fulfillment details is not active yet, and the single + // invalid address auto-opens its edit form on mount. + mockUseCheckoutContext.mockReturnValue( + contextWith({ + state: CheckoutStepState.UPCOMING, + userAddressMode: null, + }), + ) + + const { rerender } = renderSavedAddressOptions(props) + + await waitFor(() => { + expect(mockCheckoutContext.setUserAddressMode).toHaveBeenCalledWith({ + mode: "edit", + address: mockSingleInvalidAddress, + }) + }) + expect(mockCheckoutContext.setSectionErrorMessage).not.toHaveBeenCalled() + + const userAddressMode = { + mode: "edit", + address: mockSingleInvalidAddress, + } as const + + mockUseCheckoutContext.mockReturnValue( + contextWith({ state: CheckoutStepState.UPCOMING, userAddressMode }), + ) + rerender(buildSavedAddressOptions(props)) + + expect(mockCheckoutContext.setSectionErrorMessage).not.toHaveBeenCalled() + + // Completing the offer step activates fulfillment details, and the error + // explaining why the form is open is surfaced (and tracked as viewed). + mockUseCheckoutContext.mockReturnValue( + contextWith({ state: CheckoutStepState.ACTIVE, userAddressMode }), + ) + rerender(buildSavedAddressOptions(props)) + + await waitFor(() => { + expect(mockCheckoutContext.setSectionErrorMessage).toHaveBeenCalledWith( + { + section: CheckoutStepName.FULFILLMENT_DETAILS, + error: { + title: "Missing required information", + message: "Edit your address and/or phone number to continue.", + }, + }, + ) + }) + }) + it("does not open edit form on mount for single valid address", async () => { renderSavedAddressOptions({ savedAddresses: [mockUSAddress1] })