Skip to content

Commit 3e20e6c

Browse files
authored
@W-17442945 BOPIS Shop the Store PLP and PDP SteelThread (#2226)
* initial PLP implementation * initial implementation PDP * check inventoryId is in list of productInventories for checking inStock * move StoreLocatorProvider to app level to ensure selectedStore is saved in the context * update PDP StoreAvailabilityText to display Out of Stock when selectedStore is missing inventoryId
1 parent bc83836 commit 3e20e6c

File tree

25 files changed

+756
-154
lines changed

25 files changed

+756
-154
lines changed

packages/template-retail-react-app/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## v6.0.0
2+
- [BOPIS] Added text indicating the product's stock status at the selected store on the PDP and filtered products based on the selected store on the PLP. [#2226](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2226)
23
- DNT Consent Banner: [#2203](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2203)
34

45
## v5.1.0-dev (TBD)

packages/template-retail-react-app/app/components/_app/index.jsx

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ import {DrawerMenu} from '@salesforce/retail-react-app/app/components/drawer-men
4646
import {ListMenu, ListMenuContent} from '@salesforce/retail-react-app/app/components/list-menu'
4747
import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive'
4848
import AboveHeader from '@salesforce/retail-react-app/app/components/_app/partials/above-header'
49-
import StoreLocatorModal from '@salesforce/retail-react-app/app/components/store-locator-modal'
49+
import StoreLocatorModal, {
50+
StoreLocatorProvider
51+
} from '@salesforce/retail-react-app/app/components/store-locator-modal'
52+
5053
// Hooks
5154
import {AuthModal, useAuthModal} from '@salesforce/retail-react-app/app/hooks/use-auth-modal'
5255
import {
@@ -354,10 +357,6 @@ const App = (props) => {
354357

355358
<Box id="app" display="flex" flexDirection="column" flex={1}>
356359
<SkipNavLink zIndex="skipLink">Skip to Content</SkipNavLink>
357-
<StoreLocatorModal
358-
isOpen={isOpenStoreLocator}
359-
onClose={onCloseStoreLocator}
360-
/>
361360
<Box {...styles.headerWrapper}>
362361
{!isCheckout ? (
363362
<>
@@ -402,32 +401,38 @@ const App = (props) => {
402401
</Box>
403402
{!isOnline && <OfflineBanner />}
404403
<AddToCartModalProvider>
405-
<SkipNavContent
406-
style={{
407-
display: 'flex',
408-
flexDirection: 'column',
409-
flex: 1,
410-
outline: 0
411-
}}
412-
>
413-
<Box
414-
as="main"
415-
id="app-main"
416-
role="main"
417-
display="flex"
418-
flexDirection="column"
419-
flex="1"
404+
<StoreLocatorProvider>
405+
<SkipNavContent
406+
style={{
407+
display: 'flex',
408+
flexDirection: 'column',
409+
flex: 1,
410+
outline: 0
411+
}}
420412
>
421-
<OfflineBoundary isOnline={false}>
422-
{children}
423-
</OfflineBoundary>
424-
</Box>
425-
</SkipNavContent>
426-
427-
{!isCheckout ? <Footer /> : <CheckoutFooter />}
428-
429-
<AuthModal {...authModal} />
430-
<DntNotification {...dntNotification} />
413+
<Box
414+
as="main"
415+
id="app-main"
416+
role="main"
417+
display="flex"
418+
flexDirection="column"
419+
flex="1"
420+
>
421+
<OfflineBoundary isOnline={false}>
422+
{children}
423+
</OfflineBoundary>
424+
</Box>
425+
</SkipNavContent>
426+
427+
{!isCheckout ? <Footer /> : <CheckoutFooter />}
428+
429+
<AuthModal {...authModal} />
430+
<StoreLocatorModal
431+
isOpen={isOpenStoreLocator}
432+
onClose={onCloseStoreLocator}
433+
/>
434+
<DntNotification {...dntNotification} />
435+
</StoreLocatorProvider>
431436
</AddToCartModalProvider>
432437
</Box>
433438
</CurrencyProvider>

packages/template-retail-react-app/app/components/product-view/index.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import Swatch from '@salesforce/retail-react-app/app/components/swatch-group/swa
3939
import SwatchGroup from '@salesforce/retail-react-app/app/components/swatch-group'
4040
import {getPriceData} from '@salesforce/retail-react-app/app/utils/product-utils'
4141
import PromoCallout from '@salesforce/retail-react-app/app/components/product-tile/promo-callout'
42+
import StoreAvailabilityText from '@salesforce/retail-react-app/app/components/store-availability-text'
4243

4344
const ProductViewHeader = ({
4445
name,
@@ -598,6 +599,7 @@ const ProductView = forwardRef(
598599
/>
599600
</VStack>
600601
)}
602+
<StoreAvailabilityText productInventories={product?.inventories} />
601603
<Box ref={errorContainerRef}>
602604
{!showLoading && showOptionsMessage && (
603605
<Fade in={true}>

packages/template-retail-react-app/app/components/product-view/index.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ test('ProductView Component renders properly', async () => {
5454
expect(screen.getAllByText(/Add to cart/i)).toHaveLength(2)
5555
expect(screen.getAllByRole('radiogroup')).toHaveLength(3)
5656
expect(screen.getAllByText(/add to cart/i)).toHaveLength(2)
57+
expect(screen.getByText(/In Stock at/i)).toBeInTheDocument()
58+
expect(screen.getByText(/Select Store/i)).toBeInTheDocument()
5759
})
5860

5961
test('ProductView Component renders with addToCart event handler', async () => {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) 2024, Salesforce, 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, {useContext} from 'react'
8+
import PropTypes from 'prop-types'
9+
import {FormattedMessage} from 'react-intl'
10+
11+
// Components
12+
import {Box, Link} from '@salesforce/retail-react-app/app/components/shared/ui'
13+
14+
// Others
15+
import {StoreLocatorContext} from '@salesforce/retail-react-app/app/components/store-locator-modal'
16+
17+
const StoreAvailabilityText = ({productInventories}) => {
18+
const {selectedStore, isStoreSelected} = useContext(StoreLocatorContext)
19+
const inStock =
20+
selectedStore.inventoryId &&
21+
Boolean(
22+
productInventories?.find(
23+
(inventory) => inventory.id === selectedStore.inventoryId && inventory.orderable
24+
)
25+
)
26+
27+
return (
28+
<Box gap={1} fontWeight={400} display="flex">
29+
{isStoreSelected ? (
30+
<>
31+
{inStock ? (
32+
<FormattedMessage
33+
id={'product_view.store_availability.in_stock_at'}
34+
defaultMessage={'In Stock at'}
35+
/>
36+
) : (
37+
<FormattedMessage
38+
id={'product_view.store_availability.out_of_stock_at'}
39+
defaultMessage={'Out of Stock at'}
40+
/>
41+
)}
42+
43+
<Link>{selectedStore.name}</Link>
44+
</>
45+
) : (
46+
<>
47+
<FormattedMessage
48+
id={'product_view.store_availability.in_stock_at'}
49+
defaultMessage={'In Stock at'}
50+
/>
51+
<Link>
52+
<FormattedMessage
53+
id={'product_view.link.select_store'}
54+
defaultMessage={'Select Store'}
55+
/>
56+
</Link>
57+
</>
58+
)}
59+
</Box>
60+
)
61+
}
62+
63+
StoreAvailabilityText.propTypes = {
64+
productInventories: PropTypes.arrayOf(PropTypes.object)
65+
}
66+
67+
export default StoreAvailabilityText
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright (c) 2024, Salesforce, 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 {screen} from '@testing-library/react'
9+
import PropTypes from 'prop-types'
10+
11+
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
12+
import StoreAvailabilityText from '@salesforce/retail-react-app/app/components/store-availability-text'
13+
import {StoreLocatorContext} from '@salesforce/retail-react-app/app/components/store-locator-modal'
14+
15+
const selectedStore = {
16+
name: 'Test Store',
17+
id: 'test_store',
18+
inventoryId: 'inventory_1'
19+
}
20+
21+
const MockComponent = ({selectedStore, isStoreSelected = true, productInventories}) => {
22+
return (
23+
<StoreLocatorContext.Provider value={{selectedStore, isStoreSelected}}>
24+
<StoreAvailabilityText productInventories={productInventories} />
25+
</StoreLocatorContext.Provider>
26+
)
27+
}
28+
MockComponent.propTypes = {
29+
selectedStore: PropTypes.object,
30+
isStoreSelected: PropTypes.bool,
31+
productInventories: PropTypes.array
32+
}
33+
34+
describe('StoreAvailability', () => {
35+
describe('store is not selected', () => {
36+
test('renders "In Stock at Select Store"', () => {
37+
renderWithProviders(<MockComponent selectedStore={{}} isStoreSelected={false} />)
38+
expect(screen.getByText(/In Stock at/i)).toBeInTheDocument()
39+
expect(screen.getByText(/Select Store/i)).toBeInTheDocument()
40+
})
41+
})
42+
43+
describe('store is selected', () => {
44+
test('renders "Out of Stock at Test Store" when productInventories is undefined', () => {
45+
renderWithProviders(<MockComponent selectedStore={selectedStore} />)
46+
expect(screen.getByText(/Out of Stock at/i)).toBeInTheDocument()
47+
expect(screen.getByText(selectedStore.name)).toBeInTheDocument()
48+
})
49+
50+
test('renders "Out of Stock at Test Store" when selectedStore does not have an inventoryId', () => {
51+
const storeWithNoInventoryId = {...selectedStore, inventoryId: null}
52+
renderWithProviders(<MockComponent selectedStore={storeWithNoInventoryId} />)
53+
expect(screen.getByText(/Out of Stock at/i)).toBeInTheDocument()
54+
expect(screen.getByText(selectedStore.name)).toBeInTheDocument()
55+
})
56+
57+
test('renders "Out of Stock at Test Store" if selectedStore inventoryId is not in productInventories', () => {
58+
const productInventories = [{id: 'inventory_99', orderable: true}]
59+
renderWithProviders(
60+
<MockComponent
61+
selectedStore={selectedStore}
62+
productInventories={productInventories}
63+
/>
64+
)
65+
expect(screen.getByText(/Out of Stock at/i)).toBeInTheDocument()
66+
expect(screen.getByText(selectedStore.name)).toBeInTheDocument()
67+
})
68+
69+
test('renders "Out of Stock at Test Store" if orderable is false', () => {
70+
const productInventories = [{id: 'inventory_1', orderable: false}]
71+
renderWithProviders(
72+
<MockComponent
73+
selectedStore={selectedStore}
74+
productInventories={productInventories}
75+
/>
76+
)
77+
expect(screen.getByText(/Out of Stock at/i)).toBeInTheDocument()
78+
expect(screen.getByText(selectedStore.name)).toBeInTheDocument()
79+
})
80+
81+
test('renders "In Stock at Test Store" if orderable is true', () => {
82+
const productInventories = [{id: 'inventory_1', orderable: true}]
83+
renderWithProviders(
84+
<MockComponent
85+
selectedStore={selectedStore}
86+
productInventories={productInventories}
87+
/>
88+
)
89+
expect(screen.getByText(/In Stock at/i)).toBeInTheDocument()
90+
expect(screen.getByText(selectedStore.name)).toBeInTheDocument()
91+
})
92+
})
93+
})

0 commit comments

Comments
 (0)