@salesforce/retail-react-app 7.1.0-preview.0 → 8.0.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +8 -4
  2. package/app/components/_app/index.jsx +9 -7
  3. package/app/components/_app/index.test.js +2 -2
  4. package/app/components/_app-config/index.jsx +9 -3
  5. package/app/components/drawer-menu/drawer-menu.jsx +3 -1
  6. package/app/components/footer/index.jsx +3 -1
  7. package/app/components/header/index.jsx +3 -1
  8. package/app/components/header/index.test.js +2 -2
  9. package/app/components/island/README.md +1 -1
  10. package/app/components/island/index.jsx +3 -1
  11. package/app/components/island/index.test.js +94 -5
  12. package/app/components/item-variant/item-attributes.jsx +12 -3
  13. package/app/components/multiship/multiship-order-summary.jsx +137 -0
  14. package/app/components/multiship/multiship-order-summary.test.js +121 -0
  15. package/app/components/order-summary/index.jsx +2 -4
  16. package/app/components/pickup-or-delivery/index.jsx +80 -0
  17. package/app/components/pickup-or-delivery/index.test.jsx +182 -0
  18. package/app/components/product-item/index.jsx +26 -16
  19. package/app/components/product-item/index.test.js +29 -2
  20. package/app/components/product-item-list/index.jsx +10 -0
  21. package/app/components/product-item-list/index.test.jsx +14 -0
  22. package/app/components/product-view/index.jsx +9 -6
  23. package/app/components/product-view/index.test.js +25 -21
  24. package/app/components/quantity-picker/index.test.jsx +12 -12
  25. package/app/components/reset-password/index.test.js +1 -1
  26. package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
  27. package/app/components/shared/ui/index.jsx +1 -0
  28. package/app/components/store-display/index.jsx +28 -4
  29. package/app/components/store-display/index.test.js +71 -0
  30. package/app/components/store-locator/form.test.jsx +16 -4
  31. package/app/components/store-locator/list.jsx +9 -4
  32. package/app/components/toggle-card/index.jsx +14 -0
  33. package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
  34. package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
  35. package/app/constants.js +20 -6
  36. package/app/contexts/store-locator-provider.jsx +7 -1
  37. package/app/contexts/store-locator-provider.test.jsx +36 -1
  38. package/app/hooks/use-address-form.js +155 -0
  39. package/app/hooks/use-address-form.test.js +501 -0
  40. package/app/hooks/use-auth-modal.js +2 -6
  41. package/app/hooks/use-current-basket.js +71 -2
  42. package/app/hooks/use-current-basket.test.js +37 -1
  43. package/app/hooks/use-dnt-notification.js +4 -4
  44. package/app/hooks/use-dnt-notification.test.js +5 -5
  45. package/app/hooks/use-item-shipment-management.js +233 -0
  46. package/app/hooks/use-item-shipment-management.test.js +696 -0
  47. package/app/hooks/use-multiship.js +589 -0
  48. package/app/hooks/use-multiship.test.js +776 -0
  49. package/app/hooks/use-pickup-shipment.js +70 -106
  50. package/app/hooks/use-pickup-shipment.test.js +345 -209
  51. package/app/hooks/use-product-address-assignment.js +280 -0
  52. package/app/hooks/use-product-address-assignment.test.js +414 -0
  53. package/app/hooks/use-product-inventory.js +100 -0
  54. package/app/hooks/use-product-inventory.test.js +254 -0
  55. package/app/hooks/use-shipment-operations.js +168 -0
  56. package/app/hooks/use-shipment-operations.test.js +385 -0
  57. package/app/hooks/use-store-locator.js +24 -2
  58. package/app/hooks/use-store-locator.test.jsx +109 -1
  59. package/app/pages/account/index.test.js +1 -1
  60. package/app/pages/account/profile.test.js +0 -2
  61. package/app/pages/cart/index.jsx +397 -157
  62. package/app/pages/cart/index.test.js +353 -2
  63. package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
  64. package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
  65. package/app/pages/cart/partials/order-type-display.jsx +68 -0
  66. package/app/pages/cart/partials/order-type-display.test.js +241 -0
  67. package/app/pages/checkout/confirmation.jsx +79 -158
  68. package/app/pages/checkout/index.jsx +34 -9
  69. package/app/pages/checkout/index.test.js +245 -118
  70. package/app/pages/checkout/partials/contact-info.jsx +2 -6
  71. package/app/pages/checkout/partials/contact-info.test.js +93 -7
  72. package/app/pages/checkout/partials/payment.jsx +19 -5
  73. package/app/pages/checkout/partials/pickup-address.jsx +340 -70
  74. package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
  75. package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
  76. package/app/pages/checkout/partials/shipment-details.jsx +209 -0
  77. package/app/pages/checkout/partials/shipment-details.test.js +246 -0
  78. package/app/pages/checkout/partials/shipping-address.jsx +156 -68
  79. package/app/pages/checkout/partials/shipping-address.test.js +673 -0
  80. package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
  81. package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
  82. package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
  83. package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
  84. package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
  85. package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
  86. package/app/pages/checkout/util/checkout-context.js +25 -18
  87. package/app/pages/login/index.jsx +2 -6
  88. package/app/pages/product-detail/index.jsx +96 -81
  89. package/app/pages/product-detail/index.test.js +103 -19
  90. package/app/pages/product-list/index.jsx +3 -1
  91. package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
  92. package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
  93. package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
  94. package/app/ssr.js +1 -5
  95. package/app/static/translations/compiled/en-GB.json +316 -30
  96. package/app/static/translations/compiled/en-US.json +316 -30
  97. package/app/static/translations/compiled/en-XA.json +673 -75
  98. package/app/utils/address-utils.js +112 -0
  99. package/app/utils/address-utils.test.js +484 -0
  100. package/app/utils/product-utils.js +17 -5
  101. package/app/utils/product-utils.test.js +17 -8
  102. package/app/utils/sfdc-user-agent-utils.js +32 -0
  103. package/app/utils/sfdc-user-agent-utils.test.js +82 -0
  104. package/app/utils/shipment-utils.js +196 -0
  105. package/app/utils/shipment-utils.test.js +458 -0
  106. package/app/utils/test-utils.js +4 -4
  107. package/app/utils/utils.js +6 -1
  108. package/config/default.js +4 -1
  109. package/config/mocks/default.js +3 -1
  110. package/package.json +9 -9
  111. package/translations/en-GB.json +127 -10
  112. package/translations/en-US.json +127 -10
  113. package/app/pages/checkout/partials/shipping-options.jsx +0 -269
package/CHANGELOG.md CHANGED
@@ -1,8 +1,13 @@
1
- ## v7.1.0-preview.0 (Aug 01, 2025)
2
-
1
+ ## v8.0.0-dev (August 19, 2025)
3
2
  - Add support for environment level base paths on /mobify routes [#2892](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2892)
4
-
3
+ - Fix private client endpoint prop name [#3177](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3177)
5
4
  - This feature introduces an AI-powered shopping assistant that integrates Salesforce Embedded Messaging Service with PWA Kit applications. The shopper agent provides real-time chat support, search assistance, and personalized shopping guidance directly within the e-commerce experience. [#2658](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2658)
5
+ - Added support for Multi-Ship [#3056](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3056)
6
+ - The feature toggle for partial hydration is now found in the config file (`config.app.partialHydrationEnabled`) [#3058](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3058)
7
+ - Mask user not found messages to prevent user enumeration from passwordless login [#3113](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3113)
8
+ - [Bugfix] Pin `@chakra-ui/react` version to 2.7.0 to avoid breaking changes from 2.10.9 [#2658](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2658)
9
+ - Introduce optional prop `hybridAuthEnabled` to control Hybrid Auth specific behaviors in commerce-sdk-react [#3151](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3151)
10
+ - Inject sfdc_user_agent request header into all SCAPI requests for debugging and metrics prupose [#3183](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3183)
6
11
 
7
12
  ## v7.0.0 (July 22, 2025)
8
13
 
@@ -28,7 +33,6 @@
28
33
  - Add Data Cloud partyIdentification events and improve error handling [#2811](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2811)
29
34
  - Introduce the cursor rules to assist project developers [#2820](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2820)
30
35
 
31
-
32
36
  ## v6.1.0 (May 22, 2025)
33
37
 
34
38
  - Fix hreflang alternate links [#2269](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2269)
@@ -52,6 +52,7 @@ import Island from '@salesforce/retail-react-app/app/components/island'
52
52
 
53
53
  // Hooks
54
54
  import {AuthModal, useAuthModal} from '@salesforce/retail-react-app/app/hooks/use-auth-modal'
55
+ import {useStoreLocatorModal} from '@salesforce/retail-react-app/app/hooks/use-store-locator'
55
56
  import {
56
57
  DntNotification,
57
58
  useDntNotification
@@ -140,15 +141,16 @@ const App = (props) => {
140
141
  const authModal = useAuthModal()
141
142
  const dntNotification = useDntNotification()
142
143
  const {site, locale, buildUrl} = useMultiSite()
144
+ const {
145
+ isOpen: isStoreLocatorOpen,
146
+ onOpen: onOpenStoreLocator,
147
+ onClose: onCloseStoreLocator
148
+ } = useStoreLocatorModal()
149
+ const storeLocatorEnabled = getConfig()?.app?.storeLocatorEnabled ?? STORE_LOCATOR_IS_ENABLED
143
150
 
144
151
  const [isOnline, setIsOnline] = useState(true)
145
152
  const styles = useStyleConfig('App')
146
153
  const {isOpen, onOpen, onClose} = useDisclosure()
147
- const {
148
- isOpen: isOpenStoreLocator,
149
- onOpen: onOpenStoreLocator,
150
- onClose: onCloseStoreLocator
151
- } = useDisclosure()
152
154
 
153
155
  const targetLocale = getTargetLocale({
154
156
  getUserPreferredLocales: () => {
@@ -374,9 +376,9 @@ const App = (props) => {
374
376
 
375
377
  <Box id="app" display="flex" flexDirection="column" flex={1}>
376
378
  <SkipNavLink zIndex="skipLink">Skip to Content</SkipNavLink>
377
- {STORE_LOCATOR_IS_ENABLED && (
379
+ {storeLocatorEnabled && (
378
380
  <StoreLocatorModal
379
- isOpen={isOpenStoreLocator}
381
+ isOpen={isStoreLocatorOpen}
380
382
  onClose={onCloseStoreLocator}
381
383
  />
382
384
  )}
@@ -26,12 +26,12 @@ jest.mock('../../hooks/use-update-shopper-context', () => ({
26
26
 
27
27
  let windowSpy
28
28
 
29
- const mockUpdateDNT = jest.fn()
29
+ const mockUpdateDnt = jest.fn()
30
30
  jest.mock('@salesforce/commerce-sdk-react', () => {
31
31
  const originalModule = jest.requireActual('@salesforce/commerce-sdk-react')
32
32
  return {
33
33
  ...originalModule,
34
- useDNT: () => ({selectedDnt: undefined, updateDNT: mockUpdateDNT})
34
+ useDNT: () => ({selectedDnt: undefined, updateDnt: mockUpdateDnt})
35
35
  }
36
36
  })
37
37
 
@@ -40,6 +40,7 @@ import {CommerceApiProvider} from '@salesforce/commerce-sdk-react'
40
40
  import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/components/with-react-query'
41
41
  import {useCorrelationId} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
42
42
  import {ReactQueryDevtools} from '@tanstack/react-query-devtools'
43
+ import {generateSfdcUserAgent} from '@salesforce/retail-react-app/app/utils/sfdc-user-agent-utils'
43
44
  import {
44
45
  DEFAULT_DNT_STATE,
45
46
  STORE_LOCATOR_RADIUS,
@@ -51,6 +52,8 @@ import {
51
52
  STORE_LOCATOR_SUPPORTED_COUNTRIES
52
53
  } from '@salesforce/retail-react-app/app/constants'
53
54
 
55
+ const sfdcUserAgent = generateSfdcUserAgent()
56
+
54
57
  /**
55
58
  * Use the AppConfig component to inject extra arguments into the getProps
56
59
  * methods for all Route Components in the app – typically you'd want to do this
@@ -62,7 +65,8 @@ import {
62
65
  const AppConfig = ({children, locals = {}}) => {
63
66
  const {correlationId} = useCorrelationId()
64
67
  const headers = {
65
- 'correlation-id': correlationId
68
+ 'correlation-id': correlationId,
69
+ sfdc_user_agent: sfdcUserAgent
66
70
  }
67
71
 
68
72
  const commerceApiConfig = locals.appConfig.commerceAPI
@@ -102,10 +106,12 @@ const AppConfig = ({children, locals = {}}) => {
102
106
  proxy={proxy}
103
107
  headers={headers}
104
108
  defaultDnt={DEFAULT_DNT_STATE}
105
- // Uncomment 'enablePWAKitPrivateClient' to use SLAS private client login flows.
109
+ // Set 'enablePWAKitPrivateClient' to true to use SLAS private client login flows.
106
110
  // Make sure to also enable useSLASPrivateClient in ssr.js when enabling this setting.
107
111
  enablePWAKitPrivateClient={false}
108
- slasPrivateClientProxyEndpoint={slasPrivateClientProxyEndpoint}
112
+ privateClientProxyEndpoint={slasPrivateClientProxyEndpoint}
113
+ // Uncomment 'hybridAuthEnabled' if the current site has Hybrid Auth enabled. Do NOT set this flag for hybrid storefronts using Plugin SLAS.
114
+ // hybridAuthEnabled={true}
109
115
  logger={createLogger({packageName: 'commerce-sdk-react'})}
110
116
  >
111
117
  <MultiSiteProvider site={locals.site} locale={locals.locale} buildUrl={locals.buildUrl}>
@@ -60,6 +60,7 @@ import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation
60
60
  import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
61
61
 
62
62
  import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants'
63
+ import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
63
64
  // The FONT_SIZES and FONT_WEIGHTS constants are used to control the styling for
64
65
  // the accordion buttons as their current depth. In the below definition we assign
65
66
  // values for depths 0 - 3, any depth deeper than that will use the default styling.
@@ -100,6 +101,7 @@ const DrawerMenu = ({
100
101
  const socialIconVariant = useBreakpointValue({base: 'flex', md: 'flex-start'})
101
102
  const {site, buildUrl} = useMultiSite()
102
103
  const {l10n} = site
104
+ const storeLocatorEnabled = getConfig()?.app?.storeLocatorEnabled ?? STORE_LOCATOR_IS_ENABLED
103
105
  const [showLoading, setShowLoading] = useState(false)
104
106
  const [ariaBusy, setAriaBusy] = useState('true')
105
107
  const logout = useAuthHelper(AuthHelpers.Logout)
@@ -274,7 +276,7 @@ const DrawerMenu = ({
274
276
  </Link>
275
277
  )}
276
278
  </Box>
277
- {STORE_LOCATOR_IS_ENABLED && (
279
+ {storeLocatorEnabled && (
278
280
  <Box {...styles.actionsItem}>
279
281
  <Link to={STORE_LOCATOR_HREF}>
280
282
  <HStack>
@@ -31,6 +31,7 @@ import LocaleText from '@salesforce/retail-react-app/app/components/locale-text'
31
31
  import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
32
32
  import styled from '@emotion/styled'
33
33
  import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants'
34
+ import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
34
35
 
35
36
  const [StylesProvider, useStyles] = createStylesContext('Footer')
36
37
  const Footer = ({...otherProps}) => {
@@ -39,6 +40,7 @@ const Footer = ({...otherProps}) => {
39
40
  const [locale, setLocale] = useState(intl.locale)
40
41
  const {site, buildUrl} = useMultiSite()
41
42
  const {l10n} = site
43
+ const storeLocatorEnabled = getConfig()?.app?.storeLocatorEnabled ?? STORE_LOCATOR_IS_ENABLED
42
44
  const supportedLocaleIds = l10n?.supportedLocales.map((locale) => locale.id)
43
45
  const showLocaleSelector = supportedLocaleIds?.length > 1
44
46
 
@@ -51,7 +53,7 @@ const Footer = ({...otherProps}) => {
51
53
  })
52
54
  const makeOurCompanyLinks = () => {
53
55
  const links = []
54
- if (STORE_LOCATOR_IS_ENABLED)
56
+ if (storeLocatorEnabled)
55
57
  links.push({
56
58
  href: '/store-locator',
57
59
  text: intl.formatMessage({
@@ -51,6 +51,7 @@ import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-
51
51
  import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive'
52
52
  import {isHydrated, noop} from '@salesforce/retail-react-app/app/utils/utils'
53
53
  import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants'
54
+ import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
54
55
  const IconButtonWithRegistration = withRegistration(IconButton)
55
56
 
56
57
  /**
@@ -117,6 +118,7 @@ const Header = ({
117
118
  const {isRegistered} = useCustomerType()
118
119
  const logout = useAuthHelper(AuthHelpers.Logout)
119
120
  const navigate = useNavigation()
121
+ const storeLocatorEnabled = getConfig()?.app?.storeLocatorEnabled ?? STORE_LOCATOR_IS_ENABLED
120
122
  const {
121
123
  getButtonProps: getAccountMenuButtonProps,
122
124
  getDisclosureProps: getAccountMenuDisclosureProps,
@@ -311,7 +313,7 @@ const Header = ({
311
313
  {...styles.wishlistIcon}
312
314
  onClick={onWishlistClick}
313
315
  />
314
- {STORE_LOCATOR_IS_ENABLED && (
316
+ {storeLocatorEnabled && (
315
317
  <IconButton
316
318
  aria-label={intl.formatMessage({
317
319
  defaultMessage: 'Store Locator',
@@ -240,7 +240,7 @@ test('shows loading spinner during sign out process', async () => {
240
240
 
241
241
  test('handles keyboard navigation with Tab+Shift in account menu', async () => {
242
242
  // Test keyboard navigation without requiring auth
243
- const {user} = renderWithProviders(<Header />)
243
+ renderWithProviders(<Header />)
244
244
 
245
245
  // Just verify the component renders without errors when keyboard events occur on any element
246
246
  const accountIcon = screen.getByLabelText('My Account')
@@ -250,7 +250,7 @@ test('handles keyboard navigation with Tab+Shift in account menu', async () => {
250
250
  })
251
251
 
252
252
  test('handles mouse leave events without crashing', async () => {
253
- const {user} = renderWithProviders(<Header />)
253
+ renderWithProviders(<Header />)
254
254
 
255
255
  const accountIcon = screen.getByLabelText('My Account')
256
256
 
@@ -45,7 +45,7 @@ This delay can be achieved using various `hydrateOn` strategies:
45
45
  3. `hydrateOn={'visible'}`: Useful for lower-priority UI elements that don’t need to be immediately interactive. Hydration occurs once the component has [entered the user’s viewport](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver). As a result, such an element doesn’t get hydrated at all if the user never sees it.
46
46
  4. `hydrateOn={'off'}`: Useful for non-interactive UI elements or lower-priority UI elements to completely suppress the hydration for. On the surface, this behavior overlaps in certain ways with Server Components. However, there are fundamental differences. While server components don’t contribute any JavaScript to what’s delivered to the client, islands are client-oriented constructs, and thus the JavaScript for a non-hydrated section is still fully transmitted. After all, `hydrateOn` can be dynamically updated at any time in case of using a binding property instead of a hard-coded value. An initial value of `off` therefore opens up the possibilities for completely custom hydration triggers. Furthermore, if Server Components aren’t supported by the runtime used, `hydrateOn={'off'}` offers a simple way to at least reduce the hydration overhead for certain non-interactive areas.
47
47
 
48
- > **🔀️ Important:** Partial hydration support is put behind a feature toggle that can be turned on by setting the constant `PARTIAL_HYDRATION_ENABLED` to `true` (see `@salesforce/retail-react-app/app/constants`).
48
+ > **🔀️ Important:** Partial hydration support is put behind a feature toggle that can be turned on by setting `partialHydrationEnabled` to `true` (see `@salesforce/retail-react-app/config/default.js`).
49
49
 
50
50
  > **ℹ️️ Note:** `<Island/>` components only influence the hydration behavior of server-rendered content. Once an application bootstrapped on the client and returned to SPA mode, any subsequent client-side rendering is not impacted by the `<Island/>` components anymore.
51
51
 
@@ -19,6 +19,7 @@ import React, {
19
19
  import PropTypes from 'prop-types'
20
20
  import {isServer} from '@salesforce/retail-react-app/app/components/island/utils'
21
21
  import {PARTIAL_HYDRATION_ENABLED} from '@salesforce/retail-react-app/app/constants'
22
+ import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
22
23
 
23
24
  const IslandContext = createContext(null)
24
25
 
@@ -84,7 +85,8 @@ function findChildren(children, componentType) {
84
85
  */
85
86
  function Island(props) {
86
87
  const {children} = props
87
- if (!PARTIAL_HYDRATION_ENABLED) {
88
+ const isEnabled = getConfig()?.app?.partialHydrationEnabled ?? PARTIAL_HYDRATION_ENABLED // in a backward compatible way
89
+ if (!isEnabled) {
88
90
  return <>{children}</>
89
91
  }
90
92
 
@@ -6,17 +6,22 @@
6
6
  */
7
7
  /* eslint-disable no-import-assign */
8
8
  import React from 'react'
9
- import {act, render, screen} from '@testing-library/react'
9
+ import {act, render, screen, cleanup} from '@testing-library/react'
10
10
  import {renderToString} from 'react-dom/server'
11
11
  import Island from '@salesforce/retail-react-app/app/components/island'
12
12
  import {isServer} from '@salesforce/retail-react-app/app/components/island/utils'
13
13
  import * as constants from '@salesforce/retail-react-app/app/constants'
14
+ import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
14
15
 
15
16
  jest.mock('@salesforce/retail-react-app/app/components/island/utils', () => ({
16
17
  ...jest.requireActual('@salesforce/retail-react-app/app/components/island/utils'),
17
18
  isServer: jest.fn()
18
19
  }))
19
20
 
21
+ jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => ({
22
+ getConfig: jest.fn()
23
+ }))
24
+
20
25
  // Setup global mocks
21
26
  const mockRequestIdleCallback = jest.fn()
22
27
  const mockCancelIdleCallback = jest.fn()
@@ -54,6 +59,13 @@ describe('Island Component', () => {
54
59
 
55
60
  beforeEach(() => {
56
61
  jest.clearAllMocks()
62
+ // Set default config to enable partial hydration
63
+ getConfig.mockReturnValue({
64
+ app: {
65
+ partialHydrationEnabled: true
66
+ }
67
+ })
68
+ // Keep the constant as fallback for backward compatibility tests
57
69
  Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', true)
58
70
  global.requestIdleCallback = mockRequestIdleCallback
59
71
  global.cancelIdleCallback = mockCancelIdleCallback
@@ -70,8 +82,12 @@ describe('Island Component', () => {
70
82
  isServer.mockReturnValue(true)
71
83
  })
72
84
 
73
- test('should not render an island at all if constant "PARTIAL_HYDRATION_ENABLED" is false', () => {
74
- Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', false)
85
+ test('should not render an island at all if config "partialHydrationEnabled" is false', () => {
86
+ getConfig.mockReturnValue({
87
+ app: {
88
+ partialHydrationEnabled: false
89
+ }
90
+ })
75
91
 
76
92
  const {container} = render(
77
93
  <Island>
@@ -120,8 +136,12 @@ describe('Island Component', () => {
120
136
  })
121
137
 
122
138
  describe('Client-Side Rendering (CSR)', () => {
123
- test('should not render an island at all if constant "PARTIAL_HYDRATION_ENABLED" is false', () => {
124
- Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', false)
139
+ test('should not render an island at all if config "partialHydrationEnabled" is false', () => {
140
+ getConfig.mockReturnValue({
141
+ app: {
142
+ partialHydrationEnabled: false
143
+ }
144
+ })
125
145
 
126
146
  const {container} = render(
127
147
  <Island>
@@ -911,4 +931,73 @@ describe('Island Component', () => {
911
931
  })
912
932
  })
913
933
  })
934
+
935
+ describe('Backward Compatibility', () => {
936
+ test('should fall back to PARTIAL_HYDRATION_ENABLED constant when config.app.partialHydrationEnabled is not available', () => {
937
+ // Mock getConfig to return config without partialHydrationEnabled property
938
+ getConfig.mockReturnValue({
939
+ app: {}
940
+ })
941
+ Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', true)
942
+
943
+ // Test SSR behavior first
944
+ isServer.mockReturnValue(true)
945
+ const {container: serverContainer, getByTestId: getByTestIdServer} = render(
946
+ <Island>
947
+ <div data-testid="server-content">Server Content</div>
948
+ </Island>
949
+ )
950
+ expect(serverContainer.firstElementChild?.dataset?.sfdcIslandOrigin).toBe('server')
951
+ expect(getByTestIdServer('server-content')).toBeInTheDocument()
952
+
953
+ // Clean up and test CSR behavior
954
+ cleanup()
955
+ isServer.mockReturnValue(false)
956
+ const {container: clientContainer, getByTestId: getByTestIdClient} = render(
957
+ <Island>
958
+ <div data-testid="server-content">Server Content</div>
959
+ </Island>
960
+ )
961
+ expect(clientContainer.firstElementChild?.dataset?.sfdcIslandOrigin).toBe('client')
962
+ expect(getByTestIdClient('server-content')).toBeInTheDocument()
963
+ })
964
+
965
+ test('should disable islands when both config and constant are false', () => {
966
+ getConfig.mockReturnValue({
967
+ app: {
968
+ partialHydrationEnabled: false
969
+ }
970
+ })
971
+ Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', false)
972
+
973
+ const {container} = render(
974
+ <Island>
975
+ <div data-testid="server-content">Server Content</div>
976
+ </Island>
977
+ )
978
+
979
+ // Should render children directly without island wrapper
980
+ expect(screen.getByTestId('server-content')).toBeInTheDocument()
981
+ expect(screen.getByTestId('server-content')).toBe(container.firstElementChild)
982
+ })
983
+
984
+ test('should disable islands when config is false even if constant is true', () => {
985
+ getConfig.mockReturnValue({
986
+ app: {
987
+ partialHydrationEnabled: false
988
+ }
989
+ })
990
+ Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', true)
991
+
992
+ const {container} = render(
993
+ <Island>
994
+ <div data-testid="server-content">Server Content</div>
995
+ </Island>
996
+ )
997
+
998
+ // Config should take precedence over constant
999
+ expect(screen.getByTestId('server-content')).toBeInTheDocument()
1000
+ expect(screen.getByTestId('server-content')).toBe(container.firstElementChild)
1001
+ })
1002
+ })
914
1003
  })
@@ -20,7 +20,13 @@ import {getDisplayVariationValues} from '@salesforce/retail-react-app/app/utils/
20
20
  * In the context of a cart product item variant, this component renders a styled
21
21
  * list of the selected variation values as well as any promos (w/ info popover).
22
22
  */
23
- const ItemAttributes = ({includeQuantity, currency, excludeBonusLabel, ...props}) => {
23
+ const ItemAttributes = ({
24
+ includeQuantity,
25
+ currency,
26
+ excludeBonusLabel,
27
+ hideAttributeLabels = false,
28
+ ...props
29
+ }) => {
24
30
  const variant = useItemVariant()
25
31
  const {data: basket} = useCurrentBasket()
26
32
  const {currency: activeCurrency} = useCurrency()
@@ -110,7 +116,9 @@ const ItemAttributes = ({includeQuantity, currency, excludeBonusLabel, ...props}
110
116
  fontSize="sm"
111
117
  key={`${key}: ${variationValues[key]}`}
112
118
  >
113
- {key}: {variationValues[key]}
119
+ {hideAttributeLabels
120
+ ? variationValues[key]
121
+ : `${key}: ${variationValues[key]}`}
114
122
  </Text>
115
123
  ))}
116
124
 
@@ -222,7 +230,8 @@ const ItemAttributes = ({includeQuantity, currency, excludeBonusLabel, ...props}
222
230
  ItemAttributes.propTypes = {
223
231
  includeQuantity: PropTypes.bool,
224
232
  currency: PropTypes.string,
225
- excludeBonusLabel: PropTypes.bool
233
+ excludeBonusLabel: PropTypes.bool,
234
+ hideAttributeLabels: PropTypes.bool
226
235
  }
227
236
 
228
237
  export default ItemAttributes
@@ -0,0 +1,137 @@
1
+ /*
2
+ * Copyright (c) 2025, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import React from 'react'
8
+ import {defineMessages, FormattedMessage} from 'react-intl'
9
+ import PropTypes from 'prop-types'
10
+ import {
11
+ Box,
12
+ Stack,
13
+ Text,
14
+ Flex,
15
+ Divider
16
+ } from '@salesforce/retail-react-app/app/components/shared/ui'
17
+ import ItemVariantProvider from '@salesforce/retail-react-app/app/components/item-variant'
18
+ import CartItemVariantImage from '@salesforce/retail-react-app/app/components/item-variant/item-image'
19
+ import CartItemVariantName from '@salesforce/retail-react-app/app/components/item-variant/item-name'
20
+ import CartItemVariantAttributes from '@salesforce/retail-react-app/app/components/item-variant/item-attributes'
21
+ import CartItemVariantPrice from '@salesforce/retail-react-app/app/components/item-variant/item-price'
22
+ import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants'
23
+ import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
24
+ import {isPickupShipment} from '@salesforce/retail-react-app/app/utils/shipment-utils'
25
+
26
+ const MultiShipOrderSummary = ({order, productItemsMap, currency}) => {
27
+ const storeLocatorEnabled = getConfig()?.app?.storeLocatorEnabled ?? STORE_LOCATOR_IS_ENABLED
28
+ // Group shipments by type (pickup vs delivery)
29
+ const pickupShipments = []
30
+ const deliveryShipments = []
31
+
32
+ const messages = defineMessages({
33
+ pickupItems: {
34
+ id: 'order_summary.label.pickup_items',
35
+ defaultMessage: 'Pickup Items'
36
+ },
37
+ deliveryItems: {
38
+ id: 'order_summary.label.delivery_items',
39
+ defaultMessage: 'Delivery Items'
40
+ }
41
+ })
42
+
43
+ order.shipments.forEach((shipment) => {
44
+ const isPickup = storeLocatorEnabled && isPickupShipment(shipment)
45
+
46
+ if (isPickup) {
47
+ pickupShipments.push(shipment)
48
+ } else {
49
+ deliveryShipments.push(shipment)
50
+ }
51
+ })
52
+
53
+ // Group product items by shipment
54
+ const getItemsForShipment = (shipmentId) => {
55
+ return order.productItems.filter((item) => item.shipmentId === shipmentId)
56
+ }
57
+
58
+ const renderItemGroup = (shipments, title) => {
59
+ if (shipments.length === 0) return null
60
+
61
+ return (
62
+ <Box key={title.id}>
63
+ <Text fontSize="sm" fontWeight="semibold" mb={3}>
64
+ <FormattedMessage {...title} />
65
+ </Text>
66
+ <Stack spacing={4}>
67
+ {shipments.map((shipment) => {
68
+ const items = getItemsForShipment(shipment.shipmentId)
69
+
70
+ return (
71
+ <Box key={shipment.shipmentId}>
72
+ <Stack
73
+ spacing={3}
74
+ align="flex-start"
75
+ width="full"
76
+ divider={<Divider />}
77
+ >
78
+ {items.map((product, idx) => {
79
+ const productDetail =
80
+ productItemsMap?.[product.productId] || {}
81
+ const variant = {
82
+ ...product,
83
+ ...productDetail,
84
+ price: product.price
85
+ }
86
+
87
+ return (
88
+ <ItemVariantProvider
89
+ key={product.productId}
90
+ index={idx}
91
+ variant={variant}
92
+ >
93
+ <Flex width="full" alignItems="flex-start">
94
+ <CartItemVariantImage width="80px" mr={2} />
95
+ <Stack spacing={1} marginTop="-3px" flex={1}>
96
+ <CartItemVariantName />
97
+ <Flex
98
+ width="full"
99
+ justifyContent="space-between"
100
+ alignItems="flex-end"
101
+ >
102
+ <CartItemVariantAttributes
103
+ includeQuantity
104
+ />
105
+ <CartItemVariantPrice
106
+ currency={currency}
107
+ />
108
+ </Flex>
109
+ </Stack>
110
+ </Flex>
111
+ </ItemVariantProvider>
112
+ )
113
+ })}
114
+ </Stack>
115
+ </Box>
116
+ )
117
+ })}
118
+ </Stack>
119
+ </Box>
120
+ )
121
+ }
122
+
123
+ return (
124
+ <Stack spacing={6} width="full">
125
+ {renderItemGroup(pickupShipments, messages.pickupItems)}
126
+ {renderItemGroup(deliveryShipments, messages.deliveryItems)}
127
+ </Stack>
128
+ )
129
+ }
130
+
131
+ MultiShipOrderSummary.propTypes = {
132
+ order: PropTypes.object.isRequired,
133
+ productItemsMap: PropTypes.object.isRequired,
134
+ currency: PropTypes.string.isRequired
135
+ }
136
+
137
+ export default MultiShipOrderSummary