@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.
- package/CHANGELOG.md +8 -4
- package/app/components/_app/index.jsx +9 -7
- package/app/components/_app/index.test.js +2 -2
- package/app/components/_app-config/index.jsx +9 -3
- package/app/components/drawer-menu/drawer-menu.jsx +3 -1
- package/app/components/footer/index.jsx +3 -1
- package/app/components/header/index.jsx +3 -1
- package/app/components/header/index.test.js +2 -2
- package/app/components/island/README.md +1 -1
- package/app/components/island/index.jsx +3 -1
- package/app/components/island/index.test.js +94 -5
- package/app/components/item-variant/item-attributes.jsx +12 -3
- package/app/components/multiship/multiship-order-summary.jsx +137 -0
- package/app/components/multiship/multiship-order-summary.test.js +121 -0
- package/app/components/order-summary/index.jsx +2 -4
- package/app/components/pickup-or-delivery/index.jsx +80 -0
- package/app/components/pickup-or-delivery/index.test.jsx +182 -0
- package/app/components/product-item/index.jsx +26 -16
- package/app/components/product-item/index.test.js +29 -2
- package/app/components/product-item-list/index.jsx +10 -0
- package/app/components/product-item-list/index.test.jsx +14 -0
- package/app/components/product-view/index.jsx +9 -6
- package/app/components/product-view/index.test.js +25 -21
- package/app/components/quantity-picker/index.test.jsx +12 -12
- package/app/components/reset-password/index.test.js +1 -1
- package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
- package/app/components/shared/ui/index.jsx +1 -0
- package/app/components/store-display/index.jsx +28 -4
- package/app/components/store-display/index.test.js +71 -0
- package/app/components/store-locator/form.test.jsx +16 -4
- package/app/components/store-locator/list.jsx +9 -4
- package/app/components/toggle-card/index.jsx +14 -0
- package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
- package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
- package/app/constants.js +20 -6
- package/app/contexts/store-locator-provider.jsx +7 -1
- package/app/contexts/store-locator-provider.test.jsx +36 -1
- package/app/hooks/use-address-form.js +155 -0
- package/app/hooks/use-address-form.test.js +501 -0
- package/app/hooks/use-auth-modal.js +2 -6
- package/app/hooks/use-current-basket.js +71 -2
- package/app/hooks/use-current-basket.test.js +37 -1
- package/app/hooks/use-dnt-notification.js +4 -4
- package/app/hooks/use-dnt-notification.test.js +5 -5
- package/app/hooks/use-item-shipment-management.js +233 -0
- package/app/hooks/use-item-shipment-management.test.js +696 -0
- package/app/hooks/use-multiship.js +589 -0
- package/app/hooks/use-multiship.test.js +776 -0
- package/app/hooks/use-pickup-shipment.js +70 -106
- package/app/hooks/use-pickup-shipment.test.js +345 -209
- package/app/hooks/use-product-address-assignment.js +280 -0
- package/app/hooks/use-product-address-assignment.test.js +414 -0
- package/app/hooks/use-product-inventory.js +100 -0
- package/app/hooks/use-product-inventory.test.js +254 -0
- package/app/hooks/use-shipment-operations.js +168 -0
- package/app/hooks/use-shipment-operations.test.js +385 -0
- package/app/hooks/use-store-locator.js +24 -2
- package/app/hooks/use-store-locator.test.jsx +109 -1
- package/app/pages/account/index.test.js +1 -1
- package/app/pages/account/profile.test.js +0 -2
- package/app/pages/cart/index.jsx +397 -157
- package/app/pages/cart/index.test.js +353 -2
- package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
- package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
- package/app/pages/cart/partials/order-type-display.jsx +68 -0
- package/app/pages/cart/partials/order-type-display.test.js +241 -0
- package/app/pages/checkout/confirmation.jsx +79 -158
- package/app/pages/checkout/index.jsx +34 -9
- package/app/pages/checkout/index.test.js +245 -118
- package/app/pages/checkout/partials/contact-info.jsx +2 -6
- package/app/pages/checkout/partials/contact-info.test.js +93 -7
- package/app/pages/checkout/partials/payment.jsx +19 -5
- package/app/pages/checkout/partials/pickup-address.jsx +340 -70
- package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
- package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
- package/app/pages/checkout/partials/shipment-details.jsx +209 -0
- package/app/pages/checkout/partials/shipment-details.test.js +246 -0
- package/app/pages/checkout/partials/shipping-address.jsx +156 -68
- package/app/pages/checkout/partials/shipping-address.test.js +673 -0
- package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
- package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
- package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
- package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
- package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
- package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
- package/app/pages/checkout/util/checkout-context.js +25 -18
- package/app/pages/login/index.jsx +2 -6
- package/app/pages/product-detail/index.jsx +96 -81
- package/app/pages/product-detail/index.test.js +103 -19
- package/app/pages/product-list/index.jsx +3 -1
- package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
- package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
- package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
- package/app/ssr.js +1 -5
- package/app/static/translations/compiled/en-GB.json +316 -30
- package/app/static/translations/compiled/en-US.json +316 -30
- package/app/static/translations/compiled/en-XA.json +673 -75
- package/app/utils/address-utils.js +112 -0
- package/app/utils/address-utils.test.js +484 -0
- package/app/utils/product-utils.js +17 -5
- package/app/utils/product-utils.test.js +17 -8
- package/app/utils/sfdc-user-agent-utils.js +32 -0
- package/app/utils/sfdc-user-agent-utils.test.js +82 -0
- package/app/utils/shipment-utils.js +196 -0
- package/app/utils/shipment-utils.test.js +458 -0
- package/app/utils/test-utils.js +4 -4
- package/app/utils/utils.js +6 -1
- package/config/default.js +4 -1
- package/config/mocks/default.js +3 -1
- package/package.json +9 -9
- package/translations/en-GB.json +127 -10
- package/translations/en-US.json +127 -10
- package/app/pages/checkout/partials/shipping-options.jsx +0 -269
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
##
|
|
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
|
-
{
|
|
379
|
+
{storeLocatorEnabled && (
|
|
378
380
|
<StoreLocatorModal
|
|
379
|
-
isOpen={
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
{
|
|
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 (
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
74
|
-
|
|
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
|
|
124
|
-
|
|
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 = ({
|
|
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
|
-
{
|
|
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
|