@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 +5 -0
- package/app/components/_app/index.jsx +3 -2
- package/app/components/display-price/index.jsx +67 -0
- package/app/components/display-price/index.test.js +36 -0
- package/app/components/item-variant/item-price.jsx +1 -1
- package/app/components/product-view/index.jsx +16 -21
- package/app/hooks/use-add-to-cart-modal.js +16 -13
- package/app/hooks/use-derived-product.js +6 -1
- package/app/mocks/mock-data.js +8 -0
- package/app/pages/product-detail/index.jsx +38 -3
- package/app/pages/registration/index.test.jsx +0 -1
- package/app/ssr.js +54 -8
- package/app/utils/product-utils.js +18 -0
- package/app/utils/product-utils.test.js +27 -1
- package/package.json +6 -6
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
|
+
})
|
|
@@ -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,
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
<
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
}
|
package/app/mocks/mock-data.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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.
|
|
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
|
|
49
|
-
"@salesforce/pwa-kit-dev": "3.
|
|
50
|
-
"@salesforce/pwa-kit-react-sdk": "3.
|
|
51
|
-
"@salesforce/pwa-kit-runtime": "3.
|
|
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": "
|
|
106
|
+
"gitHead": "0ce804a25eeb347d8b4d2fb74752907b20ce4031"
|
|
107
107
|
}
|