diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx b/packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx
index 2cefd049e5..5f7b338393 100644
--- a/packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx
+++ b/packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx
@@ -99,6 +99,7 @@ const CheckoutOneClick = () => {
const hasDeliveryShipments = deliveryShipments.length > 0
const isPickupOnly = hasPickupShipments && !hasDeliveryShipments
const [billingSameAsShipping, setBillingSameAsShipping] = useState(true)
+ const [isShipmentCleanupComplete, setIsShipmentCleanupComplete] = useState(false)
// For billing=shipping, align with legacy: use the first delivery shipment's address
const selectedShippingAddress =
deliveryShipments.length > 0 ? deliveryShipments[0]?.shippingAddress : null
@@ -135,8 +136,23 @@ const CheckoutOneClick = () => {
// Remove any empty shipments whenever navigating to the checkout page
// Using basketId ensures that the basket is in a valid state before removing empty shipments
useEffect(() => {
- if (basket?.shipments?.length > 1) {
- removeEmptyShipments(basket)
+ if (!basket?.basketId) {
+ return
+ }
+ if (basket?.shipments?.length <= 1) {
+ setIsShipmentCleanupComplete(true)
+ return
+ }
+
+ let cancelled = false
+ setIsShipmentCleanupComplete(false)
+ removeEmptyShipments(basket).then(() => {
+ if (!cancelled) {
+ setIsShipmentCleanupComplete(true)
+ }
+ })
+ return () => {
+ cancelled = true
}
}, [basket?.basketId])
@@ -587,7 +603,10 @@ const CheckoutOneClick = () => {
/>
{hasPickupShipments && }
{hasDeliveryShipments && (
-
+
)}
{hasDeliveryShipments && }
{
w="full"
onClick={onPlaceOrder}
isLoading={isLoading}
- disabled={isOtpLoading || isPlacingOrder}
+ disabled={
+ isOtpLoading ||
+ isPlacingOrder ||
+ !isShipmentCleanupComplete
+ }
data-testid="place-order-button"
size="lg"
px={8}
diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/index.test.js b/packages/template-retail-react-app/app/pages/checkout-one-click/index.test.js
index 28faa58c7c..c9b45ab1ae 100644
--- a/packages/template-retail-react-app/app/pages/checkout-one-click/index.test.js
+++ b/packages/template-retail-react-app/app/pages/checkout-one-click/index.test.js
@@ -28,6 +28,20 @@ jest.setTimeout(40_000)
mockConfig.app.oneClickCheckout.enabled = true
+const mockRemoveEmptyShipments = jest.fn().mockResolvedValue(undefined)
+jest.mock('@salesforce/retail-react-app/app/hooks/use-multiship', () => {
+ const actual = jest.requireActual('@salesforce/retail-react-app/app/hooks/use-multiship')
+ return {
+ useMultiship: jest.fn((basket) => ({
+ ...actual.useMultiship(basket),
+ removeEmptyShipments: (...args) => {
+ mockRemoveEmptyShipments(...args)
+ return Promise.resolve()
+ }
+ }))
+ }
+})
+
jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => {
return {
getConfig: jest.fn()
@@ -306,6 +320,46 @@ describe('Checkout One Click', () => {
)
getConfig.mockImplementation(() => mockConfig)
+ mockRemoveEmptyShipments.mockClear()
+ })
+
+ test('calls removeEmptyShipments when basket has multiple shipments', async () => {
+ const basketWithMultipleShipments = JSON.parse(JSON.stringify(scapiBasketWithItem))
+ basketWithMultipleShipments.shipments = [
+ basketWithMultipleShipments.shipments[0],
+ {
+ shipmentId: 'shipment-2',
+ shippingAddress: null,
+ shippingMethod: null
+ }
+ ]
+ global.server.use(
+ rest.get('*/baskets', (req, res, ctx) => {
+ return res(
+ ctx.json({
+ baskets: [basketWithMultipleShipments],
+ total: 1
+ })
+ )
+ })
+ )
+ window.history.pushState({}, 'Checkout', createPathWithDefaults('/checkout'))
+ renderWithProviders(, {
+ wrapperProps: {
+ isGuest: true,
+ siteAlias: 'uk',
+ appConfig: mockConfig.app
+ }
+ })
+ await waitFor(
+ () => {
+ expect(mockRemoveEmptyShipments).toHaveBeenCalled()
+ const [basket] = mockRemoveEmptyShipments.mock.calls[0]
+ expect(basket?.basketId).toBe(basketWithMultipleShipments.basketId)
+ expect(basket?.shipments?.length).toBeGreaterThan(1)
+ },
+ {timeout: 5000}
+ )
})
test('renders pickup and shipping sections for mixed baskets', async () => {
diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-address.jsx b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-address.jsx
index 125e99f177..f93e110975 100644
--- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-address.jsx
+++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-address.jsx
@@ -45,7 +45,7 @@ const shippingAddressAriaLabel = defineMessage({
})
export default function ShippingAddress(props) {
- const {enableUserRegistration = false} = props
+ const {enableUserRegistration = false, isShipmentCleanupComplete = true} = props
const {formatMessage} = useIntl()
const [isManualSubmitLoading, setIsManualSubmitLoading] = useState(false)
const [isMultiShipping, setIsMultiShipping] = useState(false)
@@ -210,6 +210,7 @@ export default function ShippingAddress(props) {
getPreferredItem: (addresses) =>
addresses.find((addr) => addr.preferred === true) || addresses[0],
shouldSkip: () => {
+ if (!isShipmentCleanupComplete) return true
if (openedByUser) return true
if (selectedShippingAddress?.address1) {
if (typeof goToNextStep === 'function') {
@@ -223,6 +224,7 @@ export default function ShippingAddress(props) {
applyItem: async (address) => {
await submitAndContinue(address)
},
+ enabled: isShipmentCleanupComplete,
// Navigation is already handled inside submitAndContinue (goToStep/goToNextStep)
onSuccess: () => {},
onError: (error) => {
@@ -230,7 +232,7 @@ export default function ShippingAddress(props) {
}
})
- const isLoading = isAutoSelectLoading || isManualSubmitLoading
+ const isLoading = isAutoSelectLoading || isManualSubmitLoading || !isShipmentCleanupComplete
const handleEdit = () => {
setOpenedByUser(true)
@@ -311,5 +313,6 @@ export default function ShippingAddress(props) {
}
ShippingAddress.propTypes = {
- enableUserRegistration: PropTypes.bool
+ enableUserRegistration: PropTypes.bool,
+ isShipmentCleanupComplete: PropTypes.bool
}
diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-address.test.js b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-address.test.js
index ea35326e74..6dabd6cddc 100644
--- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-address.test.js
+++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-address.test.js
@@ -317,6 +317,26 @@ describe('ShippingAddress Component', () => {
).toBeInTheDocument()
})
+ test('does not run auto-select when isShipmentCleanupComplete is false', async () => {
+ mockUpdateShippingAddress.mutateAsync.mockResolvedValue({})
+ renderWithProviders()
+ // Allow time for useCheckoutAutoSelect effect to run (it should skip due to enabled: false)
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 50))
+ })
+ // Auto-select must not have called updateShippingAddressForShipment
+ expect(mockUpdateShippingAddress.mutateAsync).not.toHaveBeenCalled()
+ })
+
+ test('runs auto-select when isShipmentCleanupComplete is true', async () => {
+ mockUpdateShippingAddress.mutateAsync.mockResolvedValue({})
+ renderWithProviders()
+ await waitFor(() => {
+ expect(mockUpdateShippingAddress.mutateAsync).toHaveBeenCalled()
+ })
+ await waitForNotLoading()
+ })
+
test('handles submission errors gracefully', async () => {
mockUpdateShippingAddress.mutateAsync.mockRejectedValue(new Error('API Error'))