@salesforce/retail-react-app 1.1.0-preview.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,10 +1,20 @@
1
- ## v1.1.0-preview.0
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
+
6
+ ## v2.0.0 (Sep 21, 2023)
2
7
 
3
- - Fix price display when a discounted price is zero [#1361](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1361)
4
- - Fix price adjustments of a line item when more than one is present [#1362](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1362)
8
+ - V3 Fix checkout card number [#1424](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1424)
9
+ - Cleanup wrong import path for page-designer component [#1441](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1441) @clavery
10
+ - Modularize country code source for targeting via extensibility [#1445](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1445) @bfeister
11
+ - Export icon helper function for target via overrides [#1420](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1420) @cbrother-csu
12
+ - Migrate Page Designer core types to commerce-sdk-react [#1441](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1441) @clavery
5
13
 
6
- ## v1.1.0-dev (June 15, 2023)
14
+ ## v1.0.1 (Jul 26, 2023)
7
15
 
16
+ - Fix price display when a discounted price is zero [#1361](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1361)
17
+ - Fix price adjustments of a line item when more than one is present [#1362](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1362)
8
18
  - Registered user checkout should set order with an email [#1363](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1363)
9
19
 
10
20
  ## v1.0.0 (June 15, 2023)
@@ -16,9 +26,9 @@
16
26
 
17
27
  ## Older Versions Without Namespace
18
28
 
19
- The older versions below were published without the `@salesforce` namespace.
29
+ The versions published below were not published on npm, and the versioning matched that of the PWA Kit SDK. These versions did not use the `@salesforce` namespace.
20
30
 
21
- ## v2.7.1 (May 11, 2023)
31
+ ### v2.7.1 (May 11, 2023)
22
32
 
23
33
  - Replace invalid row value with nowrap [#1179](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1179)
24
34
  - Add a redirect to login page after user signs out from checkout page [#1172](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1172)
@@ -32,7 +42,7 @@ The older versions below were published without the `@salesforce` namespace.
32
42
  - Fix addresses not having preferred address first. [#1051](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1051)
33
43
  - Changed type of the phone number field to bring up numberic keyboard on mobile devices - W-9871940 [#1016)](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1016)
34
44
 
35
- ## v2.7.0 (Mar 03, 2023)
45
+ ### v2.7.0 (Mar 03, 2023)
36
46
 
37
47
  - Add Page Designer ImageTile component [#967](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/967)
38
48
  - Add Page Designer ImageWithText component [#991](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/991)
@@ -40,7 +50,7 @@ The older versions below were published without the `@salesforce` namespace.
40
50
  - Add Page Designer layout components [#993](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/993)
41
51
  - Support the product-set type [#1019](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1019)
42
52
 
43
- ## v2.6.0 (Jan 25, 2023)
53
+ ### v2.6.0 (Jan 25, 2023)
44
54
 
45
55
  - Mega menu fixes [#875](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/875) and [#910](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/910)
46
56
  - Cache SLAS callback using request processor [#884](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/884)
@@ -49,14 +59,14 @@ The older versions below were published without the `@salesforce` namespace.
49
59
  - Fix PropTypes [#924](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/924)
50
60
  - Remove unnecessary map statement [#929](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/929)
51
61
 
52
- ## v2.5.0 (Jan 5, 2023)
62
+ ### v2.5.0 (Jan 5, 2023)
53
63
 
54
64
  - Add instanceType to Einstein activity body [#858](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/858)
55
65
  - Do not use a proxy to call Einstein [#857](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/857)
56
66
  - Einstein handle variants [#867](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/867)
57
67
  - Fix cc exp year [#874](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/874)
58
68
 
59
- ## v2.4.0 (Dec 1, 2022)
69
+ ### v2.4.0 (Dec 1, 2022)
60
70
 
61
71
  - Dynamic footer Copyright date [#741](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/741)
62
72
  - Footer copyright: remove the remaining hardcoded year [#760](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/760)
@@ -66,7 +76,7 @@ The older versions below were published without the `@salesforce` namespace.
66
76
  - pwa-kit-dev command for tailing logs [#789](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/789)
67
77
  - Update usages of zzrf-001 ODS instance to the new short URL format [#816](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/816)
68
78
 
69
- ## v2.3.0 (Oct 27, 2022)
79
+ ### v2.3.0 (Oct 27, 2022)
70
80
 
71
81
  - Fix locale `alias` by including `locale` object inside the MultiSite Context. [#716](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/716)
72
82
  - Updated translations. [#725](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/725)
@@ -77,20 +87,20 @@ The older versions below were published without the `@salesforce` namespace.
77
87
  - Bugfix: image gallery thumbnails not working without DIS [#786](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/786)
78
88
  - Ensure that a valid target locale is use if none is provided [#788](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/788)
79
89
 
80
- ## v2.2.0 (Aug 25, 2022)
90
+ ### v2.2.0 (Aug 25, 2022)
81
91
 
82
92
  - Update zzrf-001 url [#694](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/694)
83
93
  - Optimize Server-side performance [#667](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/667)
84
94
  - Remove references to session bridging [#684](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/684)
85
95
 
86
- ## v2.1.0 (Jul 05, 2022)
96
+ ### v2.1.0 (Jul 05, 2022)
87
97
 
88
98
  - Update translations [#643](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/643)
89
99
  - Update translations [#653](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/653)
90
100
  - Add bundlesize test back into CI [#652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/652)
91
101
  - Fix UI bug on notifications badge [#620](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/620)
92
102
 
93
- ## v2.0.0 (May 16, 2022)
103
+ ### v2.0.0 (May 16, 2022)
94
104
 
95
105
  - Update translation docs [#570](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/570)
96
106
  - Fixed loading correct amount of skeletons [#576] (<https://github.com/SalesforceCommerceCloud/pwa-kit/pull/576>)
@@ -110,7 +120,7 @@ The older versions below were published without the `@salesforce` namespace.
110
120
  - Service worker loading for dev server [#464](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/464)
111
121
  - Environment Specific Configuration Support [#477](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/447)
112
122
 
113
- ## v1.5.0 (Mar 17, 2022)
123
+ ### v1.5.0 (Mar 17, 2022)
114
124
 
115
125
  - Support storing authentication tokens in cookie [#429](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/429)
116
126
  - Make sure the forgot-password modal also shows up in the checkout flow [#373](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/373)
@@ -118,7 +128,7 @@ The older versions below were published without the `@salesforce` namespace.
118
128
  - Import cross-fetch to make OCAPI client isomorphic [#382](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/382)
119
129
  - Multi-site implementation for Retail React App [#391](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/391)
120
130
 
121
- ## v1.4.0 (Jan 27, 2022)
131
+ ### v1.4.0 (Jan 27, 2022)
122
132
 
123
133
  - Do not send HSTS header during local development [#288](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/288)
124
134
  - Add constants for `<meta>` tags `theme-color` and `apple-mobile-web-app-title` [#287](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/287)
@@ -126,7 +136,7 @@ The older versions below were published without the `@salesforce` namespace.
126
136
  - Import preliminary translations [#324](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/324)
127
137
  - Remove old dependencies that are no longer used [#317](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/317)
128
138
 
129
- ## v1.3.0 (Jan 06, 2022)
139
+ ### v1.3.0 (Jan 06, 2022)
130
140
 
131
141
  - Remove Einstein from home page [#208](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/208)
132
142
  - Add git2gus config to allow git2gus integration [210](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/210)
@@ -148,7 +158,7 @@ The older versions below were published without the `@salesforce` namespace.
148
158
  - Fix show applied promotion codes case sensitive issue [#224](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/224)
149
159
  - Fix section subtitle incorrect prop warning [#282](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/282)
150
160
 
151
- ## v1.2.0 (Nov 18, 2021)
161
+ ### v1.2.0 (Nov 18, 2021)
152
162
 
153
163
  - Simplify homepage for Retail React App [#201](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/201)
154
164
  - Ensure `cookieId` value is sent always for Einstein recommendations [#179](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/179)
@@ -169,7 +179,7 @@ The older versions below were published without the `@salesforce` namespace.
169
179
  - Add cache control header to product details page [#172](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/172)
170
180
  - Clear SLAS tokens when OID is changed [#178](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/178)
171
181
 
172
- ## v1.1.0 (Sep 27, 2021)
182
+ ### v1.1.0 (Sep 27, 2021)
173
183
 
174
184
  - Fix wishlist bugs and provide better hooks for wishlist features. [#64](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/64)
175
185
  - Lazy load Popover component. [#134](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/134)
@@ -182,6 +192,6 @@ The older versions below were published without the `@salesforce` namespace.
182
192
  - Security - `inquirer` package upgrade. [#121](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/121)
183
193
  - New quantity picker. [#102](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/102)
184
194
 
185
- ## v1.0.0 (Sep 08, 2021)
195
+ ### v1.0.0 (Sep 08, 2021)
186
196
 
187
197
  - PWA Kit General Availability and open source. 🎉
@@ -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
+ })
@@ -10,6 +10,7 @@ import {
10
10
  stateOptions,
11
11
  provinceOptions
12
12
  } from '@salesforce/retail-react-app/app/components/forms/state-province-options'
13
+ import {SHIPPING_COUNTRY_CODES} from '@salesforce/retail-react-app/app/constants'
13
14
 
14
15
  const messages = defineMessages({
15
16
  required: {defaultMessage: 'Required', id: 'use_address_fields.error.required'},
@@ -103,10 +104,7 @@ export default function useAddressFields({
103
104
  label: formatMessage(messages.country),
104
105
  defaultValue: 'US',
105
106
  type: 'select',
106
- options: [
107
- {value: 'CA', label: 'Canada'},
108
- {value: 'US', label: 'United States'}
109
- ],
107
+ options: SHIPPING_COUNTRY_CODES,
110
108
  rules: {
111
109
  required: formatMessage({
112
110
  defaultMessage: 'Please select your country.',
@@ -84,7 +84,7 @@ VisaSymbol.viewBox = VisaSymbol.viewBox || '0 0 38 22'
84
84
  * @param {string} name - the filename of the imported svg (does not include extension)
85
85
  */
86
86
  /* istanbul ignore next */
87
- const icon = (name, passProps) => {
87
+ export const icon = (name, passProps) => {
88
88
  const displayName = name
89
89
  .toLowerCase()
90
90
  .replace(/(?:^|[\s-/])\w/g, (match) => match.toUpperCase())
@@ -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}
package/app/constants.js CHANGED
@@ -107,3 +107,8 @@ export const EINSTEIN_RECOMMENDERS = {
107
107
  EMPTY_SEARCH_RESULTS_TOP_SELLERS: 'home-top-revenue-for-category',
108
108
  EMPTY_SEARCH_RESULTS_MOST_VIEWED: 'products-in-all-categories'
109
109
  }
110
+
111
+ export const SHIPPING_COUNTRY_CODES = [
112
+ {value: 'CA', label: 'Canada'},
113
+ {value: 'US', label: 'United States'}
114
+ ]
@@ -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:
@@ -5,6 +5,6 @@
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 {Page, pageType} from '@salesforce/retail-react-app/app/page-designer/core'
8
+ import {Page, pageType} from '@salesforce/commerce-sdk-react/components'
9
9
 
10
10
  export {Page, pageType}
@@ -24,6 +24,7 @@ import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-curre
24
24
  import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout/util/checkout-context'
25
25
  import {
26
26
  getPaymentInstrumentCardType,
27
+ getMaskCreditCardNumber,
27
28
  getCreditCardIcon
28
29
  } from '@salesforce/retail-react-app/app/utils/cc-utils'
29
30
  import {
@@ -84,7 +85,7 @@ const Payment = () => {
84
85
  paymentMethodId: 'CREDIT_CARD',
85
86
  paymentCard: {
86
87
  holder: formValue.holder,
87
- issueNumber: formValue.number.replace(/ /g, ''),
88
+ maskedNumber: getMaskCreditCardNumber(formValue.number),
88
89
  cardType: getPaymentInstrumentCardType(formValue.cardType),
89
90
  expirationMonth: parseInt(expirationMonth),
90
91
  expirationYear: parseInt(`20${expirationYear}`)
@@ -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
  })
@@ -83,6 +83,21 @@ export const getPaymentInstrumentCardType = (type) => {
83
83
  }[type]
84
84
  }
85
85
 
86
+ /**
87
+ * Returns the masked credit card number by removing white spaces,
88
+ * replacing all digits except the last 4 using the '*' character.
89
+ * @param cardNumber - The card number unmasked
90
+ * @returns {string} - The masked card number
91
+ */
92
+ export const getMaskCreditCardNumber = (cardNumber) => {
93
+ const trimmedCardNumber = cardNumber.replace(/\s/g, '')
94
+
95
+ const maskedDigits = trimmedCardNumber.slice(0, -4).replace(/\d/g, '*')
96
+ const lastFourDigits = trimmedCardNumber.slice(-4)
97
+
98
+ return maskedDigits + lastFourDigits
99
+ }
100
+
86
101
  export const createCreditCardPaymentBodyFromForm = (paymentFormData) => {
87
102
  // Using destructuring to omit properties
88
103
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -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": "1.1.0-preview.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.1-preview.0",
49
- "@salesforce/pwa-kit-dev": "3.1.0-preview.4",
50
- "@salesforce/pwa-kit-react-sdk": "3.1.0-preview.4",
51
- "@salesforce/pwa-kit-runtime": "3.1.0-preview.4",
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": "8ef8310dabf77dfdc50b76fbe2464f771c6a69c0"
106
+ "gitHead": "0ce804a25eeb347d8b4d2fb74752907b20ce4031"
107
107
  }