Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -587,7 +603,10 @@ const CheckoutOneClick = () => {
/>
{hasPickupShipments && <PickupAddress />}
{hasDeliveryShipments && (
<ShippingAddress enableUserRegistration={enableUserRegistration} />
<ShippingAddress
enableUserRegistration={enableUserRegistration}
isShipmentCleanupComplete={isShipmentCleanupComplete}
/>
)}
{hasDeliveryShipments && <ShippingOptions />}
<Payment
Expand Down Expand Up @@ -615,7 +634,11 @@ const CheckoutOneClick = () => {
w="full"
onClick={onPlaceOrder}
isLoading={isLoading}
disabled={isOtpLoading || isPlacingOrder}
disabled={
isOtpLoading ||
isPlacingOrder ||
!isShipmentCleanupComplete
}
data-testid="place-order-button"
size="lg"
px={8}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(<WrappedCheckout history={history} />, {
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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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') {
Expand All @@ -223,14 +224,15 @@ export default function ShippingAddress(props) {
applyItem: async (address) => {
await submitAndContinue(address)
},
enabled: isShipmentCleanupComplete,
// Navigation is already handled inside submitAndContinue (goToStep/goToNextStep)
onSuccess: () => {},
onError: (error) => {
console.error('Failed to auto-select address:', error)
}
})

const isLoading = isAutoSelectLoading || isManualSubmitLoading
const isLoading = isAutoSelectLoading || isManualSubmitLoading || !isShipmentCleanupComplete

const handleEdit = () => {
setOpenedByUser(true)
Expand Down Expand Up @@ -311,5 +313,6 @@ export default function ShippingAddress(props) {
}

ShippingAddress.propTypes = {
enableUserRegistration: PropTypes.bool
enableUserRegistration: PropTypes.bool,
isShipmentCleanupComplete: PropTypes.bool
}
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,26 @@ describe('ShippingAddress Component', () => {
).toBeInTheDocument()
})

test('does not run auto-select when isShipmentCleanupComplete is false', async () => {
mockUpdateShippingAddress.mutateAsync.mockResolvedValue({})
renderWithProviders(<ShippingAddress isShipmentCleanupComplete={false} />)
// 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(<ShippingAddress isShipmentCleanupComplete={true} />)
await waitFor(() => {
expect(mockUpdateShippingAddress.mutateAsync).toHaveBeenCalled()
})
await waitForNotLoading()
})

test('handles submission errors gracefully', async () => {
mockUpdateShippingAddress.mutateAsync.mockRejectedValue(new Error('API Error'))

Expand Down
Loading