Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d2e515e
Initial commit for PDP changes for pickup in Store
snilakandan13 Jun 6, 2025
17ed9cb
Merge branch 'develop' of https://github.com/SalesforceCommerceCloud/…
snilakandan13 Jun 6, 2025
983089a
Modified Pickup as a Radio Button
snilakandan13 Jun 6, 2025
51282f7
Modified Pickup as a Radio Button
snilakandan13 Jun 6, 2025
ca7d6ad
Linting errors
snilakandan13 Jun 9, 2025
23d3bb1
Fixing product bundles
snilakandan13 Jun 9, 2025
f39b47f
Reverting git ignore
snilakandan13 Jun 9, 2025
c9b5e61
Remove my-test-project from repository and add to .gitignore
snilakandan13 Jun 9, 2025
fe8e9a4
Reverting other linting files
snilakandan13 Jun 9, 2025
b8a71e9
reverting unchanged files
snilakandan13 Jun 9, 2025
5219741
Translations
snilakandan13 Jun 9, 2025
f20e9e2
Cleaning up comments
snilakandan13 Jun 9, 2025
7625fd8
Added changes to PDP page
snilakandan13 Jun 9, 2025
cbcd369
Review comments
snilakandan13 Jun 9, 2025
af96e17
Review comments
snilakandan13 Jun 9, 2025
f76d717
Added a test for ProductView
snilakandan13 Jun 9, 2025
392cff0
Review comments
snilakandan13 Jun 10, 2025
66e7c9c
Review comments
snilakandan13 Jun 10, 2025
b98aaf9
Merge branch 'feature/shop-in-store' into t/commerce/W-18669904/pdpCh…
snilakandan13 Jun 10, 2025
41161eb
linting error fix
snilakandan13 Jun 10, 2025
e972465
Merge branch 't/commerce/W-18669904/pdpChangesForPickupInStore' of ht…
snilakandan13 Jun 10, 2025
5e354cb
Review comment
snilakandan13 Jun 10, 2025
4094418
linting issue
snilakandan13 Jun 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/template-retail-react-app/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
- Improved the layout of product tiles in product scroll and product list [#2446](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2446)
- Updated 6 new languagues [#2495](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2495)
- Show Bonus Product Label on OrderSummary component [#2524](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2524)
- Added support for Shop in Store Functionality
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no PR link?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is just the group heading, the PR link is in line #7, the shopInStore is going to have multiple Prs, the first one is the one to the PDP page

- Added support for PDP page to support Pickup in Store[#2537] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2537)
- Show Bonus Product Label on OrderSummary component [#2524](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2524)


## v6.1.0 (May 22, 2025)

Expand Down
234 changes: 223 additions & 11 deletions packages/template-retail-react-app/app/components/product-view/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ import {
Text,
VStack,
Fade,
useTheme
useTheme,
Stack,
Radio,
RadioGroup
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the hook at the bottom, and arrange the components in alphabetical order for readability

} from '@salesforce/retail-react-app/app/components/shared/ui'
import {useCurrency, useDerivedProduct} from '@salesforce/retail-react-app/app/hooks'
import {useAddToCartModalContext} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'
import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'

// project components
import ImageGallery from '@salesforce/retail-react-app/app/components/image-gallery'
Expand Down Expand Up @@ -118,7 +122,9 @@ const ProductView = forwardRef(
!isProductLoading && variant?.orderable && quantity > 0 && quantity <= stockLevel,
showImageGallery = true,
setSelectedBundleQuantity = () => {},
selectedBundleParentQuantity = 1
selectedBundleParentQuantity = 1,
pickupInStore,
setPickupInStore
},
ref
) => {
Expand Down Expand Up @@ -155,6 +161,30 @@ const ProductView = forwardRef(
const isProductASet = product?.type.set
const isProductABundle = product?.type.bundle
const errorContainerRef = useRef(null)
const [pickupEnabled, setPickupEnabled] = useState(false)
const [pickupError, setPickupError] = useState('')
const {site} = useMultiSite()

// Get store info from localStorage for stock status display (move to main render scope)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of logic is added into this component. Can we create a hook or components for the entire "delivery" section ?

Copy link
Contributor Author

@snilakandan13 snilakandan13 Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole section is going to be modified based on the changes to the hook for "useDerivedProduct" which Yuming is going on currently, this is more of a placeHolder until then

With that the "useDerivedProduct" will return the storeStockStatus

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again all this goes to the feature branch, with the caveat that future WIs/stories incrementally are present to encapsulate code better

let storeName = null
let inventoryId = null
let storeStockStatus = null
if (typeof window !== 'undefined' && site?.id && product?.inventories) {
const storeInfoKey = `store_${site.id}`
try {
const storeInfo = JSON.parse(window.localStorage.getItem(storeInfoKey))
inventoryId = storeInfo?.inventoryId
storeName = storeInfo?.name
if (inventoryId) {
const inventoryObj = product.inventories.find((inv) => inv.id === inventoryId)
if (inventoryObj) {
storeStockStatus = inventoryObj.orderable
}
}
} catch (e) {
// intentionally empty: ignore errors
}
}

const {disableButton, customInventoryMessage} = useMemo(() => {
let shouldDisableButton = showInventoryMessage
Expand Down Expand Up @@ -272,14 +302,17 @@ const ProductView = forwardRef(
return
}
try {
const itemsAdded = await addToCart(variant, quantity)
const itemsAdded = await addToCart([{variant, quantity}])
// Open modal only when `addToCart` returns some data
// It's possible that the item has been added to cart, but we don't want to open the modal.
// See wishlist_primary_action for example.
if (itemsAdded) {
onAddToCartModalOpen({
product,
itemsAdded,
itemsAdded: itemsAdded.map((item) => ({
...item,
product // attach the full product object
})),
selectedQuantity: quantity
})
}
Expand Down Expand Up @@ -399,6 +432,65 @@ const ProductView = forwardRef(
}
}, [showInventoryMessage, inventoryMessage])

useEffect(() => {
if (site?.id) {
const storeInfoKey = `store_${site.id}`
let inventoryId = null
try {
const storeInfo = JSON.parse(window.localStorage.getItem(storeInfoKey))
inventoryId = storeInfo?.inventoryId
} catch (e) {
// intentionally empty: ignore errors
}
setPickupEnabled(!!inventoryId)
}
}, [site?.id])

const showError = (error) => {
showToast({
title: error?.message || 'An error occurred',
status: 'error'
})
}

// Refactored handler for delivery option change
const handleDeliveryOptionChange = (value) => {
setPickupError('')
if (value === 'pickup') {
if (pickupEnabled) {
const storeInfoKey = `store_${site.id}`
let inventoryId = null
let storeName = null
try {
const storeInfo = JSON.parse(window.localStorage.getItem(storeInfoKey))
inventoryId = storeInfo?.inventoryId
storeName = storeInfo?.name
} catch (e) {
showError()
}
if (inventoryId && product?.inventories) {
const inventoryObj = product.inventories.find(
(inv) => inv.id === inventoryId
)
if (!inventoryObj?.orderable) {
setPickupInStore(false)
setPickupError(
intl.formatMessage(
{
id: 'product_view.error.not_available_for_pickup',
defaultMessage: 'Out of Stock in {storeName}'
},
{storeName: storeName || ''}
)
)
return
}
}
}
}
setPickupInStore(value === 'pickup')
}

return (
<Flex direction={'column'} data-testid="product-view" ref={ref}>
{/* Basic information etc. title, price, breadcrumb*/}
Expand Down Expand Up @@ -641,12 +733,130 @@ const ProductView = forwardRef(
</Text>
</Fade>
)}
<Box
display={
isProductPartOfSet ? 'block' : ['none', 'none', 'none', 'block']
}
>
{renderActionButtons()}
<Box>
<Box mb={1}>
<Text as="label" fontWeight="bold" mb={1} display="block">
<FormattedMessage
defaultMessage="Delivery:"
id="product_view.label.delivery"
/>
</Text>
<RadioGroup
value={pickupInStore ? 'pickup' : 'ship'}
onChange={handleDeliveryOptionChange}
mb={1}
>
<Stack direction="column" spacing={2}>
<Radio value="ship">
<FormattedMessage
defaultMessage="Ship to Address"
id="product_view.label.ship_to_address"
/>
</Radio>
<Radio
value="pickup"
isDisabled={
!pickupEnabled ||
(storeName &&
inventoryId &&
storeStockStatus === false)
}
>
<FormattedMessage
defaultMessage="Pickup in Store"
id="product_view.label.pickup_in_store"
/>
</Radio>
</Stack>
</RadioGroup>
</Box>
{storeName && inventoryId && (
<Text
color="black"
fontWeight={600}
mb={2}
data-testid="store-stock-status-msg"
>
{storeStockStatus
? intl.formatMessage(
{
id: 'product_view.status.in_stock_at_store',
defaultMessage: 'In Stock at {storeName}'
},
{
storeName: (
<Link
to="/store-locator"
color="blue.600"
textDecoration="underline"
>
{storeName}
</Link>
)
}
)
: intl.formatMessage(
{
id: 'product_view.status.out_of_stock_at_store',
defaultMessage: 'Out of Stock at {storeName}'
},
{
storeName: (
<Link
to="/store-locator"
color="blue.600"
textDecoration="underline"
>
{storeName}
</Link>
)
}
)}
</Text>
)}
{pickupError && (
<Text
color="orange.600"
fontWeight={600}
mb={3}
data-testid="pickup-error-msg"
>
{pickupError}
</Text>
)}
<Box
display={
isProductPartOfSet
? 'block'
: ['none', 'none', 'none', 'block']
}
>
{/* Show label if pickup is disabled due to no store/inventoryId */}
{!pickupEnabled && (
<Text
color="black"
fontWeight={600}
mb={3}
data-testid="pickup-select-store-msg"
>
<FormattedMessage
defaultMessage="Pickup in "
id="product_view.label.pickup_in_select_store_prefix"
/>{' '}
<Link
to="/store-locator"
color="blue.600"
textDecoration="underline"
>
<FormattedMessage
defaultMessage="Select Store"
id="product_view.label.select_store_link"
/>
</Link>
</Text>
)}
{renderActionButtons()}
</Box>
</Box>
</Box>
</VStack>
Expand Down Expand Up @@ -698,7 +908,9 @@ ProductView.propTypes = {
validateOrderability: PropTypes.func,
showImageGallery: PropTypes.bool,
setSelectedBundleQuantity: PropTypes.func,
selectedBundleParentQuantity: PropTypes.number
selectedBundleParentQuantity: PropTypes.number,
pickupInStore: PropTypes.bool,
setPickupInStore: PropTypes.func
}

export default ProductView
Loading
Loading