@salesforce/retail-react-app 2.0.0 → 2.1.0-nightly-20230927165653

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 CHANGED
@@ -1,3 +1,8 @@
1
+ ## v2.1.0-dev (Sep 26, 2023)
2
+ - Support Storefront Preview
3
+ - [#1413](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1413)
4
+ - [#1440](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1440)
5
+
1
6
  ## v2.0.0 (Sep 21, 2023)
2
7
 
3
8
  - V3 Fix checkout card number [#1424](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1424)
@@ -8,6 +8,7 @@
8
8
  import React, {useState, useEffect} from 'react'
9
9
  import PropTypes from 'prop-types'
10
10
  import {useHistory, useLocation} from 'react-router-dom'
11
+ import StorefrontPreview from '@salesforce/pwa-kit-react-sdk/storefront-preview'
11
12
  import {getAssetUrl} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils'
12
13
  import {getAppOrigin} from '@salesforce/pwa-kit-react-sdk/utils/url'
13
14
  import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
@@ -112,7 +113,7 @@ const App = (props) => {
112
113
  const {children} = props
113
114
  const {data: categoriesTree} = useLazyLoadCategories()
114
115
  const categories = flatten(categoriesTree || {}, 'categories')
115
-
116
+ const {getTokenWhenReady} = useAccessToken()
116
117
  const appOrigin = getAppOrigin()
117
118
 
118
119
  const history = useHistory()
@@ -267,7 +268,6 @@ const App = (props) => {
267
268
  const path = buildUrl('/account/wishlist')
268
269
  history.push(path)
269
270
  }
270
-
271
271
  return (
272
272
  <Box className="sf-app" {...styles.container}>
273
273
  <IntlProvider
@@ -294,6 +294,7 @@ const App = (props) => {
294
294
  defaultLocale={DEFAULT_LOCALE}
295
295
  >
296
296
  <CurrencyProvider currency={currency}>
297
+ <StorefrontPreview getToken={getTokenWhenReady} />
297
298
  <Seo>
298
299
  <meta name="theme-color" content={THEME_COLOR} />
299
300
  <meta name="apple-mobile-web-app-title" content={DEFAULT_SITE_TITLE} />
@@ -0,0 +1,67 @@
1
+ /*
2
+ * Copyright (c) 2023, 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
+
8
+ import React from 'react'
9
+ import PropTypes from 'prop-types'
10
+ import {Skeleton, Text} from '@salesforce/retail-react-app/app/components/shared/ui'
11
+ import {useIntl} from 'react-intl'
12
+ import {useCurrency} from '@salesforce/retail-react-app/app/hooks'
13
+
14
+ const DisplayPrice = ({
15
+ basePrice,
16
+ discountPrice,
17
+ isProductASet = false,
18
+ currency,
19
+ discountPriceProps,
20
+ basePriceProps,
21
+ skeletonProps
22
+ }) => {
23
+ const intl = useIntl()
24
+ const {currency: activeCurrency} = useCurrency()
25
+ return (
26
+ <Skeleton isLoaded={basePrice} display={'flex'} {...skeletonProps}>
27
+ <Text fontWeight="bold" fontSize="md" mr={1}>
28
+ {isProductASet &&
29
+ `${intl.formatMessage({
30
+ id: 'product_view.label.starting_at_price',
31
+ defaultMessage: 'Starting at'
32
+ })} `}
33
+ </Text>
34
+ {typeof discountPrice === 'number' && (
35
+ <Text as="b" {...discountPriceProps}>
36
+ {intl.formatNumber(discountPrice, {
37
+ style: 'currency',
38
+ currency: currency || activeCurrency
39
+ })}
40
+ </Text>
41
+ )}
42
+ <Text
43
+ as={discountPrice > 0 ? 's' : 'b'}
44
+ ml={discountPrice > 0 ? 2 : 0}
45
+ fontWeight={discountPrice ? 'normal' : 'bold'}
46
+ {...basePriceProps}
47
+ >
48
+ {intl.formatNumber(basePrice, {
49
+ style: 'currency',
50
+ currency: currency || activeCurrency
51
+ })}
52
+ </Text>
53
+ </Skeleton>
54
+ )
55
+ }
56
+
57
+ DisplayPrice.propTypes = {
58
+ basePrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
59
+ discountPrice: PropTypes.number,
60
+ currency: PropTypes.string,
61
+ isProductASet: PropTypes.bool,
62
+ discountPriceProps: PropTypes.object,
63
+ basePriceProps: PropTypes.object,
64
+ skeletonProps: PropTypes.object
65
+ }
66
+
67
+ export default DisplayPrice
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Copyright (c) 2021, 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 {screen, within} from '@testing-library/react'
9
+ import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price/index'
10
+ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
11
+
12
+ describe('DisplayPrice', function () {
13
+ test('should render without error', () => {
14
+ renderWithProviders(<DisplayPrice currency="GBP" basePrice={100} discountPrice={90} />)
15
+ expect(screen.getByText(/£90\.00/i)).toBeInTheDocument()
16
+ expect(screen.getByText(/£100\.00/i)).toBeInTheDocument()
17
+ })
18
+
19
+ test('should render according html tag for prices', () => {
20
+ const {container} = renderWithProviders(
21
+ <DisplayPrice currency="GBP" basePrice={100} discountPrice={90} />
22
+ )
23
+ const discountPriceTag = container.querySelectorAll('b')
24
+ const basePriceTag = container.querySelectorAll('s')
25
+ expect(within(discountPriceTag[0]).getByText(/£90\.00/i)).toBeDefined()
26
+ expect(within(basePriceTag[0]).getByText(/£100\.00/i)).toBeDefined()
27
+ expect(discountPriceTag).toHaveLength(1)
28
+ expect(basePriceTag).toHaveLength(1)
29
+ })
30
+
31
+ test('should not render discount price if not available', () => {
32
+ renderWithProviders(<DisplayPrice currency="GBP" basePrice={100} />)
33
+ expect(screen.queryByText(/£90\.00/i)).not.toBeInTheDocument()
34
+ expect(screen.getByText(/£100\.00/i)).toBeInTheDocument()
35
+ })
36
+ })
@@ -34,7 +34,7 @@ const PricePerItem = ({currency, basket, basePrice}) => {
34
34
  PricePerItem.propTypes = {
35
35
  currency: PropTypes.string,
36
36
  basket: PropTypes.object,
37
- basePrice: PropTypes.string
37
+ basePrice: PropTypes.number
38
38
  }
39
39
 
40
40
  /**
@@ -31,16 +31,15 @@ import ImageGallery from '@salesforce/retail-react-app/app/components/image-gall
31
31
  import Breadcrumb from '@salesforce/retail-react-app/app/components/breadcrumb'
32
32
  import Link from '@salesforce/retail-react-app/app/components/link'
33
33
  import withRegistration from '@salesforce/retail-react-app/app/components/with-registration'
34
- import {useCurrency} from '@salesforce/retail-react-app/app/hooks'
35
34
  import {Skeleton as ImageGallerySkeleton} from '@salesforce/retail-react-app/app/components/image-gallery'
36
35
  import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive'
37
36
  import QuantityPicker from '@salesforce/retail-react-app/app/components/quantity-picker'
38
37
  import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
39
38
  import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants'
39
+ import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price'
40
+ import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils'
40
41
 
41
- const ProductViewHeader = ({name, price, currency, category, productType}) => {
42
- const intl = useIntl()
43
- const {currency: activeCurrency} = useCurrency()
42
+ const ProductViewHeader = ({name, basePrice, discountPrice, currency, category, productType}) => {
44
43
  const isProductASet = productType?.set
45
44
 
46
45
  return (
@@ -56,27 +55,20 @@ const ProductViewHeader = ({name, price, currency, category, productType}) => {
56
55
  <Heading fontSize="2xl">{`${name}`}</Heading>
57
56
  </Skeleton>
58
57
 
59
- {/* Price */}
60
- <Skeleton isLoaded={price} minWidth={32}>
61
- <Text fontWeight="bold" fontSize="md" aria-label="price">
62
- {isProductASet &&
63
- `${intl.formatMessage({
64
- id: 'product_view.label.starting_at_price',
65
- defaultMessage: 'Starting at'
66
- })} `}
67
- {intl.formatNumber(price, {
68
- style: 'currency',
69
- currency: currency || activeCurrency
70
- })}
71
- </Text>
72
- </Skeleton>
58
+ <DisplayPrice
59
+ basePrice={basePrice}
60
+ discountPrice={discountPrice}
61
+ currency={currency}
62
+ isProductASet={isProductASet}
63
+ />
73
64
  </VStack>
74
65
  )
75
66
  }
76
67
 
77
68
  ProductViewHeader.propTypes = {
78
69
  name: PropTypes.string,
79
- price: PropTypes.number,
70
+ basePrice: PropTypes.number,
71
+ discountPrice: PropTypes.number,
80
72
  currency: PropTypes.string,
81
73
  category: PropTypes.array,
82
74
  productType: PropTypes.object
@@ -134,6 +126,7 @@ const ProductView = forwardRef(
134
126
  stockLevel,
135
127
  stepQuantity
136
128
  } = useDerivedProduct(product, isProductPartOfSet)
129
+ const {basePrice, discountPrice} = getDisplayPrice(product)
137
130
  const canAddToWishlist = !isProductLoading
138
131
  const isProductASet = product?.type.set
139
132
  const errorContainerRef = useRef(null)
@@ -299,7 +292,8 @@ const ProductView = forwardRef(
299
292
  <Box display={['block', 'block', 'block', 'none']}>
300
293
  <ProductViewHeader
301
294
  name={product?.name}
302
- price={product?.pricePerUnit || product?.price}
295
+ basePrice={basePrice}
296
+ discountPrice={discountPrice}
303
297
  productType={product?.type}
304
298
  currency={product?.currency}
305
299
  category={category}
@@ -338,7 +332,8 @@ const ProductView = forwardRef(
338
332
  <Box display={['none', 'none', 'none', 'block']}>
339
333
  <ProductViewHeader
340
334
  name={product?.name}
341
- price={product?.pricePerUnit || product?.price}
335
+ basePrice={basePrice}
336
+ discountPrice={discountPrice}
342
337
  productType={product?.type}
343
338
  currency={product?.currency}
344
339
  category={category}
@@ -29,8 +29,12 @@ import Link from '@salesforce/retail-react-app/app/components/link'
29
29
  import RecommendedProducts from '@salesforce/retail-react-app/app/components/recommended-products'
30
30
  import {LockIcon} from '@salesforce/retail-react-app/app/components/icons'
31
31
  import {findImageGroupBy} from '@salesforce/retail-react-app/app/utils/image-groups-utils'
32
- import {getDisplayVariationValues} from '@salesforce/retail-react-app/app/utils/product-utils'
32
+ import {
33
+ getDisplayPrice,
34
+ getDisplayVariationValues
35
+ } from '@salesforce/retail-react-app/app/utils/product-utils'
33
36
  import {EINSTEIN_RECOMMENDERS} from '@salesforce/retail-react-app/app/constants'
37
+ import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price'
34
38
 
35
39
  /**
36
40
  * This is the context for managing the AddToCartModal.
@@ -63,7 +67,7 @@ export const AddToCartModal = () => {
63
67
  derivedData: {totalItems}
64
68
  } = useCurrentBasket()
65
69
  const size = useBreakpointValue({base: 'full', lg: '2xl', xl: '4xl'})
66
- const {currency, productItems, productSubTotal} = basket
70
+ const {currency, productSubTotal} = basket
67
71
  const numerOfItemsAdded = itemsAdded.reduce((acc, {quantity}) => acc + quantity, 0)
68
72
 
69
73
  if (!isOpen) {
@@ -110,10 +114,10 @@ export const AddToCartModal = () => {
110
114
  viewType: 'small',
111
115
  selectedVariationAttributes: variant.variationValues
112
116
  })?.images?.[0]
113
- const lineItemPrice =
114
- productItems?.find(
115
- (item) => item.productId === variant.productId
116
- )?.basePrice * quantity
117
+ const {
118
+ basePrice: lineItemBasePrice,
119
+ discountPrice: lineItemDiscountPrice
120
+ } = getDisplayPrice(product)
117
121
  const variationAttributeValues = getDisplayVariationValues(
118
122
  product.variationAttributes,
119
123
  variant.variationValues
@@ -165,13 +169,12 @@ export const AddToCartModal = () => {
165
169
  </Flex>
166
170
 
167
171
  <Box flex="none" alignSelf="flex-end" fontWeight="600">
168
- <Text>
169
- {!!lineItemPrice &&
170
- intl.formatNumber(lineItemPrice, {
171
- style: 'currency',
172
- currency: currency
173
- })}
174
- </Text>
172
+ <DisplayPrice
173
+ discountPriceProps={{as: 'p'}}
174
+ basePrice={lineItemBasePrice * quantity}
175
+ discountPrice={lineItemDiscountPrice * quantity}
176
+ currency={currency}
177
+ />
175
178
  </Box>
176
179
  </Flex>
177
180
  )
@@ -10,6 +10,7 @@ import {useVariant} from '@salesforce/retail-react-app/app/hooks/use-variant'
10
10
  import {useIntl} from 'react-intl'
11
11
  import {useVariationParams} from '@salesforce/retail-react-app/app/hooks/use-variation-params'
12
12
  import {useVariationAttributes} from '@salesforce/retail-react-app/app/hooks/use-variation-attributes'
13
+ import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils'
13
14
 
14
15
  const OUT_OF_STOCK = 'OUT_OF_STOCK'
15
16
  const UNFULFILLABLE = 'UNFULFILLABLE'
@@ -54,6 +55,8 @@ export const useDerivedProduct = (product, isProductPartOfSet = false) => {
54
55
  (isOutOfStock && inventoryMessages[OUT_OF_STOCK]) ||
55
56
  (unfulfillable && inventoryMessages[UNFULFILLABLE])
56
57
 
58
+ const {basePrice, discountPrice} = getDisplayPrice(product)
59
+
57
60
  // If the `initialQuantity` changes, update the state. This typically happens
58
61
  // when either the master product changes, or the inventory of the product changes
59
62
  // from out-of-stock to in-stock or vice versa.
@@ -72,6 +75,8 @@ export const useDerivedProduct = (product, isProductPartOfSet = false) => {
72
75
  variationParams,
73
76
  setQuantity,
74
77
  variant,
75
- stockLevel
78
+ stockLevel,
79
+ basePrice,
80
+ discountPrice
76
81
  }
77
82
  }
@@ -2211,6 +2211,14 @@ export const mockedCustomerProductListsDetails = {
2211
2211
  {
2212
2212
  calloutMsg: '$50offOrderCountAbove5',
2213
2213
  promotionId: '$50offOrderCountAbove5'
2214
+ },
2215
+ {
2216
+ promotionalPrice: 189.0,
2217
+ promotionId: '10$offIpod'
2218
+ },
2219
+ {
2220
+ promotionalPrice: 194.0,
2221
+ promotionId: '5$offIpod'
2214
2222
  }
2215
2223
  ],
2216
2224
  shortDescription:
@@ -31,6 +31,8 @@ import RecommendedProducts from '@salesforce/retail-react-app/app/components/rec
31
31
  import ProductView from '@salesforce/retail-react-app/app/components/product-view'
32
32
  import InformationAccordion from '@salesforce/retail-react-app/app/pages/product-detail/partials/information-accordion'
33
33
 
34
+ import {HTTPNotFound, HTTPError} from '@salesforce/pwa-kit-react-sdk/ssr/universal/errors'
35
+
34
36
  // constant
35
37
  import {
36
38
  API_ERROR_MESSAGE,
@@ -66,7 +68,12 @@ const ProductDetail = () => {
66
68
  /*************************** Product Detail and Category ********************/
67
69
  const {productId} = useParams()
68
70
  const urlParams = new URLSearchParams(location.search)
69
- const {data: product, isLoading: isProductLoading} = useProduct(
71
+ const {
72
+ data: product,
73
+ isLoading: isProductLoading,
74
+ isError: isProductError,
75
+ error: productError
76
+ } = useProduct(
70
77
  {
71
78
  parameters: {
72
79
  id: urlParams.get('pid') || productId,
@@ -79,15 +86,43 @@ const ProductDetail = () => {
79
86
  keepPreviousData: true
80
87
  }
81
88
  )
82
- const isProductASet = product?.type.set
89
+
83
90
  // Note: Since category needs id from product detail, it can't be server side rendered atm
84
91
  // until we can do dependent query on server
85
- const {data: category} = useCategory({
92
+ const {
93
+ data: category,
94
+ isError: isCategoryError,
95
+ error: categoryError
96
+ } = useCategory({
86
97
  parameters: {
87
98
  id: product?.primaryCategoryId,
88
99
  level: 1
89
100
  }
90
101
  })
102
+
103
+ /**************** Error Handling ****************/
104
+
105
+ if (isProductError) {
106
+ const errorStatus = productError?.response?.status
107
+ switch (errorStatus) {
108
+ case 404:
109
+ throw new HTTPNotFound('Product Not Found.')
110
+ default:
111
+ throw new HTTPError(`HTTP Error ${errorStatus} occurred.`)
112
+ }
113
+ }
114
+ if (isCategoryError) {
115
+ const errorStatus = categoryError?.response?.status
116
+ switch (errorStatus) {
117
+ case 404:
118
+ throw new HTTPNotFound('Category Not Found.')
119
+ default:
120
+ throw new HTTPError(`HTTP Error ${errorStatus} occurred.`)
121
+ }
122
+ }
123
+
124
+ const isProductASet = product?.type.set
125
+
91
126
  const [primaryCategory, setPrimaryCategory] = useState(category)
92
127
  const variant = useVariant(product)
93
128
  // This page uses the `primaryCategoryId` to retrieve the category data. This attribute
@@ -91,7 +91,6 @@ test('Allows customer to create an account', async () => {
91
91
  await user.type(withinForm.getByLabelText('Last Name'), 'Tester')
92
92
  await user.type(withinForm.getByPlaceholderText(/you@email.com/i), 'customer@test.com')
93
93
  await user.type(withinForm.getAllByLabelText(/password/i)[0], 'Password!1')
94
- screen.logTestingPlaygroundURL()
95
94
 
96
95
  // login with credentials
97
96
  global.server.use(
package/app/ssr.js CHANGED
@@ -11,6 +11,7 @@ import path from 'path'
11
11
  import {getRuntime} from '@salesforce/pwa-kit-runtime/ssr/server/express'
12
12
  import {isRemote} from '@salesforce/pwa-kit-runtime/utils/ssr-server'
13
13
  import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
14
+ import {getAppOrigin} from '@salesforce/pwa-kit-react-sdk/utils/url'
14
15
  import helmet from 'helmet'
15
16
 
16
17
  const options = {
@@ -35,19 +36,64 @@ const options = {
35
36
  const runtime = getRuntime()
36
37
 
37
38
  const {handler} = runtime.createHandler(options, (app) => {
39
+ const getRuntimeEnv = () => {
40
+ if (process.env.NODE_ENV !== 'production') return process.env.NODE_ENV ?? 'development'
41
+ const origin = getAppOrigin()
42
+ // mobify-storefront-staging sites have NODE_ENV set to production, but for the purposes
43
+ // of CSP we consider the sites to be staging.
44
+ return origin.endsWith('.mobify-storefront-staging.com') ? 'staging' : 'production'
45
+ }
46
+
47
+ // This is a temporary solution while we work on Storefront Preview - a full solution will
48
+ // prevent required CSP headers from being modified.
49
+ const getCSP = () => {
50
+ const trustedMap = {
51
+ development: [
52
+ 'localhost:*',
53
+ '*.commercecloud.salesforce.com',
54
+ '*.demandware.net',
55
+ '*.mobify-staging.com',
56
+ '*.mobify-storefront-staging.com',
57
+ '*.mobify-storefront.com',
58
+ 'runtime.commercecloud.com'
59
+ ],
60
+ staging: [
61
+ '*.demandware.net',
62
+ '*.mobify-staging.com',
63
+ '*.mobify-storefront-staging.com',
64
+ '*.mobify-storefront.com',
65
+ '*.commercecloud.salesforce.com',
66
+ 'runtime.commercecloud.com'
67
+ ],
68
+ production: [
69
+ '*.demandware.com',
70
+ '*.mobify.com',
71
+ '*.mobify-storefront.com',
72
+ '*.commercecloud.salesforce.com',
73
+ 'runtime.commercecloud.com'
74
+ ]
75
+ }
76
+
77
+ const env = getRuntimeEnv()
78
+ const trusted = ["'self'", ...(trustedMap[env] ? trustedMap[env] : [])]
79
+
80
+ return {
81
+ 'connect-src': ['api.cquotient.com', ...trusted],
82
+ 'frame-ancestors': [...trusted],
83
+ 'img-src': ['data:', ...trusted],
84
+ 'script-src': ["'unsafe-eval'", 'storage.googleapis.com', ...trusted],
85
+
86
+ // Do not upgrade insecure requests for local development
87
+ 'upgrade-insecure-requests': isRemote() ? [] : null
88
+ }
89
+ }
90
+
38
91
  // Set HTTP security headers
39
92
  app.use(
40
93
  helmet({
41
94
  contentSecurityPolicy: {
42
95
  useDefaults: true,
43
- directives: {
44
- 'img-src': ["'self'", '*.commercecloud.salesforce.com', 'data:'],
45
- 'script-src': ["'self'", "'unsafe-eval'", 'storage.googleapis.com'],
46
- 'connect-src': ["'self'", 'api.cquotient.com'],
47
-
48
- // Do not upgrade insecure requests for local development
49
- 'upgrade-insecure-requests': isRemote() ? [] : null
50
- }
96
+ directives: getCSP()
51
97
  },
52
98
  hsts: isRemote()
53
99
  })
@@ -33,3 +33,21 @@ export const getDisplayVariationValues = (variationAttributes, values = {}) => {
33
33
  }, {})
34
34
  return returnVal
35
35
  }
36
+
37
+ /**
38
+ * This function extract the promotional price from a product. If there are more than one price, the smallest price will be picked
39
+ * @param {object} product - product detail object
40
+ * @returns {{discountPrice: number, basePrice: number | string}}
41
+ */
42
+ export const getDisplayPrice = (product) => {
43
+ const basePrice = product?.pricePerUnit || product?.price
44
+ const promotionalPriceList = product?.productPromotions
45
+ ?.map((promo) => promo.promotionalPrice)
46
+ .filter((i) => i !== null && i !== undefined)
47
+ // choose the smallest price among the promotionalPrice
48
+ const discountPrice = promotionalPriceList?.length ? Math.min(...promotionalPriceList) : null
49
+ return {
50
+ basePrice,
51
+ discountPrice
52
+ }
53
+ }
@@ -5,7 +5,11 @@
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
7
 
8
- import {getDisplayVariationValues} from '@salesforce/retail-react-app/app/utils/product-utils'
8
+ import {
9
+ getDisplayPrice,
10
+ getDisplayVariationValues
11
+ } from '@salesforce/retail-react-app/app/utils/product-utils'
12
+ import {mockedCustomerProductListsDetails} from '@salesforce/retail-react-app/app/mocks/mock-data'
9
13
 
10
14
  const variationAttributes = [
11
15
  {
@@ -49,3 +53,25 @@ test('getDisplayVariationValues', () => {
49
53
  Width: 'M'
50
54
  })
51
55
  })
56
+
57
+ describe('getDisplayPrice', function () {
58
+ test('returns basePrice and discountPrice', () => {
59
+ const {basePrice, discountPrice} = getDisplayPrice(
60
+ mockedCustomerProductListsDetails.data[0]
61
+ )
62
+
63
+ expect(basePrice).toBe(199.0)
64
+ expect(discountPrice).toBe(189.0)
65
+ })
66
+
67
+ test('returns null if there is not discount promotion', () => {
68
+ const data = {
69
+ ...mockedCustomerProductListsDetails.data[0],
70
+ productPromotions: []
71
+ }
72
+ const {basePrice, discountPrice} = getDisplayPrice(data)
73
+
74
+ expect(basePrice).toBe(199.0)
75
+ expect(discountPrice).toBeNull()
76
+ })
77
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/retail-react-app",
3
- "version": "2.0.0",
3
+ "version": "2.1.0-nightly-20230927165653",
4
4
  "license": "See license in LICENSE",
5
5
  "author": "cc-pwa-kit@salesforce.com",
6
6
  "ccExtensibility": {
@@ -45,10 +45,10 @@
45
45
  "@lhci/cli": "^0.11.0",
46
46
  "@loadable/component": "^5.15.3",
47
47
  "@peculiar/webcrypto": "^1.4.2",
48
- "@salesforce/commerce-sdk-react": "1.0.2",
49
- "@salesforce/pwa-kit-dev": "3.1.1",
50
- "@salesforce/pwa-kit-react-sdk": "3.1.1",
51
- "@salesforce/pwa-kit-runtime": "3.1.1",
48
+ "@salesforce/commerce-sdk-react": "1.1.0-nightly-20230927165653",
49
+ "@salesforce/pwa-kit-dev": "3.2.0-nightly-20230927165653",
50
+ "@salesforce/pwa-kit-react-sdk": "3.2.0-nightly-20230927165653",
51
+ "@salesforce/pwa-kit-runtime": "3.2.0-nightly-20230927165653",
52
52
  "@tanstack/react-query": "^4.28.0",
53
53
  "@tanstack/react-query-devtools": "^4.29.1",
54
54
  "@testing-library/dom": "^9.0.1",
@@ -103,5 +103,5 @@
103
103
  "overrides": {
104
104
  "nwsapi": "2.2.2"
105
105
  },
106
- "gitHead": "0194d62014788b280549e70e6e80da279227b500"
106
+ "gitHead": "0ce804a25eeb347d8b4d2fb74752907b20ce4031"
107
107
  }