Skip to content

Commit c1059b8

Browse files
committed
fix for multi-shipment
Signed-off-by: d.phan <d.phan@salesforce.com>
1 parent 6d96809 commit c1059b8

File tree

2 files changed

+273
-114
lines changed

2 files changed

+273
-114
lines changed

packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options.jsx

Lines changed: 111 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import React, {useEffect, useState, useMemo} from 'react'
7+
import React, {useCallback, useEffect, useState, useMemo} from 'react'
88
import {FormattedMessage, FormattedNumber, useIntl} from 'react-intl'
99
import {
1010
Box,
@@ -51,6 +51,7 @@ export default function ShippingOptions() {
5151
const updateShippingMethod = useShopperBasketsMutation('updateShippingMethodForShipment')
5252
const showToast = useToast()
5353
const [noMethodsToastShown, setNoMethodsToastShown] = useState(false)
54+
const [shipmentIdsWithNoMethods, setShipmentIdsWithNoMethods] = useState(() => new Set())
5455
// Identify delivery shipments (exclude pickup and those without shipping addresses)
5556
const deliveryShipments =
5657
basket?.shipments?.filter((s) => s.shippingAddress && !isPickupShipment(s)) || []
@@ -68,42 +69,79 @@ export default function ShippingOptions() {
6869
enabled:
6970
Boolean(basket?.basketId) &&
7071
step === STEPS.SHIPPING_OPTIONS &&
71-
!hasMultipleDeliveryShipments,
72-
onSuccess: (data) => {
73-
const deliveryOnly = getDeliveryShippingMethods(data?.applicableShippingMethods)
74-
const noDeliveryMethods = !deliveryOnly || deliveryOnly.length === 0
75-
if (
76-
step === STEPS.SHIPPING_OPTIONS &&
77-
!hasMultipleDeliveryShipments &&
78-
noDeliveryMethods &&
79-
!noMethodsToastShown
80-
) {
81-
showToast({
82-
title: formatMessage({
83-
defaultMessage:
84-
'Unfortunately, we are unable to ship to this address at this time. Please reach out to customer support for further assistance.',
85-
id: 'shipping_options.error.no_shipping_methods'
86-
}),
87-
status: 'error'
88-
})
89-
setNoMethodsToastShown(true)
90-
}
91-
}
72+
!hasMultipleDeliveryShipments
73+
// Single-shipment "no methods" toast is handled in useEffect when data is available
9274
}
9375
)
9476

9577
const selectedShippingMethod = targetDeliveryShipment?.shippingMethod
9678
const selectedShippingAddress = targetDeliveryShipment?.shippingAddress
9779

98-
// Reset error toast when state changes
80+
// Reset error toast when address state changes (affecting applicable shipping methods)
81+
const deliveryAddressStateKey = hasMultipleDeliveryShipments
82+
? deliveryShipments.map((s) => s.shippingAddress?.stateCode ?? '').join(',')
83+
: selectedShippingAddress?.stateCode
9984
useEffect(() => {
10085
setNoMethodsToastShown(false)
101-
}, [selectedShippingAddress?.stateCode])
86+
}, [deliveryAddressStateKey])
10287

10388
// Filter out pickup methods for delivery shipment
10489
const deliveryMethods = getDeliveryShippingMethods(
10590
shippingMethods?.applicableShippingMethods || []
10691
)
92+
const noShippingMethodsToast = useMemo(
93+
() => ({
94+
title: formatMessage({
95+
defaultMessage:
96+
'Unfortunately, we are unable to ship to this address at this time. Please reach out to customer support for further assistance.',
97+
id: 'shipping_options.error.no_shipping_methods'
98+
}),
99+
status: 'error'
100+
}),
101+
[formatMessage]
102+
)
103+
104+
// For multi-shipment, report whether this shipment has no applicable delivery methods
105+
const handleShipmentMethodsResolved = useCallback(
106+
(shipmentId, hasNoApplicableMethods) => {
107+
setShipmentIdsWithNoMethods((prev) => {
108+
const next = new Set(prev)
109+
if (hasNoApplicableMethods) next.add(shipmentId)
110+
else next.delete(shipmentId)
111+
return next
112+
})
113+
if (
114+
hasNoApplicableMethods &&
115+
step === STEPS.SHIPPING_OPTIONS &&
116+
hasMultipleDeliveryShipments
117+
) {
118+
setNoMethodsToastShown((prev) => {
119+
if (!prev) showToast(noShippingMethodsToast)
120+
return true
121+
})
122+
}
123+
},
124+
[
125+
step,
126+
STEPS.SHIPPING_OPTIONS,
127+
hasMultipleDeliveryShipments,
128+
showToast,
129+
noShippingMethodsToast
130+
]
131+
)
132+
const hasAnyShipmentWithNoMethods = shipmentIdsWithNoMethods.size > 0
133+
134+
// Single shipment: show toast when methods data is available and has no delivery methods
135+
const singleShipmentNoMethods =
136+
!hasMultipleDeliveryShipments &&
137+
step === STEPS.SHIPPING_OPTIONS &&
138+
shippingMethods != null &&
139+
deliveryMethods.length === 0
140+
useEffect(() => {
141+
if (!singleShipmentNoMethods || noMethodsToastShown) return
142+
showToast(noShippingMethodsToast)
143+
setNoMethodsToastShown(true)
144+
}, [singleShipmentNoMethods, noMethodsToastShown, showToast, noShippingMethodsToast])
107145

108146
const {isLoading: isAutoSelectLoading} = useCheckoutAutoSelect({
109147
currentStep: step,
@@ -229,10 +267,6 @@ export default function ShippingOptions() {
229267
shippingItem?.priceAfterItemDiscount || 0
230268
)
231269

232-
const filteredShippingMethods = getDeliveryShippingMethods(
233-
shippingMethods?.applicableShippingMethods
234-
)
235-
236270
const freeLabel = formatMessage({
237271
defaultMessage: 'Free',
238272
id: 'checkout_confirmation.label.free'
@@ -280,26 +314,29 @@ export default function ShippingOptions() {
280314
index={idx + 1}
281315
shipment={shipment}
282316
currency={currency}
317+
onShipmentMethodsResolved={handleShipmentMethodsResolved}
283318
/>
284319
))}
285-
<Box>
286-
<Container variant="form">
287-
<Button w="full" onClick={() => goToNextStep()}>
288-
<FormattedMessage
289-
defaultMessage="Continue to Payment"
290-
id="shipping_options.button.continue_to_payment"
291-
/>
292-
</Button>
293-
</Container>
294-
</Box>
320+
{!hasAnyShipmentWithNoMethods && (
321+
<Box>
322+
<Container variant="form">
323+
<Button w="full" onClick={() => goToNextStep()}>
324+
<FormattedMessage
325+
defaultMessage="Continue to Payment"
326+
id="shipping_options.button.continue_to_payment"
327+
/>
328+
</Button>
329+
</Container>
330+
</Box>
331+
)}
295332
</Stack>
296333
) : (
297334
<form
298335
onSubmit={form.handleSubmit(submitForm)}
299336
data-testid="sf-checkout-shipping-options-form"
300337
>
301338
<Stack spacing={6}>
302-
{filteredShippingMethods.length > 0 && (
339+
{deliveryMethods.length > 0 && (
303340
<Controller
304341
name="shippingMethodId"
305342
control={form.control}
@@ -311,7 +348,7 @@ export default function ShippingOptions() {
311348
onChange={onChange}
312349
>
313350
<Stack spacing={5}>
314-
{filteredShippingMethods.map((opt) => (
351+
{deliveryMethods.map((opt) => (
315352
<Radio value={opt.id} key={opt.id}>
316353
<Flex justify="space-between" w="full">
317354
<Box>
@@ -347,7 +384,7 @@ export default function ShippingOptions() {
347384
)}
348385
/>
349386
)}
350-
{filteredShippingMethods.length > 0 && (
387+
{deliveryMethods.length > 0 && (
351388
<Box>
352389
<Container variant="form">
353390
<Button w="full" type="submit">
@@ -370,13 +407,15 @@ export default function ShippingOptions() {
370407
totalShippingCost={totalShippingCost}
371408
currency={currency}
372409
freeLabel={freeLabel}
410+
shipmentIdsWithNoMethods={shipmentIdsWithNoMethods}
373411
/>
374412
)}
375413

376414
{!hasMultipleDeliveryShipments &&
377415
!effectiveIsLoading &&
378416
selectedShippingMethod &&
379-
selectedShippingAddress && (
417+
selectedShippingAddress &&
418+
deliveryMethods.length > 0 && (
380419
<SingleShipmentSummary
381420
selectedShippingMethod={selectedShippingMethod}
382421
selectedMethodDisplayPrice={selectedMethodDisplayPrice}
@@ -390,7 +429,13 @@ export default function ShippingOptions() {
390429
}
391430

392431
// Child component for multi-shipment summary
393-
const MultiShipmentSummary = ({deliveryShipments, totalShippingCost, currency, freeLabel}) => {
432+
const MultiShipmentSummary = ({
433+
deliveryShipments,
434+
totalShippingCost,
435+
currency,
436+
freeLabel,
437+
shipmentIdsWithNoMethods = new Set()
438+
}) => {
394439
const {formatMessage} = useIntl()
395440

396441
return (
@@ -399,11 +444,12 @@ const MultiShipmentSummary = ({deliveryShipments, totalShippingCost, currency, f
399444
{deliveryShipments.map((shipment) => {
400445
// Use shipment.shippingTotal to include all costs (base + promotions + surcharges + other fees)
401446
const itemCost = shipment.shippingTotal || 0
447+
const hasNoApplicableMethods = shipmentIdsWithNoMethods.has(shipment.shipmentId)
402448
return (
403449
<Box key={shipment.shipmentId}>
404450
<Flex justify="space-between" w="full">
405451
<Box flex="1">
406-
{shipment.shippingMethod ? (
452+
{shipment.shippingMethod && !hasNoApplicableMethods ? (
407453
<>
408454
<Text mt={2}>{shipment.shippingMethod.name}</Text>
409455
<Text fontSize="sm" color="gray.700">
@@ -471,7 +517,8 @@ MultiShipmentSummary.propTypes = {
471517
).isRequired,
472518
totalShippingCost: PropTypes.number.isRequired,
473519
currency: PropTypes.string.isRequired,
474-
freeLabel: PropTypes.string.isRequired
520+
freeLabel: PropTypes.string.isRequired,
521+
shipmentIdsWithNoMethods: PropTypes.instanceOf(Set)
475522
}
476523

477524
// Child component for single-shipment summary
@@ -516,7 +563,7 @@ SingleShipmentSummary.propTypes = {
516563
freeLabel: PropTypes.string.isRequired
517564
}
518565

519-
const ShipmentMethods = ({shipment, index, currency}) => {
566+
const ShipmentMethods = ({shipment, index, currency, onShipmentMethodsResolved}) => {
520567
const {formatMessage} = useIntl()
521568
const {data: basket} = useCurrentBasket()
522569
const updateShippingMethod = useShopperBasketsMutation('updateShippingMethodForShipment')
@@ -527,11 +574,26 @@ const ShipmentMethods = ({shipment, index, currency}) => {
527574
shipmentId: shipment.shipmentId
528575
}
529576
},
530-
{enabled: Boolean(basket?.basketId && shipment?.shipmentId)}
577+
{
578+
enabled: Boolean(basket?.basketId && shipment?.shipmentId),
579+
onSuccess: (data) => {
580+
if (!onShipmentMethodsResolved) return
581+
const deliveryOnly = getDeliveryShippingMethods(data?.applicableShippingMethods)
582+
const noDeliveryMethods = !deliveryOnly || deliveryOnly.length === 0
583+
onShipmentMethodsResolved(shipment.shipmentId, noDeliveryMethods)
584+
}
585+
}
531586
)
532587
const [selected, setSelected] = useState(shipment?.shippingMethod?.id || undefined)
533588
const [hasAutoSelected, setHasAutoSelected] = useState(false)
534589

590+
// Sync to parent when methods are already available (from cache)
591+
useEffect(() => {
592+
if (!onShipmentMethodsResolved || methods === undefined) return
593+
const deliveryMethods = getDeliveryShippingMethods(methods?.applicableShippingMethods)
594+
onShipmentMethodsResolved(shipment.shipmentId, deliveryMethods.length === 0)
595+
}, [methods, shipment.shipmentId, onShipmentMethodsResolved])
596+
535597
useEffect(() => {
536598
// Only attempt auto-select when there are applicable methods available and we haven't already auto-selected
537599
const applicableMethods = getDeliveryShippingMethods(methods?.applicableShippingMethods)
@@ -674,5 +736,6 @@ const ShipmentMethods = ({shipment, index, currency}) => {
674736
ShipmentMethods.propTypes = {
675737
shipment: PropTypes.object.isRequired,
676738
index: PropTypes.number.isRequired,
677-
currency: PropTypes.string.isRequired
739+
currency: PropTypes.string.isRequired,
740+
onShipmentMethodsResolved: PropTypes.func
678741
}

0 commit comments

Comments
 (0)