@@ -19,10 +19,20 @@ import {
1919 Text ,
2020 VStack ,
2121 Fade ,
22+ Stack ,
23+ Radio ,
24+ RadioGroup ,
2225 useTheme
2326} from '@salesforce/retail-react-app/app/components/shared/ui'
27+
28+ // Constants
29+ const DELIVERY_OPTIONS = {
30+ SHIP : 'ship' ,
31+ PICKUP : 'pickup'
32+ }
2433import { useCurrency , useDerivedProduct } from '@salesforce/retail-react-app/app/hooks'
2534import { useAddToCartModalContext } from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'
35+ import { STORE_LOCATOR_IS_ENABLED } from '@salesforce/retail-react-app/app/constants'
2636
2737// project components
2838import ImageGallery from '@salesforce/retail-react-app/app/components/image-gallery'
@@ -84,7 +94,8 @@ ProductViewHeader.propTypes = {
8494 category : PropTypes . array ,
8595 priceData : PropTypes . object ,
8696 product : PropTypes . object ,
87- isProductPartOfBundle : PropTypes . bool
97+ isProductPartOfBundle : PropTypes . bool ,
98+ showDeliveryOptions : PropTypes . bool
8899}
89100
90101const ButtonWithRegistration = withRegistration ( Button )
@@ -118,7 +129,11 @@ const ProductView = forwardRef(
118129 ! isProductLoading && variant ?. orderable && quantity > 0 && quantity <= stockLevel ,
119130 showImageGallery = true ,
120131 setSelectedBundleQuantity = ( ) => { } ,
121- selectedBundleParentQuantity = 1
132+ selectedBundleParentQuantity = 1 ,
133+ pickupInStore = false ,
134+ setPickupInStore = ( ) => { } ,
135+ onOpenStoreLocator = ( ) => { } ,
136+ showDeliveryOptions = true
122137 } ,
123138 ref
124139 ) => {
@@ -146,7 +161,9 @@ const ProductView = forwardRef(
146161 stockLevel,
147162 stepQuantity,
148163 isOutOfStock,
149- unfulfillable
164+ unfulfillable,
165+ isSelectedStoreOutOfStock,
166+ selectedStore
150167 } = useDerivedProduct ( product , isProductPartOfSet , isProductPartOfBundle )
151168 const priceData = useMemo ( ( ) => {
152169 return getPriceData ( product , { quantity} )
@@ -155,6 +172,9 @@ const ProductView = forwardRef(
155172 const isProductASet = product ?. type . set
156173 const isProductABundle = product ?. type . bundle
157174 const errorContainerRef = useRef ( null )
175+ const [ pickupEnabled , setPickupEnabled ] = useState ( false )
176+ const storeName = selectedStore ?. name
177+ const inventoryId = selectedStore ?. inventoryId
158178
159179 const { disableButton, customInventoryMessage} = useMemo ( ( ) => {
160180 let shouldDisableButton = showInventoryMessage
@@ -272,14 +292,17 @@ const ProductView = forwardRef(
272292 return
273293 }
274294 try {
275- const itemsAdded = await addToCart ( variant , quantity )
295+ const itemsAdded = await addToCart ( [ { variant, quantity} ] )
276296 // Open modal only when `addToCart` returns some data
277297 // It's possible that the item has been added to cart, but we don't want to open the modal.
278298 // See wishlist_primary_action for example.
279299 if ( itemsAdded ) {
280300 onAddToCartModalOpen ( {
281301 product,
282- itemsAdded,
302+ itemsAdded : itemsAdded . map ( ( item ) => ( {
303+ ...item ,
304+ product // attach the full product object
305+ } ) ) ,
283306 selectedQuantity : quantity
284307 } )
285308 }
@@ -399,6 +422,18 @@ const ProductView = forwardRef(
399422 }
400423 } , [ showInventoryMessage , inventoryMessage ] )
401424
425+ // Auto-switch off pickup in store when product becomes unavailable at selected store
426+ useEffect ( ( ) => {
427+ setPickupEnabled ( ! ! selectedStore ?. inventoryId )
428+ if ( pickupInStore && isSelectedStoreOutOfStock ) {
429+ setPickupInStore ( false )
430+ }
431+ } , [ selectedStore ] )
432+
433+ const handleDeliveryOptionChange = ( value ) => {
434+ setPickupInStore ( value === DELIVERY_OPTIONS . PICKUP )
435+ }
436+
402437 return (
403438 < Flex direction = { 'column' } data-testid = "product-view" ref = { ref } >
404439 { /* Basic information etc. title, price, breadcrumb*/ }
@@ -641,12 +676,148 @@ const ProductView = forwardRef(
641676 </ Text >
642677 </ Fade >
643678 ) }
644- < Box
645- display = {
646- isProductPartOfSet ? 'block' : [ 'none' , 'none' , 'none' , 'block' ]
647- }
648- >
649- { renderActionButtons ( ) }
679+ < Box >
680+ { showDeliveryOptions && (
681+ < >
682+ < Box mb = { 4 } >
683+ < Text fontWeight = { 600 } mb = { 3 } >
684+ < FormattedMessage
685+ defaultMessage = "Delivery:"
686+ id = "product_view.label.delivery"
687+ />
688+ </ Text >
689+ < RadioGroup
690+ value = {
691+ pickupInStore
692+ ? DELIVERY_OPTIONS . PICKUP
693+ : DELIVERY_OPTIONS . SHIP
694+ }
695+ onChange = { handleDeliveryOptionChange }
696+ mb = { 1 }
697+ >
698+ < Stack direction = "column" spacing = { 2 } >
699+ < Radio
700+ value = { DELIVERY_OPTIONS . SHIP }
701+ isDisabled = { disableButton }
702+ >
703+ < FormattedMessage
704+ defaultMessage = "Ship to Address"
705+ id = "product_view.label.ship_to_address"
706+ />
707+ </ Radio >
708+ { STORE_LOCATOR_IS_ENABLED && (
709+ < Radio
710+ value = { DELIVERY_OPTIONS . PICKUP }
711+ isDisabled = {
712+ ! pickupEnabled ||
713+ ( storeName &&
714+ inventoryId &&
715+ isSelectedStoreOutOfStock )
716+ }
717+ >
718+ < FormattedMessage
719+ defaultMessage = "Pickup in Store"
720+ id = "product_view.label.pickup_in_store"
721+ />
722+ </ Radio >
723+ ) }
724+ </ Stack >
725+ </ RadioGroup >
726+ </ Box >
727+
728+ { STORE_LOCATOR_IS_ENABLED && (
729+ < >
730+ { storeName && inventoryId && (
731+ < Text
732+ color = "black"
733+ fontWeight = { 600 }
734+ mb = { 2 }
735+ data-testid = "store-stock-status-msg"
736+ >
737+ { ! isSelectedStoreOutOfStock
738+ ? intl . formatMessage (
739+ {
740+ id : 'product_view.status.in_stock_at_store' ,
741+ defaultMessage :
742+ 'In Stock at {storeName}'
743+ } ,
744+ {
745+ storeName : (
746+ < Link
747+ as = "button"
748+ color = "blue.600"
749+ textDecoration = "underline"
750+ onClick = {
751+ onOpenStoreLocator
752+ }
753+ >
754+ { storeName }
755+ </ Link >
756+ )
757+ }
758+ )
759+ : intl . formatMessage (
760+ {
761+ id : 'product_view.status.out_of_stock_at_store' ,
762+ defaultMessage :
763+ 'Out of Stock at {storeName}'
764+ } ,
765+ {
766+ storeName : (
767+ < Link
768+ as = "button"
769+ color = "blue.600"
770+ textDecoration = "underline"
771+ onClick = {
772+ onOpenStoreLocator
773+ }
774+ >
775+ { storeName }
776+ </ Link >
777+ )
778+ }
779+ ) }
780+ </ Text >
781+ ) }
782+
783+ { /* Show label if pickup is disabled due to no store/inventoryId */ }
784+ { ! pickupEnabled && ! storeName && ! inventoryId && (
785+ < Text
786+ color = "black"
787+ fontWeight = { 600 }
788+ mb = { 3 }
789+ data-testid = "pickup-select-store-msg"
790+ >
791+ < FormattedMessage
792+ defaultMessage = "Pickup in "
793+ id = "product_view.label.pickup_in_select_store_prefix"
794+ /> { ' ' }
795+ < Link
796+ as = "button"
797+ color = "blue.600"
798+ textDecoration = "underline"
799+ onClick = { onOpenStoreLocator }
800+ >
801+ < FormattedMessage
802+ defaultMessage = "Select Store"
803+ id = "product_view.label.select_store_link"
804+ />
805+ </ Link >
806+ </ Text >
807+ ) }
808+ </ >
809+ ) }
810+ </ >
811+ ) }
812+ < Box
813+ display = {
814+ isProductPartOfSet
815+ ? 'block'
816+ : [ 'none' , 'none' , 'none' , 'block' ]
817+ }
818+ >
819+ { renderActionButtons ( ) }
820+ </ Box >
650821 </ Box >
651822 </ Box >
652823 </ VStack >
@@ -698,7 +869,11 @@ ProductView.propTypes = {
698869 validateOrderability : PropTypes . func ,
699870 showImageGallery : PropTypes . bool ,
700871 setSelectedBundleQuantity : PropTypes . func ,
701- selectedBundleParentQuantity : PropTypes . number
872+ selectedBundleParentQuantity : PropTypes . number ,
873+ pickupInStore : PropTypes . bool ,
874+ setPickupInStore : PropTypes . func ,
875+ onOpenStoreLocator : PropTypes . func ,
876+ showDeliveryOptions : PropTypes . bool
702877}
703878
704879export default ProductView
0 commit comments