diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md
index 97df94e750..9455479e60 100644
--- a/packages/template-retail-react-app/CHANGELOG.md
+++ b/packages/template-retail-react-app/CHANGELOG.md
@@ -5,6 +5,7 @@
- Enhanced the shopping assistant that integrates Salesforce Embedded Messaging Service with PWA Kit applications, adding comprehensive context support, localization capabilities, and improved user experience features. [#3259](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3259)
- [Breaking] Removed domainUrl, locale, basetId properties as part off the ShopperAgent during initialization. [#3259](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3259)
- Added support for Choice of Bonus Products feature. Users can now select from available bonus products when they qualify for the associated promotion. The bonus product selection flow can be entered from either the "Item Added to Cart" modal (when adding the qualifying product to the cart) or from the cart page. [#3292] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3292)
+- Fixed shipping method section expanding unnecessarily when default options are auto-selected.[#3345] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3345)
## v8.0.0 (Sep 04, 2025)
- Add support for environment level base paths on /mobify routes [#2892](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2892)
diff --git a/packages/template-retail-react-app/app/pages/checkout/index.test.js b/packages/template-retail-react-app/app/pages/checkout/index.test.js
index 068d2e1bf6..552eda1efd 100644
--- a/packages/template-retail-react-app/app/pages/checkout/index.test.js
+++ b/packages/template-retail-react-app/app/pages/checkout/index.test.js
@@ -552,7 +552,8 @@ test('Can edit address during checkout as a registered customer', async () => {
expect(screen.getByTestId('sf-toggle-card-step-2-content')).not.toBeEmptyDOMElement()
})
- expect(screen.getByText('369 Main Street')).toBeInTheDocument()
+ const shippingAddressCard = screen.getByTestId('sf-toggle-card-step-1-content')
+ expect(within(shippingAddressCard).getByText('369 Main Street')).toBeInTheDocument()
})
test('Can add address during checkout as a registered customer', async () => {
diff --git a/packages/template-retail-react-app/app/pages/checkout/partials/shipping-methods.jsx b/packages/template-retail-react-app/app/pages/checkout/partials/shipping-methods.jsx
index 06e80bbb5e..6985d1f029 100644
--- a/packages/template-retail-react-app/app/pages/checkout/partials/shipping-methods.jsx
+++ b/packages/template-retail-react-app/app/pages/checkout/partials/shipping-methods.jsx
@@ -192,6 +192,34 @@ export default function ShippingMethods() {
)
if (hasNewFields) {
form.reset(newDefaults)
+ deliveryShipments.forEach(async (shipment) => {
+ const methodId = newDefaults[`shippingMethodId_${shipment.shipmentId}`]
+ const hasMethodInBasket = shipment.shippingMethod && shipment.shippingMethod.id
+
+ // auto-submit if;
+ // - default method to submit present
+ // - the shipment doesn't already have a method in basket
+ // - user hasn't manually selected
+ if (
+ methodId &&
+ !hasMethodInBasket &&
+ methodId === shippingMethods?.defaultShippingMethodId
+ ) {
+ try {
+ await updateShippingMethod.mutateAsync({
+ parameters: {
+ basketId: basket.basketId,
+ shipmentId: shipment.shipmentId
+ },
+ body: {
+ id: methodId
+ }
+ })
+ } catch (error) {
+ console.warn(error)
+ }
+ }
+ })
}
}, [deliveryShipments.length, shippingMethods?.defaultShippingMethodId])
diff --git a/packages/template-retail-react-app/app/pages/checkout/partials/shipping-methods.test.js b/packages/template-retail-react-app/app/pages/checkout/partials/shipping-methods.test.js
index 21a342870a..326b45d8c6 100644
--- a/packages/template-retail-react-app/app/pages/checkout/partials/shipping-methods.test.js
+++ b/packages/template-retail-react-app/app/pages/checkout/partials/shipping-methods.test.js
@@ -12,7 +12,11 @@ import ShippingMethods from '@salesforce/retail-react-app/app/pages/checkout/par
import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout/util/checkout-context'
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
import {useCurrency} from '@salesforce/retail-react-app/app/hooks'
-import {useShippingMethodsForShipment, useProducts} from '@salesforce/commerce-sdk-react'
+import {
+ useShippingMethodsForShipment,
+ useProducts,
+ useShopperBasketsMutation
+} from '@salesforce/commerce-sdk-react'
// Mock the hooks
jest.mock('@salesforce/retail-react-app/app/pages/checkout/util/checkout-context')
@@ -25,6 +29,7 @@ const mockUseCurrentBasket = useCurrentBasket
const mockUseCurrency = useCurrency
const mockUseShippingMethodsForShipment = useShippingMethodsForShipment
const mockUseProducts = useProducts
+const mockUseShopperBasketsMutation = useShopperBasketsMutation
// Mock data
const mockBasket = {
@@ -169,6 +174,7 @@ describe('ShippingMethods', () => {
data: mockProductsMap,
isLoading: false
})
+ mockUseShopperBasketsMutation.mockReturnValue(jest.fn().mockResolvedValue({}))
})
afterEach(() => {
@@ -538,4 +544,160 @@ describe('ShippingMethods', () => {
expect(screen.getByText('Standard Shipping')).toBeInTheDocument()
})
})
+
+ describe('auto-submit functionality', () => {
+ test('should auto-submit default shipping method when available', async () => {
+ const basketWithoutMethods = {
+ ...mockBasket,
+ shipments: [
+ {
+ ...mockBasket.shipments[0],
+ shippingMethod: null
+ }
+ ]
+ }
+
+ const mockShippingMethods = {
+ defaultShippingMethodId: 'default-shipping-method',
+ applicableShippingMethods: [
+ {
+ id: 'default-shipping-method',
+ name: 'Default Shipping'
+ }
+ ]
+ }
+
+ const mockMutateAsync = jest.fn().mockResolvedValue({})
+ mockUseShopperBasketsMutation.mockReturnValue({
+ updateShippingMethod: {mutateAsync: mockMutateAsync}
+ })
+
+ // after auto-submit, step should advance to PAYMENT (summary mode)
+ mockUseCheckout.mockReturnValue({
+ step: 'PAYMENT',
+ STEPS: {SHIPPING_OPTIONS: 'SHIPPING_OPTIONS', PAYMENT: 'PAYMENT'},
+ goToStep: jest.fn(),
+ goToNextStep: jest.fn()
+ })
+
+ mockUseCurrentBasket.mockReturnValue({
+ data: basketWithoutMethods,
+ derivedData: {
+ totalShippingCost: 5.99,
+ isMissingShippingMethod: false
+ },
+ isLoading: false
+ })
+
+ mockUseShippingMethodsForShipment.mockReturnValue({
+ data: mockShippingMethods,
+ isLoading: false
+ })
+
+ renderWithIntl()
+
+ // component is in SUMMARY mode (collapsed) after auto-submit
+ expect(screen.getByRole('button', {name: 'Edit Shipping Options'})).toBeInTheDocument()
+ expect(
+ screen.queryByRole('radio', {name: 'Default Shipping $5.99'})
+ ).not.toBeInTheDocument()
+ expect(
+ screen.queryByRole('button', {name: 'Continue to Payment'})
+ ).not.toBeInTheDocument()
+ })
+
+ test('should not auto-submit if shipment already has a method', async () => {
+ // Mock basket that already has a shipping method
+ const basketWithMethod = {
+ ...mockBasket,
+ shipments: [
+ {
+ ...mockBasket.shipments[0],
+ shippingMethod: {
+ id: 'existing-method',
+ name: 'Existing Shipping'
+ }
+ }
+ ]
+ }
+
+ const mockUpdateShippingMethod = jest.fn().mockResolvedValue({})
+ mockUpdateShippingMethod.mutateAsync = jest.fn().mockResolvedValue({})
+
+ // Mock the mutation hook
+ mockUseShopperBasketsMutation.mockReturnValue(mockUpdateShippingMethod)
+
+ mockUseCurrentBasket.mockReturnValue({
+ data: basketWithMethod,
+ derivedData: {totalShippingCost: 0},
+ isLoading: false
+ })
+
+ mockUseShippingMethodsForShipment.mockReturnValue({
+ data: {
+ defaultShippingMethodId: 'default-method',
+ applicableShippingMethods: []
+ },
+ isLoading: false
+ })
+
+ renderWithIntl()
+
+ // no auto-submit happens
+ await waitFor(() => {
+ expect(mockUpdateShippingMethod).not.toHaveBeenCalled()
+ })
+ })
+
+ test('should not auto-submit if user has manually selected a different method', async () => {
+ const basketWithoutMethods = {
+ ...mockBasket,
+ shipments: [
+ {
+ ...mockBasket.shipments[0],
+ shippingMethod: null
+ }
+ ]
+ }
+
+ // Mock shipping methods with default
+ const mockShippingMethods = {
+ defaultShippingMethodId: 'default-method',
+ applicableShippingMethods: [
+ {
+ id: 'default-method',
+ name: 'Default Shipping'
+ },
+ {
+ id: 'user-selected-method',
+ name: 'User Selected Shipping'
+ }
+ ]
+ }
+
+ const mockUpdateShippingMethod = jest.fn().mockResolvedValue({})
+ mockUpdateShippingMethod.mutateAsync = jest.fn().mockResolvedValue({})
+
+ // Mock the mutation hook
+ mockUseShopperBasketsMutation.mockReturnValue(mockUpdateShippingMethod)
+
+ mockUseCurrentBasket.mockReturnValue({
+ data: basketWithoutMethods,
+ derivedData: {totalShippingCost: 0},
+ isLoading: false
+ })
+
+ mockUseShippingMethodsForShipment.mockReturnValue({
+ data: mockShippingMethods,
+ isLoading: false
+ })
+
+ renderWithIntl()
+
+ // no auto-submit happens because the form would have user-selected-method, not default-method)
+ await waitFor(() => {
+ expect(mockUpdateShippingMethod).not.toHaveBeenCalled()
+ })
+ })
+ })
})