@salesforce/retail-react-app 8.3.0 → 8.4.0-nightly-20260116080227
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/multiship/multiship-order-summary.jsx +6 -2
- package/app/components/order-summary/index.jsx +33 -25
- package/app/constants.js +0 -6
- package/app/hooks/use-password-reset.js +2 -1
- package/app/hooks/use-password-reset.test.js +11 -2
- package/app/pages/account/order-detail.jsx +46 -32
- package/app/pages/account/orders.test.js +116 -0
- package/app/pages/checkout/confirmation.jsx +52 -42
- package/app/pages/login/index.jsx +2 -2
- package/app/pages/login/passwordless-landing.test.js +114 -7
- package/app/pages/reset-password/index.jsx +2 -3
- package/app/pages/reset-password/index.test.jsx +31 -1
- package/app/routes.jsx +34 -28
- package/app/routes.test.js +272 -0
- package/app/utils/bonus-product/cart.js +32 -0
- package/app/utils/bonus-product/cart.test.js +353 -0
- package/app/utils/routes-utils.js +131 -13
- package/app/utils/routes-utils.test.js +177 -0
- package/config/mocks/default.js +5 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## v8.4.0-dev (Dec 17, 2025)
|
|
2
|
+
- [Feature] Add `fuzzyPathMatching` to reduce computational overhead of route generation at time of application load [#3530](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3530)
|
|
3
|
+
- [Bugfix] Fix Passwordless Login landingPath, Reset Password landingPath, and Social Login redirectUri value in config not being used [#3560](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3560)
|
|
4
|
+
- [Feature] Integrate Order Details page to display orders data from SOM [#3573](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3573)
|
|
5
|
+
|
|
1
6
|
## v8.3.0 (Dec 17, 2025)
|
|
2
7
|
- [Bugfix] Fix Forgot Password link not working from Account Profile password update form [#3493](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3493)
|
|
3
8
|
- Introduce Address Autocompletion feature in the checkout flow, powered by Google Maps Platform [#3071](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3071)
|
|
@@ -22,6 +22,7 @@ import CartItemVariantPrice from '@salesforce/retail-react-app/app/components/it
|
|
|
22
22
|
import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants'
|
|
23
23
|
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
|
|
24
24
|
import {groupShipmentsByDeliveryOption} from '@salesforce/retail-react-app/app/utils/shipment-utils'
|
|
25
|
+
import {consolidateDuplicateBonusProducts} from '@salesforce/retail-react-app/app/utils/bonus-product/cart'
|
|
25
26
|
|
|
26
27
|
const MultiShipOrderSummary = ({order, productItemsMap, currency}) => {
|
|
27
28
|
const storeLocatorEnabled = getConfig()?.app?.storeLocatorEnabled ?? STORE_LOCATOR_IS_ENABLED
|
|
@@ -59,6 +60,7 @@ const MultiShipOrderSummary = ({order, productItemsMap, currency}) => {
|
|
|
59
60
|
<Stack spacing={4}>
|
|
60
61
|
{shipments.map((shipment) => {
|
|
61
62
|
const items = getItemsForShipment(shipment.shipmentId)
|
|
63
|
+
const consolidatedItems = consolidateDuplicateBonusProducts(items)
|
|
62
64
|
|
|
63
65
|
return (
|
|
64
66
|
<Box key={shipment.shipmentId}>
|
|
@@ -68,7 +70,7 @@ const MultiShipOrderSummary = ({order, productItemsMap, currency}) => {
|
|
|
68
70
|
width="full"
|
|
69
71
|
divider={<Divider />}
|
|
70
72
|
>
|
|
71
|
-
{
|
|
73
|
+
{consolidatedItems.map((product, idx) => {
|
|
72
74
|
const productDetail =
|
|
73
75
|
productItemsMap?.[product.productId] || {}
|
|
74
76
|
const variant = {
|
|
@@ -79,7 +81,9 @@ const MultiShipOrderSummary = ({order, productItemsMap, currency}) => {
|
|
|
79
81
|
|
|
80
82
|
return (
|
|
81
83
|
<ItemVariantProvider
|
|
82
|
-
key={product.productId}
|
|
84
|
+
key={`${product.productId}-${
|
|
85
|
+
product.itemId || idx
|
|
86
|
+
}`}
|
|
83
87
|
index={idx}
|
|
84
88
|
variant={variant}
|
|
85
89
|
>
|
|
@@ -31,6 +31,7 @@ import CartItemVariantPrice from '@salesforce/retail-react-app/app/components/it
|
|
|
31
31
|
import PromoPopover from '@salesforce/retail-react-app/app/components/promo-popover'
|
|
32
32
|
import {useProducts} from '@salesforce/commerce-sdk-react'
|
|
33
33
|
import {BasketIcon} from '@salesforce/retail-react-app/app/components/icons'
|
|
34
|
+
import {consolidateDuplicateBonusProducts} from '@salesforce/retail-react-app/app/utils/bonus-product/cart'
|
|
34
35
|
|
|
35
36
|
const CartItems = ({basket}) => {
|
|
36
37
|
const totalItems = basket?.productItems?.reduce((acc, item) => acc + item.quantity, 0) || 0
|
|
@@ -72,32 +73,39 @@ const CartItems = ({basket}) => {
|
|
|
72
73
|
</AccordionButton>
|
|
73
74
|
<AccordionPanel px={0} py={4}>
|
|
74
75
|
<Stack spacing={5} align="flex-start" divider={<Divider />}>
|
|
75
|
-
{
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
...(products && products[product.productId]),
|
|
79
|
-
price: product.price
|
|
80
|
-
}
|
|
81
|
-
return (
|
|
82
|
-
<ItemVariantProvider
|
|
83
|
-
key={`order-summary-item-${product.productId}-${product.itemId}`}
|
|
84
|
-
index={idx}
|
|
85
|
-
variant={variant}
|
|
86
|
-
>
|
|
87
|
-
<Flex width="full" alignItems="flex-start">
|
|
88
|
-
<CartItemVariantImage width="80px" mr={2} />
|
|
89
|
-
<Stack width="full" spacing={1} marginTop="-3px">
|
|
90
|
-
<CartItemVariantName />
|
|
91
|
-
<CartItemVariantAttributes includeQuantity />
|
|
92
|
-
<CartItemVariantPrice
|
|
93
|
-
baseDirection="row"
|
|
94
|
-
currency={basket?.currency}
|
|
95
|
-
/>
|
|
96
|
-
</Stack>
|
|
97
|
-
</Flex>
|
|
98
|
-
</ItemVariantProvider>
|
|
76
|
+
{(() => {
|
|
77
|
+
const consolidatedItems = consolidateDuplicateBonusProducts(
|
|
78
|
+
basket.productItems || []
|
|
99
79
|
)
|
|
100
|
-
|
|
80
|
+
return consolidatedItems.map((product, idx) => {
|
|
81
|
+
const variant = {
|
|
82
|
+
...product,
|
|
83
|
+
...(products && products[product.productId]),
|
|
84
|
+
price: product.price
|
|
85
|
+
}
|
|
86
|
+
return (
|
|
87
|
+
<ItemVariantProvider
|
|
88
|
+
key={`order-summary-item-${product.productId}-${
|
|
89
|
+
product.itemId || idx
|
|
90
|
+
}`}
|
|
91
|
+
index={idx}
|
|
92
|
+
variant={variant}
|
|
93
|
+
>
|
|
94
|
+
<Flex width="full" alignItems="flex-start">
|
|
95
|
+
<CartItemVariantImage width="80px" mr={2} />
|
|
96
|
+
<Stack width="full" spacing={1} marginTop="-3px">
|
|
97
|
+
<CartItemVariantName />
|
|
98
|
+
<CartItemVariantAttributes includeQuantity />
|
|
99
|
+
<CartItemVariantPrice
|
|
100
|
+
baseDirection="row"
|
|
101
|
+
currency={basket?.currency}
|
|
102
|
+
/>
|
|
103
|
+
</Stack>
|
|
104
|
+
</Flex>
|
|
105
|
+
</ItemVariantProvider>
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
})()}
|
|
101
109
|
|
|
102
110
|
<Button as={Link} to="/cart" variant="link" width="full" color="blue.700">
|
|
103
111
|
<FormattedMessage
|
package/app/constants.js
CHANGED
|
@@ -251,12 +251,6 @@ export const LOGIN_TYPES = {
|
|
|
251
251
|
SOCIAL: 'social'
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
// Constants for Password Reset
|
|
255
|
-
export const RESET_PASSWORD_LANDING_PATH = '/reset-password-landing'
|
|
256
|
-
|
|
257
|
-
// Constants for Passwordless Login
|
|
258
|
-
export const PASSWORDLESS_LOGIN_LANDING_PATH = '/passwordless-login-landing'
|
|
259
|
-
|
|
260
254
|
export const PASSWORDLESS_ERROR_MESSAGES = [
|
|
261
255
|
/callback_uri doesn't match/i,
|
|
262
256
|
/passwordless permissions error/i,
|
|
@@ -25,6 +25,7 @@ export const usePasswordReset = () => {
|
|
|
25
25
|
const callbackURI = isAbsoluteURL(resetPasswordCallback)
|
|
26
26
|
? resetPasswordCallback
|
|
27
27
|
: `${appOrigin}${getEnvBasePath()}${resetPasswordCallback}`
|
|
28
|
+
const resetPasswordLandingPath = config.app.login?.resetPassword?.landingPath
|
|
28
29
|
|
|
29
30
|
const getPasswordResetTokenMutation = useAuthHelper(AuthHelpers.GetPasswordResetToken)
|
|
30
31
|
const resetPasswordMutation = useAuthHelper(AuthHelpers.ResetPassword)
|
|
@@ -54,5 +55,5 @@ export const usePasswordReset = () => {
|
|
|
54
55
|
)
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
return {getPasswordResetToken, resetPassword}
|
|
58
|
+
return {getPasswordResetToken, resetPassword, resetPasswordLandingPath}
|
|
58
59
|
}
|
|
@@ -9,14 +9,14 @@ import {fireEvent, screen, waitFor} from '@testing-library/react'
|
|
|
9
9
|
import {useAuthHelper, AuthHelpers} from '@salesforce/commerce-sdk-react'
|
|
10
10
|
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
|
|
11
11
|
import {usePasswordReset} from '@salesforce/retail-react-app/app/hooks/use-password-reset'
|
|
12
|
+
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
|
|
12
13
|
|
|
13
14
|
const mockEmail = 'test@email.com'
|
|
14
15
|
const mockToken = '123456'
|
|
15
16
|
const mockNewPassword = 'new-password'
|
|
16
17
|
|
|
17
18
|
const MockComponent = () => {
|
|
18
|
-
const {getPasswordResetToken, resetPassword} = usePasswordReset()
|
|
19
|
-
|
|
19
|
+
const {getPasswordResetToken, resetPassword, resetPasswordLandingPath} = usePasswordReset()
|
|
20
20
|
return (
|
|
21
21
|
<div>
|
|
22
22
|
<button
|
|
@@ -33,6 +33,8 @@ const MockComponent = () => {
|
|
|
33
33
|
})
|
|
34
34
|
}
|
|
35
35
|
/>
|
|
36
|
+
|
|
37
|
+
<div data-testid="reset-password-landing-path">{resetPasswordLandingPath}</div>
|
|
36
38
|
</div>
|
|
37
39
|
)
|
|
38
40
|
}
|
|
@@ -91,4 +93,11 @@ describe('usePasswordReset', () => {
|
|
|
91
93
|
)
|
|
92
94
|
})
|
|
93
95
|
})
|
|
96
|
+
|
|
97
|
+
test('resetPasswordLandingPath is returned', () => {
|
|
98
|
+
renderWithProviders(<MockComponent />)
|
|
99
|
+
expect(screen.getByTestId('reset-password-landing-path')).toHaveTextContent(
|
|
100
|
+
mockConfig.app.login.resetPassword.landingPath
|
|
101
|
+
)
|
|
102
|
+
})
|
|
94
103
|
})
|
|
@@ -35,6 +35,7 @@ import StoreDisplay from '@salesforce/retail-react-app/app/components/store-disp
|
|
|
35
35
|
import {groupShipmentsByDeliveryOption} from '@salesforce/retail-react-app/app/utils/shipment-utils'
|
|
36
36
|
import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants'
|
|
37
37
|
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
|
|
38
|
+
import {consolidateDuplicateBonusProducts} from '@salesforce/retail-react-app/app/utils/bonus-product/cart'
|
|
38
39
|
import PropTypes from 'prop-types'
|
|
39
40
|
const onClient = typeof window !== 'undefined'
|
|
40
41
|
|
|
@@ -57,7 +58,8 @@ const OrderProducts = ({productItems, currency}) => {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
)
|
|
60
|
-
const
|
|
61
|
+
const consolidatedItems = consolidateDuplicateBonusProducts(productItems || [])
|
|
62
|
+
const variants = consolidatedItems?.map((item) => {
|
|
61
63
|
const product = products?.[item.productId]
|
|
62
64
|
return {
|
|
63
65
|
...(product ? product : {}),
|
|
@@ -115,9 +117,15 @@ const AccountOrderDetail = () => {
|
|
|
115
117
|
const {formatMessage, formatDate} = useIntl()
|
|
116
118
|
const storeLocatorEnabled = getConfig()?.app?.storeLocatorEnabled ?? STORE_LOCATOR_IS_ENABLED
|
|
117
119
|
|
|
120
|
+
// expand: 'oms' returns order data from OMS if the order is successfully
|
|
121
|
+
// ingested to OMS, otherwise returns data from ECOM
|
|
122
|
+
// For regular non-oms orders, the order data is returned from ECOM
|
|
118
123
|
const {data: order, isLoading: isOrderLoading} = useOrder(
|
|
119
124
|
{
|
|
120
|
-
parameters: {
|
|
125
|
+
parameters: {
|
|
126
|
+
orderNo: params.orderNo,
|
|
127
|
+
expand: 'oms, oms_shipments'
|
|
128
|
+
}
|
|
121
129
|
},
|
|
122
130
|
{
|
|
123
131
|
enabled: onClient && !!params.orderNo
|
|
@@ -155,9 +163,9 @@ const AccountOrderDetail = () => {
|
|
|
155
163
|
[storeData?.data]
|
|
156
164
|
)
|
|
157
165
|
|
|
158
|
-
const paymentCard = order?.paymentInstruments[0]?.paymentCard
|
|
166
|
+
const paymentCard = order?.paymentInstruments?.[0]?.paymentCard
|
|
159
167
|
const CardIcon = getCreditCardIcon(paymentCard?.cardType)
|
|
160
|
-
const itemCount = order?.productItems
|
|
168
|
+
const itemCount = order?.productItems?.reduce((count, item) => item.quantity + count, 0) || 0
|
|
161
169
|
|
|
162
170
|
const headingRef = useRef()
|
|
163
171
|
useEffect(() => {
|
|
@@ -231,7 +239,9 @@ const AccountOrderDetail = () => {
|
|
|
231
239
|
values={{orderNumber: order.orderNo}}
|
|
232
240
|
/>
|
|
233
241
|
</Text>
|
|
234
|
-
<Badge colorScheme="green">
|
|
242
|
+
<Badge colorScheme="green">
|
|
243
|
+
{order.status || order.omsData?.status}
|
|
244
|
+
</Badge>
|
|
235
245
|
</Stack>
|
|
236
246
|
</Stack>
|
|
237
247
|
) : (
|
|
@@ -383,8 +393,10 @@ const AccountOrderDetail = () => {
|
|
|
383
393
|
</Heading>
|
|
384
394
|
<Box>
|
|
385
395
|
<Text fontSize="sm">
|
|
386
|
-
{shipment.shippingAddress.firstName
|
|
387
|
-
|
|
396
|
+
{shipment.shippingAddress.firstName &&
|
|
397
|
+
shipment.shippingAddress.lastName
|
|
398
|
+
? `${shipment.shippingAddress.firstName} ${shipment.shippingAddress.lastName}`
|
|
399
|
+
: shipment.shippingAddress.fullName}
|
|
388
400
|
</Text>
|
|
389
401
|
<Text fontSize="sm">
|
|
390
402
|
{shipment.shippingAddress.address1}
|
|
@@ -400,32 +412,34 @@ const AccountOrderDetail = () => {
|
|
|
400
412
|
))}
|
|
401
413
|
|
|
402
414
|
{/* Payment Method */}
|
|
403
|
-
|
|
404
|
-
<
|
|
405
|
-
<
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
<
|
|
416
|
-
|
|
417
|
-
<
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
415
|
+
{paymentCard && (
|
|
416
|
+
<Stack spacing={1}>
|
|
417
|
+
<Heading as="h2" fontSize="sm" pt={1}>
|
|
418
|
+
<FormattedMessage
|
|
419
|
+
defaultMessage="Payment Method"
|
|
420
|
+
id="account_order_detail.heading.payment_method"
|
|
421
|
+
/>
|
|
422
|
+
</Heading>
|
|
423
|
+
<Stack direction="row">
|
|
424
|
+
{CardIcon && (
|
|
425
|
+
<CardIcon layerStyle="ccIcon" aria-hidden="true" />
|
|
426
|
+
)}
|
|
427
|
+
<Box>
|
|
428
|
+
<Text fontSize="sm">{paymentCard?.cardType}</Text>
|
|
429
|
+
<Stack direction="row">
|
|
430
|
+
<Text fontSize="sm">
|
|
431
|
+
••••{' '}
|
|
432
|
+
{paymentCard?.numberLastDigits}
|
|
433
|
+
</Text>
|
|
434
|
+
<Text fontSize="sm">
|
|
435
|
+
{paymentCard?.expirationMonth}/
|
|
436
|
+
{paymentCard?.expirationYear}
|
|
437
|
+
</Text>
|
|
438
|
+
</Stack>
|
|
439
|
+
</Box>
|
|
440
|
+
</Stack>
|
|
427
441
|
</Stack>
|
|
428
|
-
|
|
442
|
+
)}
|
|
429
443
|
|
|
430
444
|
{/* Billing Address */}
|
|
431
445
|
<Stack spacing={1}>
|
|
@@ -239,6 +239,122 @@ describe('Handles order with missing or partial data gracefully', () => {
|
|
|
239
239
|
})
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
+
// Helper to setup order details page with mock order data
|
|
243
|
+
const setupOrderDetailsPage = (mockOrder) => {
|
|
244
|
+
global.server.use(
|
|
245
|
+
rest.get('*/orders/:orderNo', (req, res, ctx) => {
|
|
246
|
+
return res(ctx.delay(0), ctx.json(mockOrder))
|
|
247
|
+
})
|
|
248
|
+
)
|
|
249
|
+
window.history.pushState(
|
|
250
|
+
{},
|
|
251
|
+
'Order Details',
|
|
252
|
+
createPathWithDefaults(`/account/orders/${mockOrder.orderNo}`)
|
|
253
|
+
)
|
|
254
|
+
renderWithProviders(<MockedComponent history={history} />, {
|
|
255
|
+
wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
describe('Order with ECOM status (non-OMS)', () => {
|
|
260
|
+
beforeEach(async () => {
|
|
261
|
+
const ecomOrder = {
|
|
262
|
+
...mockOrderHistory.data[0],
|
|
263
|
+
status: 'new'
|
|
264
|
+
}
|
|
265
|
+
setupOrderDetailsPage(ecomOrder)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('should display ECOM status when present', async () => {
|
|
269
|
+
const statusBadge = await screen.findByText('new')
|
|
270
|
+
expect(statusBadge).toBeInTheDocument()
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('Order with OMS data', () => {
|
|
275
|
+
beforeEach(async () => {
|
|
276
|
+
const omsOrder = {
|
|
277
|
+
...mockOrderHistory.data[0],
|
|
278
|
+
status: undefined,
|
|
279
|
+
omsData: {status: 'SHIPPED'}
|
|
280
|
+
}
|
|
281
|
+
setupOrderDetailsPage(omsOrder)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('should display OMS status when ECOM status is not present', async () => {
|
|
285
|
+
const statusBadge = await screen.findByText('SHIPPED')
|
|
286
|
+
expect(statusBadge).toBeInTheDocument()
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('Order without payment data', () => {
|
|
291
|
+
beforeEach(async () => {
|
|
292
|
+
const orderWithoutPayment = {
|
|
293
|
+
...mockOrderHistory.data[0],
|
|
294
|
+
paymentInstruments: []
|
|
295
|
+
}
|
|
296
|
+
setupOrderDetailsPage(orderWithoutPayment)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('should render order details page', async () => {
|
|
300
|
+
expect(await screen.findByTestId('account-order-details-page')).toBeInTheDocument()
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('should not display payment method section', async () => {
|
|
304
|
+
await screen.findByTestId('account-order-details-page')
|
|
305
|
+
expect(screen.queryByText(/payment method/i)).not.toBeInTheDocument()
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
describe('Order with firstName and lastName for shipping address', () => {
|
|
310
|
+
beforeEach(async () => {
|
|
311
|
+
const orderWithNames = {
|
|
312
|
+
...mockOrderHistory.data[0],
|
|
313
|
+
shipments: [
|
|
314
|
+
{
|
|
315
|
+
...mockOrderHistory.data[0].shipments[0],
|
|
316
|
+
shippingAddress: {
|
|
317
|
+
...mockOrderHistory.data[0].shipments[0].shippingAddress,
|
|
318
|
+
firstName: 'Jane',
|
|
319
|
+
lastName: 'Doe',
|
|
320
|
+
fullName: 'Should Not Display'
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
setupOrderDetailsPage(orderWithNames)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('should display firstName + lastName when both present', async () => {
|
|
329
|
+
expect(await screen.findByText(/Jane Doe/i)).toBeInTheDocument()
|
|
330
|
+
expect(screen.queryByText(/Should Not Display/i)).not.toBeInTheDocument()
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
describe('Order with fullName fallback for shipping address', () => {
|
|
335
|
+
beforeEach(async () => {
|
|
336
|
+
const orderWithFullName = {
|
|
337
|
+
...mockOrderHistory.data[0],
|
|
338
|
+
shipments: [
|
|
339
|
+
{
|
|
340
|
+
...mockOrderHistory.data[0].shipments[0],
|
|
341
|
+
shippingAddress: {
|
|
342
|
+
...mockOrderHistory.data[0].shipments[0].shippingAddress,
|
|
343
|
+
firstName: undefined,
|
|
344
|
+
lastName: undefined,
|
|
345
|
+
fullName: 'John Smith'
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
setupOrderDetailsPage(orderWithFullName)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test('should display fullName when firstName and lastName are not present', async () => {
|
|
354
|
+
expect(await screen.findByText(/John Smith/i)).toBeInTheDocument()
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
242
358
|
describe('Order with multiple shipments (pickup and delivery)', () => {
|
|
243
359
|
let orderNo
|
|
244
360
|
|
|
@@ -34,6 +34,7 @@ import {getCreditCardIcon} from '@salesforce/retail-react-app/app/utils/cc-utils
|
|
|
34
34
|
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
|
|
35
35
|
import {isPickupShipment} from '@salesforce/retail-react-app/app/utils/shipment-utils'
|
|
36
36
|
import {areAddressesEqual} from '@salesforce/retail-react-app/app/utils/address-utils'
|
|
37
|
+
import {consolidateDuplicateBonusProducts} from '@salesforce/retail-react-app/app/utils/bonus-product/cart'
|
|
37
38
|
|
|
38
39
|
// Components
|
|
39
40
|
import Link from '@salesforce/retail-react-app/app/components/link'
|
|
@@ -332,52 +333,61 @@ const CheckoutConfirmation = () => {
|
|
|
332
333
|
width="full"
|
|
333
334
|
divider={<Divider />}
|
|
334
335
|
>
|
|
335
|
-
{
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
336
|
+
{(() => {
|
|
337
|
+
const consolidatedItems =
|
|
338
|
+
consolidateDuplicateBonusProducts(
|
|
339
|
+
order.productItems || []
|
|
340
|
+
)
|
|
341
|
+
return consolidatedItems.map((product, idx) => {
|
|
342
|
+
const productDetail =
|
|
343
|
+
productItemsMap?.[product.productId] ||
|
|
344
|
+
{}
|
|
345
|
+
const variant = {
|
|
346
|
+
...product,
|
|
347
|
+
...productDetail,
|
|
348
|
+
price: product.price
|
|
349
|
+
}
|
|
343
350
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
width="full"
|
|
352
|
-
alignItems="flex-start"
|
|
351
|
+
return (
|
|
352
|
+
<ItemVariantProvider
|
|
353
|
+
key={`${product.productId}-${
|
|
354
|
+
product.itemId || idx
|
|
355
|
+
}`}
|
|
356
|
+
index={idx}
|
|
357
|
+
variant={variant}
|
|
353
358
|
>
|
|
354
|
-
<
|
|
355
|
-
width="
|
|
356
|
-
|
|
357
|
-
/>
|
|
358
|
-
<Stack
|
|
359
|
-
spacing={1}
|
|
360
|
-
marginTop="-3px"
|
|
361
|
-
flex={1}
|
|
359
|
+
<Flex
|
|
360
|
+
width="full"
|
|
361
|
+
alignItems="flex-start"
|
|
362
362
|
>
|
|
363
|
-
<
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
363
|
+
<CartItemVariantImage
|
|
364
|
+
width="80px"
|
|
365
|
+
mr={2}
|
|
366
|
+
/>
|
|
367
|
+
<Stack
|
|
368
|
+
spacing={1}
|
|
369
|
+
marginTop="-3px"
|
|
370
|
+
flex={1}
|
|
368
371
|
>
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
372
|
+
<CartItemVariantName />
|
|
373
|
+
<Flex
|
|
374
|
+
width="full"
|
|
375
|
+
justifyContent="space-between"
|
|
376
|
+
alignItems="flex-end"
|
|
377
|
+
>
|
|
378
|
+
<CartItemVariantAttributes
|
|
379
|
+
includeQuantity
|
|
380
|
+
/>
|
|
381
|
+
<CartItemVariantPrice
|
|
382
|
+
currency={currency}
|
|
383
|
+
/>
|
|
384
|
+
</Flex>
|
|
385
|
+
</Stack>
|
|
386
|
+
</Flex>
|
|
387
|
+
</ItemVariantProvider>
|
|
388
|
+
)
|
|
389
|
+
})
|
|
390
|
+
})()}
|
|
381
391
|
</Stack>
|
|
382
392
|
)}
|
|
383
393
|
|
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
INVALID_TOKEN_ERROR,
|
|
32
32
|
INVALID_TOKEN_ERROR_MESSAGE,
|
|
33
33
|
FEATURE_UNAVAILABLE_ERROR_MESSAGE,
|
|
34
|
-
PASSWORDLESS_LOGIN_LANDING_PATH,
|
|
35
34
|
PASSWORDLESS_ERROR_MESSAGES
|
|
36
35
|
} from '@salesforce/retail-react-app/app/constants'
|
|
37
36
|
import {usePrevious} from '@salesforce/retail-react-app/app/hooks/use-previous'
|
|
@@ -61,6 +60,7 @@ const Login = ({initialView = LOGIN_VIEW}) => {
|
|
|
61
60
|
const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless)
|
|
62
61
|
const {passwordless = {}, social = {}} = getConfig().app.login || {}
|
|
63
62
|
const isPasswordlessEnabled = !!passwordless?.enabled
|
|
63
|
+
const passwordlessLoginLandingPath = passwordless?.landingPath
|
|
64
64
|
const isSocialEnabled = !!social?.enabled
|
|
65
65
|
const idps = social?.idps
|
|
66
66
|
|
|
@@ -147,7 +147,7 @@ const Login = ({initialView = LOGIN_VIEW}) => {
|
|
|
147
147
|
// executing a passwordless login attempt using the token. The process waits for the
|
|
148
148
|
// customer baskets to be loaded to guarantee proper basket merging.
|
|
149
149
|
useEffect(() => {
|
|
150
|
-
if (path
|
|
150
|
+
if (path.endsWith(passwordlessLoginLandingPath) && isSuccessCustomerBaskets) {
|
|
151
151
|
const token = decodeURIComponent(queryParams.get('token'))
|
|
152
152
|
if (queryParams.get('redirect_url')) {
|
|
153
153
|
setRedirectPath(decodeURIComponent(queryParams.get('redirect_url')))
|