From 6ac7ee34f1515415898a47a68053a8888bbea69e Mon Sep 17 00:00:00 2001 From: "d.phan" Date: Wed, 18 Feb 2026 15:10:54 -0500 Subject: [PATCH 01/12] @W-21294742: No error toast for non-applicable shipping method Signed-off-by: d.phan --- .../partials/one-click-shipping-options.jsx | 57 +++++++++---------- .../static/translations/compiled/en-GB.json | 2 +- .../static/translations/compiled/en-US.json | 2 +- .../static/translations/compiled/en-XA.json | 2 +- .../app/utils/shipment-utils.js | 10 ++++ .../app/utils/shipment-utils.test.js | 32 +++++++++++ .../translations/en-GB.json | 2 +- .../translations/en-US.json | 2 +- 8 files changed, 74 insertions(+), 35 deletions(-) diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options.jsx b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options.jsx index 586f0ded71..89883bc48c 100644 --- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options.jsx +++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options.jsx @@ -33,7 +33,8 @@ import {useCheckoutAutoSelect} from '@salesforce/retail-react-app/app/hooks/use- import {useCurrency} from '@salesforce/retail-react-app/app/hooks' import { isPickupShipment, - isPickupMethod + isPickupMethod, + getDeliveryShippingMethods } from '@salesforce/retail-react-app/app/utils/shipment-utils' import PropTypes from 'prop-types' import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast' @@ -69,18 +70,18 @@ export default function ShippingOptions() { step === STEPS.SHIPPING_OPTIONS && !hasMultipleDeliveryShipments, onSuccess: (data) => { - const noMethods = - !data?.applicableShippingMethods || data.applicableShippingMethods.length === 0 + const deliveryOnly = getDeliveryShippingMethods(data?.applicableShippingMethods) + const noDeliveryMethods = !deliveryOnly || deliveryOnly.length === 0 if ( step === STEPS.SHIPPING_OPTIONS && !hasMultipleDeliveryShipments && - noMethods && + noDeliveryMethods && !noMethodsToastShown ) { showToast({ title: formatMessage({ defaultMessage: - 'No shipping methods are available for this address. Please enter a different address.', + 'Unfortunately, we are unable to ship to this address at this time. Please reach out to customer support for further assistance.', id: 'shipping_options.error.no_shipping_methods' }), status: 'error' @@ -94,11 +95,15 @@ export default function ShippingOptions() { const selectedShippingMethod = targetDeliveryShipment?.shippingMethod const selectedShippingAddress = targetDeliveryShipment?.shippingAddress + // Reset error toast when state changes + useEffect(() => { + setNoMethodsToastShown(false) + }, [selectedShippingAddress?.stateCode]) + // Filter out pickup methods for delivery shipment - const deliveryMethods = - (shippingMethods?.applicableShippingMethods || []).filter( - (method) => !isPickupMethod(method) - ) || [] + const deliveryMethods = getDeliveryShippingMethods( + shippingMethods?.applicableShippingMethods || [] + ) const {isLoading: isAutoSelectLoading} = useCheckoutAutoSelect({ currentStep: step, @@ -137,10 +142,9 @@ export default function ShippingOptions() { // Calculate if we should show loading state immediately for auto-selection const shouldShowInitialLoading = useMemo(() => { - const filteredMethods = - shippingMethods?.applicableShippingMethods?.filter( - (method) => !isPickupMethod(method) - ) || [] + const filteredMethods = getDeliveryShippingMethods( + shippingMethods?.applicableShippingMethods + ) const defaultMethodId = shippingMethods?.defaultShippingMethodId const defaultMethod = defaultMethodId ? shippingMethods.applicableShippingMethods?.find( @@ -173,11 +177,9 @@ export default function ShippingOptions() { }) useEffect(() => { - // Filter out pickup methods - const filteredMethods = - shippingMethods?.applicableShippingMethods?.filter( - (method) => !isPickupMethod(method) - ) || [] + const filteredMethods = getDeliveryShippingMethods( + shippingMethods?.applicableShippingMethods + ) const defaultMethodId = shippingMethods?.defaultShippingMethodId // Only use default if it's not a pickup method @@ -227,10 +229,9 @@ export default function ShippingOptions() { shippingItem?.priceAfterItemDiscount || 0 ) - // Filter out pickup methods for all shipments - const filteredShippingMethods = - shippingMethods?.applicableShippingMethods?.filter((method) => !isPickupMethod(method)) || - [] + const filteredShippingMethods = getDeliveryShippingMethods( + shippingMethods?.applicableShippingMethods + ) const freeLabel = formatMessage({ defaultMessage: 'Free', @@ -533,9 +534,7 @@ const ShipmentMethods = ({shipment, index, currency}) => { useEffect(() => { // Only attempt auto-select when there are applicable methods available and we haven't already auto-selected - // Filter out pickup methods for multi-shipments - const applicableMethods = - methods?.applicableShippingMethods?.filter((method) => !isPickupMethod(method)) || [] + const applicableMethods = getDeliveryShippingMethods(methods?.applicableShippingMethods) const applicableIds = applicableMethods.map((m) => m.id) if (!applicableIds.length || hasAutoSelected) { return @@ -613,11 +612,9 @@ const ShipmentMethods = ({shipment, index, currency}) => { )} {(() => { - // Filter out pickup methods for multi-shipments - const filteredMethods = - methods?.applicableShippingMethods?.filter( - (method) => !isPickupMethod(method) - ) || [] + const filteredMethods = getDeliveryShippingMethods( + methods?.applicableShippingMethods + ) return filteredMethods.length > 0 ? ( { return shippingMethod?.c_storePickupEnabled === true } +/** + * Returns only delivery (non-pickup) shipping methods from an array of applicable methods. + * @param {Array} applicableShippingMethods - Array of shipping method objects (e.g. from getShippingMethodsForShipment). May be null/undefined. + * @returns {Array} Array of shipping methods that are not pickup methods. Never null. + */ +export const getDeliveryShippingMethods = (applicableShippingMethods) => { + if (!Array.isArray(applicableShippingMethods)) return [] + return applicableShippingMethods.filter((method) => !isPickupMethod(method)) +} + /** * Checks if a shipment is configured for pickup-in-store * @param {object} shipment the shipment to check. can be null. diff --git a/packages/template-retail-react-app/app/utils/shipment-utils.test.js b/packages/template-retail-react-app/app/utils/shipment-utils.test.js index f54fa2560f..cb7471415f 100644 --- a/packages/template-retail-react-app/app/utils/shipment-utils.test.js +++ b/packages/template-retail-react-app/app/utils/shipment-utils.test.js @@ -8,6 +8,7 @@ import { isPickupMethod, isPickupShipment, + getDeliveryShippingMethods, getItemsForShipment, findEmptyShipments, groupItemsByAddress, @@ -431,6 +432,37 @@ describe('shipment-utils', () => { }) }) + describe('getDeliveryShippingMethods', () => { + test('returns only non-pickup methods', () => { + const methods = [ + {id: '001', name: 'Standard', c_storePickupEnabled: false}, + {id: '005', name: 'Store Pickup', c_storePickupEnabled: true}, + {id: '002', name: 'Express', c_storePickupEnabled: false} + ] + expect(getDeliveryShippingMethods(methods)).toEqual([ + {id: '001', name: 'Standard', c_storePickupEnabled: false}, + {id: '002', name: 'Express', c_storePickupEnabled: false} + ]) + }) + + test('returns empty array for null or undefined', () => { + expect(getDeliveryShippingMethods(null)).toEqual([]) + expect(getDeliveryShippingMethods(undefined)).toEqual([]) + }) + + test('returns empty array for non-array input', () => { + expect(getDeliveryShippingMethods({})).toEqual([]) + }) + + test('returns all methods when none are pickup', () => { + const methods = [ + {id: '001', name: 'Standard'}, + {id: '002', name: 'Express'} + ] + expect(getDeliveryShippingMethods(methods)).toEqual(methods) + }) + }) + describe('isPickupShipment', () => { test('should return true for pickup shipment', () => { const pickupShipment = { diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 4a2c79e17d..409ced4279 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -1821,7 +1821,7 @@ "defaultMessage": "Continue to Payment" }, "shipping_options.error.no_shipping_methods": { - "defaultMessage": "No shipping methods are available for this address. Please enter a different address." + "defaultMessage": "Unfortunately, we are unable to ship to this address at this time. Please reach out to customer support for further assistance." }, "shipping_options.free": { "defaultMessage": "Free" diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 4a2c79e17d..409ced4279 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -1821,7 +1821,7 @@ "defaultMessage": "Continue to Payment" }, "shipping_options.error.no_shipping_methods": { - "defaultMessage": "No shipping methods are available for this address. Please enter a different address." + "defaultMessage": "Unfortunately, we are unable to ship to this address at this time. Please reach out to customer support for further assistance." }, "shipping_options.free": { "defaultMessage": "Free" From 6d96809d595d904b38321310539ad78654a19fdf Mon Sep 17 00:00:00 2001 From: "d.phan" Date: Wed, 18 Feb 2026 15:16:32 -0500 Subject: [PATCH 02/12] add changelog Signed-off-by: d.phan --- packages/template-retail-react-app/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index 1f24aa2509..835b1f3acb 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -1,5 +1,6 @@ ## v9.1.0-dev - Add Node 24 support. Drop Node 16 support [#3652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3652) +- [Bugfix] Fix error toast for no applicable shipping methods in one-click checkout [#3673](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3673) ## v9.0.0 (Feb 12, 2026) - [Feature] One Click Checkout (in Developer Preview) [#3552](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3552) From c1059b88651a73a8d3aefbc33983e622d51dee9d Mon Sep 17 00:00:00 2001 From: "d.phan" Date: Thu, 19 Feb 2026 10:25:46 -0500 Subject: [PATCH 03/12] fix for multi-shipment Signed-off-by: d.phan --- .../partials/one-click-shipping-options.jsx | 159 ++++++++---- .../one-click-shipping-options.test.js | 228 +++++++++++++----- 2 files changed, 273 insertions(+), 114 deletions(-) diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options.jsx b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options.jsx index 89883bc48c..1d4ddd5b50 100644 --- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options.jsx +++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options.jsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import React, {useEffect, useState, useMemo} from 'react' +import React, {useCallback, useEffect, useState, useMemo} from 'react' import {FormattedMessage, FormattedNumber, useIntl} from 'react-intl' import { Box, @@ -51,6 +51,7 @@ export default function ShippingOptions() { const updateShippingMethod = useShopperBasketsMutation('updateShippingMethodForShipment') const showToast = useToast() const [noMethodsToastShown, setNoMethodsToastShown] = useState(false) + const [shipmentIdsWithNoMethods, setShipmentIdsWithNoMethods] = useState(() => new Set()) // Identify delivery shipments (exclude pickup and those without shipping addresses) const deliveryShipments = basket?.shipments?.filter((s) => s.shippingAddress && !isPickupShipment(s)) || [] @@ -68,42 +69,79 @@ export default function ShippingOptions() { enabled: Boolean(basket?.basketId) && step === STEPS.SHIPPING_OPTIONS && - !hasMultipleDeliveryShipments, - onSuccess: (data) => { - const deliveryOnly = getDeliveryShippingMethods(data?.applicableShippingMethods) - const noDeliveryMethods = !deliveryOnly || deliveryOnly.length === 0 - if ( - step === STEPS.SHIPPING_OPTIONS && - !hasMultipleDeliveryShipments && - noDeliveryMethods && - !noMethodsToastShown - ) { - showToast({ - title: formatMessage({ - defaultMessage: - 'Unfortunately, we are unable to ship to this address at this time. Please reach out to customer support for further assistance.', - id: 'shipping_options.error.no_shipping_methods' - }), - status: 'error' - }) - setNoMethodsToastShown(true) - } - } + !hasMultipleDeliveryShipments + // Single-shipment "no methods" toast is handled in useEffect when data is available } ) const selectedShippingMethod = targetDeliveryShipment?.shippingMethod const selectedShippingAddress = targetDeliveryShipment?.shippingAddress - // Reset error toast when state changes + // Reset error toast when address state changes (affecting applicable shipping methods) + const deliveryAddressStateKey = hasMultipleDeliveryShipments + ? deliveryShipments.map((s) => s.shippingAddress?.stateCode ?? '').join(',') + : selectedShippingAddress?.stateCode useEffect(() => { setNoMethodsToastShown(false) - }, [selectedShippingAddress?.stateCode]) + }, [deliveryAddressStateKey]) // Filter out pickup methods for delivery shipment const deliveryMethods = getDeliveryShippingMethods( shippingMethods?.applicableShippingMethods || [] ) + const noShippingMethodsToast = useMemo( + () => ({ + title: formatMessage({ + defaultMessage: + 'Unfortunately, we are unable to ship to this address at this time. Please reach out to customer support for further assistance.', + id: 'shipping_options.error.no_shipping_methods' + }), + status: 'error' + }), + [formatMessage] + ) + + // For multi-shipment, report whether this shipment has no applicable delivery methods + const handleShipmentMethodsResolved = useCallback( + (shipmentId, hasNoApplicableMethods) => { + setShipmentIdsWithNoMethods((prev) => { + const next = new Set(prev) + if (hasNoApplicableMethods) next.add(shipmentId) + else next.delete(shipmentId) + return next + }) + if ( + hasNoApplicableMethods && + step === STEPS.SHIPPING_OPTIONS && + hasMultipleDeliveryShipments + ) { + setNoMethodsToastShown((prev) => { + if (!prev) showToast(noShippingMethodsToast) + return true + }) + } + }, + [ + step, + STEPS.SHIPPING_OPTIONS, + hasMultipleDeliveryShipments, + showToast, + noShippingMethodsToast + ] + ) + const hasAnyShipmentWithNoMethods = shipmentIdsWithNoMethods.size > 0 + + // Single shipment: show toast when methods data is available and has no delivery methods + const singleShipmentNoMethods = + !hasMultipleDeliveryShipments && + step === STEPS.SHIPPING_OPTIONS && + shippingMethods != null && + deliveryMethods.length === 0 + useEffect(() => { + if (!singleShipmentNoMethods || noMethodsToastShown) return + showToast(noShippingMethodsToast) + setNoMethodsToastShown(true) + }, [singleShipmentNoMethods, noMethodsToastShown, showToast, noShippingMethodsToast]) const {isLoading: isAutoSelectLoading} = useCheckoutAutoSelect({ currentStep: step, @@ -229,10 +267,6 @@ export default function ShippingOptions() { shippingItem?.priceAfterItemDiscount || 0 ) - const filteredShippingMethods = getDeliveryShippingMethods( - shippingMethods?.applicableShippingMethods - ) - const freeLabel = formatMessage({ defaultMessage: 'Free', id: 'checkout_confirmation.label.free' @@ -280,18 +314,21 @@ export default function ShippingOptions() { index={idx + 1} shipment={shipment} currency={currency} + onShipmentMethodsResolved={handleShipmentMethodsResolved} /> ))} - - - - - + {!hasAnyShipmentWithNoMethods && ( + + + + + + )} ) : (
- {filteredShippingMethods.length > 0 && ( + {deliveryMethods.length > 0 && ( - {filteredShippingMethods.map((opt) => ( + {deliveryMethods.map((opt) => ( @@ -347,7 +384,7 @@ export default function ShippingOptions() { )} /> )} - {filteredShippingMethods.length > 0 && ( + {deliveryMethods.length > 0 && (