Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@W-15654904Showing strikethrough price on PLP #1760

Merged
merged 45 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f66b61a
strikethrough for PLP
alexvuong Apr 24, 2024
50b3bce
add param for getProducts in useEinstein
alexvuong Apr 24, 2024
249785e
PR feedback
alexvuong Apr 25, 2024
ceddadf
Refactoring getDisplayPrice
alexvuong Apr 25, 2024
56f0736
remove unused variables
alexvuong Apr 25, 2024
ae1da47
remove unvalid param
alexvuong Apr 25, 2024
5fc66c8
take closest tiered price into account
alexvuong Apr 26, 2024
b2bbabb
fix test
alexvuong Apr 26, 2024
87399dc
put back recommendation section
alexvuong Apr 26, 2024
4672407
changing label
alexvuong Apr 26, 2024
2ae213c
fix bundle size
alexvuong Apr 29, 2024
67892d7
fix tierPrices filter logic
alexvuong Apr 29, 2024
74df5ab
fix tierPrices filter logic
alexvuong Apr 29, 2024
3c62c54
fix tierPrices filter logic
alexvuong Apr 29, 2024
5025ed2
fix tierPrices filter logic
alexvuong Apr 29, 2024
1753373
fix lint
alexvuong Apr 29, 2024
913110c
add more cases for display price util
alexvuong Apr 29, 2024
5392f7e
add more cases for display price util
alexvuong Apr 29, 2024
809ca51
fix logic in getDisplayPrice
alexvuong Apr 29, 2024
90bd3d0
PR feedback
alexvuong Apr 29, 2024
906b521
PR feedback
alexvuong Apr 30, 2024
7a36794
fix tests
alexvuong Apr 30, 2024
87fbc8b
Apply formattedmessage to display price, refactor price info logic in…
alexvuong May 9, 2024
cf31976
remove console
alexvuong May 9, 2024
2afea83
guard the values
alexvuong May 9, 2024
165c2db
fix tests
alexvuong May 9, 2024
cf51887
adjust price display
alexvuong May 10, 2024
7600f95
fix tests
alexvuong May 10, 2024
e4cf009
replace formatted string with jsx logic
alexvuong May 13, 2024
04a4ce8
fix tests naming
alexvuong May 13, 2024
9045fa4
fiz translations
alexvuong May 13, 2024
9f2150f
Pr feedback
alexvuong May 13, 2024
47552a7
fix template
alexvuong May 13, 2024
9882e1a
tweak proptype
alexvuong May 13, 2024
06f8202
let bold font for PLP
alexvuong May 14, 2024
063a7f5
optimizing display price logic
alexvuong May 14, 2024
59b33ab
edit jsx
alexvuong May 14, 2024
8dac001
PR feedback
alexvuong May 14, 2024
5137079
lint
alexvuong May 14, 2024
c866572
fix test
alexvuong May 14, 2024
8dc7e96
build translations
alexvuong May 14, 2024
d5c94ff
PR feedback
alexvuong May 15, 2024
5a9a04a
separate pricing components into its own files
alexvuong May 15, 2024
e43dccb
lint
alexvuong May 15, 2024
f872153
fix flaky test
alexvuong May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const Home = () => {
const {data: productSearchResult, isLoading} = useProductSearch({
parameters: {
refine: [`cgid=${HOME_SHOP_PRODUCTS_CATEGORY_ID}`, 'htype=master'],
perPricebook: true,
allVariationProperties: true,
limit: HOME_SHOP_PRODUCTS_LIMIT
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {useQuery} from '@tanstack/react-query'
import {
useAccessToken,
useCategory,
useCustomerBaskets,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

removed unused variable

useShopperBasketsMutation
} from '@salesforce/commerce-sdk-react'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,61 +7,84 @@

import React from 'react'
import PropTypes from 'prop-types'
import {Skeleton, Text} from '@salesforce/retail-react-app/app/components/shared/ui'
import {Box, Text} from '@salesforce/retail-react-app/app/components/shared/ui'
import {useIntl} from 'react-intl'
import {useCurrency} from '@salesforce/retail-react-app/app/hooks'

const DisplayPrice = ({
basePrice,
discountPrice,
isProductASet = false,
strikethroughPrice,
bendvc marked this conversation as resolved.
Show resolved Hide resolved
currentPrice,
prefixLabel,
currency,
discountPriceProps,
basePriceProps,
skeletonProps
currentPriceProps,
strikethroughPriceProps
}) => {
const intl = useIntl()
const {currency: activeCurrency} = useCurrency()
const currentPriceText = intl.formatNumber(currentPrice, {
alexvuong marked this conversation as resolved.
Show resolved Hide resolved
style: 'currency',
currency: currency || activeCurrency
})
const strikethroughPriceText =
strikethroughPrice &&
intl.formatNumber(strikethroughPrice, {
style: 'currency',
currency: currency || activeCurrency
})
return (
<Skeleton isLoaded={basePrice} display={'flex'} {...skeletonProps}>
<Text fontWeight="bold" fontSize="md" mr={1}>
{isProductASet &&
`${intl.formatMessage({
id: 'product_view.label.starting_at_price',
defaultMessage: 'Starting at'
})} `}
</Text>
{typeof discountPrice === 'number' && (
<Text as="b" {...discountPriceProps}>
{intl.formatNumber(discountPrice, {
style: 'currency',
currency: currency || activeCurrency
})}
<Box display={'flex'}>
{prefixLabel && (
<Text fontWeight="bold" fontSize="md" mr={1}>
{prefixLabel}
</Text>
)}
<Text
as={typeof discountPrice === 'number' ? 's' : 'b'}
ml={typeof discountPrice === 'number' ? 2 : 0}
fontWeight={discountPrice ? 'normal' : 'bold'}
{...basePriceProps}
as="b"
aria-label={intl.formatMessage(
{
id: 'product_tile.assistive_msg.sale_price',
defaultMessage: 'current price {currentPrice}'
},
{
currentPrice: currentPriceText
}
)}
{...currentPriceProps}
>
{intl.formatNumber(basePrice, {
style: 'currency',
currency: currency || activeCurrency
})}
{currentPriceText}
</Text>
</Skeleton>
{/*Allowing display price of 0*/}
{typeof strikethroughPrice === 'number' && (
<Text
color="gray.600"
aria-label={intl.formatMessage(
{
id: 'product_tile.assistive_msg.original_price',
defaultMessage: 'strikethrough price {strikethroughPrice}'
},
{
strikethroughPrice: strikethroughPriceText
}
)}
as={typeof currentPrice === 'number' ? 's' : 'b'}
ml={typeof currentPrice === 'number' ? 2 : 0}
fontWeight={typeof currentPrice === 'number' ? 'normal' : 'bold'}
{...strikethroughPriceProps}
>
{strikethroughPriceText}
</Text>
)}
</Box>
)
}

DisplayPrice.propTypes = {
basePrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
discountPrice: PropTypes.number,
strikethroughPrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
currentPrice: PropTypes.number.isRequired,
currency: PropTypes.string,
isProductASet: PropTypes.bool,
discountPriceProps: PropTypes.object,
basePriceProps: PropTypes.object,
skeletonProps: PropTypes.object
prefixLabel: PropTypes.string,
strikethroughPriceProps: PropTypes.object,
currentPriceProps: PropTypes.object
}

export default DisplayPrice
alexvuong marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,27 @@ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-u

describe('DisplayPrice', function () {
test('should render without error', () => {
renderWithProviders(<DisplayPrice currency="GBP" basePrice={100} discountPrice={90} />)
renderWithProviders(
<DisplayPrice currency="GBP" currentPrice={90} strikethroughPrice={100} />
)
expect(screen.getByText(/£90\.00/i)).toBeInTheDocument()
expect(screen.getByText(/£100\.00/i)).toBeInTheDocument()
})

test('should render according html tag for prices', () => {
const {container} = renderWithProviders(
<DisplayPrice currency="GBP" basePrice={100} discountPrice={90} />
<DisplayPrice currency="GBP" currentPrice={90} strikethroughPrice={100} />
)
const discountPriceTag = container.querySelectorAll('b')
const basePriceTag = container.querySelectorAll('s')
expect(within(discountPriceTag[0]).getByText(/£90\.00/i)).toBeDefined()
expect(within(basePriceTag[0]).getByText(/£100\.00/i)).toBeDefined()
expect(discountPriceTag).toHaveLength(1)
expect(basePriceTag).toHaveLength(1)
const currentPriceTag = container.querySelectorAll('b')
const strikethroughPriceTag = container.querySelectorAll('s')
expect(within(currentPriceTag[0]).getByText(/£90\.00/i)).toBeDefined()
expect(within(strikethroughPriceTag[0]).getByText(/£100\.00/i)).toBeDefined()
expect(currentPriceTag).toHaveLength(1)
expect(strikethroughPriceTag).toHaveLength(1)
})

test('should not render discount price if not available', () => {
renderWithProviders(<DisplayPrice currency="GBP" basePrice={100} />)
renderWithProviders(<DisplayPrice currency="GBP" currentPrice={100} />)
expect(screen.queryByText(/£90\.00/i)).not.toBeInTheDocument()
expect(screen.getByText(/£100\.00/i)).toBeInTheDocument()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import React, {useRef} from 'react'
import React, {useMemo, useRef} from 'react'
import PropTypes from 'prop-types'
import {HeartIcon, HeartSolidIcon} from '@salesforce/retail-react-app/app/components/icons'
import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price'
import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils'

// Components
import {
Expand All @@ -28,7 +30,6 @@ import {useIntl} from 'react-intl'
import {productUrlBuilder} from '@salesforce/retail-react-app/app/utils/url'
import Link from '@salesforce/retail-react-app/app/components/link'
import withRegistration from '@salesforce/retail-react-app/app/components/with-registration'
import {useCurrency} from '@salesforce/retail-react-app/app/hooks'

const IconButtonWithRegistration = withRegistration(IconButton)

Expand Down Expand Up @@ -66,18 +67,56 @@ const ProductTile = (props) => {
...rest
} = props

const {currency, image, price, productId, hitType} = product

const {image, productId, hitType} = product
// ProductTile is used by two components, RecommendedProducts and ProductList.
// RecommendedProducts provides a localized product name as `name` and non-localized product
// name as `productName`. ProductList provides a localized name as `productName` and does not
// use the `name` property.
const localizedProductName = product.name ?? product.productName

const {currency: activeCurrency} = useCurrency()
const isFavouriteLoading = useRef(false)
const styles = useMultiStyleConfig('ProductTile')

// NOTE: swatches will implement later to set variant accordingly,
// On first load, get the variant that is the represent product
// this is for variant product, standard/set/bundles does not have variants
// Also, product tile can be used in RecommendedProducts where it calls getProducts which does not have representedProduct
// in that case we use the first variant that has price book to set up discount price
// Not all variants has set in a priceBook, meaning not having tieredPrices.
const variant = useMemo(() => {
return product?.variants?.find(
(i) => i?.productId === product?.representedProduct?.id || !!i?.tieredPrices
)
}, [product])
// prioritize variant promotionalPrice over standard price
let currentPrice = variant
? getDisplayPrice(variant)?.discountPrice || variant?.price
: product?.price

// check for both data returned from getProducts and productSearch
const isProductASet = hitType === 'set' || !!product?.type?.set
const isProductABundle = hitType === 'bundle' || !!product?.type?.bundle
const isProductAStandard = hitType === 'product' || !!product?.type?.item
alexvuong marked this conversation as resolved.
Show resolved Hide resolved
let tieredPrices
if (variant) {
tieredPrices = variant?.tieredPrices
} else if (isProductABundle || isProductAStandard) {
tieredPrices = product?.tieredPrices
} else {
// if none applies, we assume this is a product set
// product sets do not have tierPieces, we go with priceRanges
tieredPrices = product?.priceRanges
}

let listPrice = useMemo(() => {
const maxPriceTier = tieredPrices
? Math.max(...(tieredPrices || []).map((item) => item.price || item.maxPrice))
: 0
return tieredPrices?.find(
(tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier
)
}, [tieredPrices])

return (
<Box {...styles.container}>
<Link
Expand Down Expand Up @@ -105,25 +144,27 @@ const ProductTile = (props) => {
<Text {...styles.title}>{localizedProductName}</Text>

{/* Price */}
<Text {...styles.price} data-testid="product-tile-price">
{hitType === 'set'
? intl.formatMessage(
{
id: 'product_tile.label.starting_at_price',
defaultMessage: 'Starting at {price}'
},
{
price: intl.formatNumber(price, {
style: 'currency',
currency: currency || activeCurrency
})
}
)
: intl.formatNumber(price, {
style: 'currency',
currency: currency || activeCurrency
})}
</Text>
<DisplayPrice
strikethroughPrice={
listPrice?.maxPrice > currentPrice || listPrice?.price > currentPrice
? listPrice?.price || listPrice?.maxPrice
: null
}
prefixLabel={
isProductASet
? intl.formatMessage({
id: 'product_view.label.starting_at_price',
defaultMessage: 'Starting at'
})
: null
}
currentPriceProps={
listPrice?.maxPrice > currentPrice || listPrice?.price > currentPrice
? {as: 'b'}
: {as: 'span'}
}
currentPrice={currentPrice}
/>
</Link>
{enableFavourite && (
<Box
Expand Down Expand Up @@ -178,12 +219,15 @@ ProductTile.propTypes = {
*/
product: PropTypes.shape({
currency: PropTypes.string,
representedProduct: PropTypes.object,
image: PropTypes.shape({
alt: PropTypes.string,
disBaseLink: PropTypes.string,
link: PropTypes.string
}),
price: PropTypes.number,
priceRanges: PropTypes.array,
tieredPrices: PropTypes.array,
// `name` is present and localized when `product` is provided by a RecommendedProducts component
// (from Shopper Products `getProducts` endpoint), but is not present when `product` is
// provided by a ProductList component.
Expand All @@ -197,7 +241,14 @@ ProductTile.propTypes = {
// Note: useEinstein() transforms snake_case property names from the API response to camelCase
productName: PropTypes.string,
productId: PropTypes.string,
hitType: PropTypes.string
hitType: PropTypes.string,
variants: PropTypes.array,
type: PropTypes.shape({
set: PropTypes.bool,

bundle: PropTypes.bool,
item: PropTypes.bool
})
}),
/**
* Enable adding/removing product as a favourite.
Expand Down