@salesforce/retail-react-app 7.1.0-preview.0 → 8.0.0-dev

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.
Files changed (113) hide show
  1. package/CHANGELOG.md +8 -4
  2. package/app/components/_app/index.jsx +9 -7
  3. package/app/components/_app/index.test.js +2 -2
  4. package/app/components/_app-config/index.jsx +9 -3
  5. package/app/components/drawer-menu/drawer-menu.jsx +3 -1
  6. package/app/components/footer/index.jsx +3 -1
  7. package/app/components/header/index.jsx +3 -1
  8. package/app/components/header/index.test.js +2 -2
  9. package/app/components/island/README.md +1 -1
  10. package/app/components/island/index.jsx +3 -1
  11. package/app/components/island/index.test.js +94 -5
  12. package/app/components/item-variant/item-attributes.jsx +12 -3
  13. package/app/components/multiship/multiship-order-summary.jsx +137 -0
  14. package/app/components/multiship/multiship-order-summary.test.js +121 -0
  15. package/app/components/order-summary/index.jsx +2 -4
  16. package/app/components/pickup-or-delivery/index.jsx +80 -0
  17. package/app/components/pickup-or-delivery/index.test.jsx +182 -0
  18. package/app/components/product-item/index.jsx +26 -16
  19. package/app/components/product-item/index.test.js +29 -2
  20. package/app/components/product-item-list/index.jsx +10 -0
  21. package/app/components/product-item-list/index.test.jsx +14 -0
  22. package/app/components/product-view/index.jsx +9 -6
  23. package/app/components/product-view/index.test.js +25 -21
  24. package/app/components/quantity-picker/index.test.jsx +12 -12
  25. package/app/components/reset-password/index.test.js +1 -1
  26. package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
  27. package/app/components/shared/ui/index.jsx +1 -0
  28. package/app/components/store-display/index.jsx +28 -4
  29. package/app/components/store-display/index.test.js +71 -0
  30. package/app/components/store-locator/form.test.jsx +16 -4
  31. package/app/components/store-locator/list.jsx +9 -4
  32. package/app/components/toggle-card/index.jsx +14 -0
  33. package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
  34. package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
  35. package/app/constants.js +20 -6
  36. package/app/contexts/store-locator-provider.jsx +7 -1
  37. package/app/contexts/store-locator-provider.test.jsx +36 -1
  38. package/app/hooks/use-address-form.js +155 -0
  39. package/app/hooks/use-address-form.test.js +501 -0
  40. package/app/hooks/use-auth-modal.js +2 -6
  41. package/app/hooks/use-current-basket.js +71 -2
  42. package/app/hooks/use-current-basket.test.js +37 -1
  43. package/app/hooks/use-dnt-notification.js +4 -4
  44. package/app/hooks/use-dnt-notification.test.js +5 -5
  45. package/app/hooks/use-item-shipment-management.js +233 -0
  46. package/app/hooks/use-item-shipment-management.test.js +696 -0
  47. package/app/hooks/use-multiship.js +589 -0
  48. package/app/hooks/use-multiship.test.js +776 -0
  49. package/app/hooks/use-pickup-shipment.js +70 -106
  50. package/app/hooks/use-pickup-shipment.test.js +345 -209
  51. package/app/hooks/use-product-address-assignment.js +280 -0
  52. package/app/hooks/use-product-address-assignment.test.js +414 -0
  53. package/app/hooks/use-product-inventory.js +100 -0
  54. package/app/hooks/use-product-inventory.test.js +254 -0
  55. package/app/hooks/use-shipment-operations.js +168 -0
  56. package/app/hooks/use-shipment-operations.test.js +385 -0
  57. package/app/hooks/use-store-locator.js +24 -2
  58. package/app/hooks/use-store-locator.test.jsx +109 -1
  59. package/app/pages/account/index.test.js +1 -1
  60. package/app/pages/account/profile.test.js +0 -2
  61. package/app/pages/cart/index.jsx +397 -157
  62. package/app/pages/cart/index.test.js +353 -2
  63. package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
  64. package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
  65. package/app/pages/cart/partials/order-type-display.jsx +68 -0
  66. package/app/pages/cart/partials/order-type-display.test.js +241 -0
  67. package/app/pages/checkout/confirmation.jsx +79 -158
  68. package/app/pages/checkout/index.jsx +34 -9
  69. package/app/pages/checkout/index.test.js +245 -118
  70. package/app/pages/checkout/partials/contact-info.jsx +2 -6
  71. package/app/pages/checkout/partials/contact-info.test.js +93 -7
  72. package/app/pages/checkout/partials/payment.jsx +19 -5
  73. package/app/pages/checkout/partials/pickup-address.jsx +340 -70
  74. package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
  75. package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
  76. package/app/pages/checkout/partials/shipment-details.jsx +209 -0
  77. package/app/pages/checkout/partials/shipment-details.test.js +246 -0
  78. package/app/pages/checkout/partials/shipping-address.jsx +156 -68
  79. package/app/pages/checkout/partials/shipping-address.test.js +673 -0
  80. package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
  81. package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
  82. package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
  83. package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
  84. package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
  85. package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
  86. package/app/pages/checkout/util/checkout-context.js +25 -18
  87. package/app/pages/login/index.jsx +2 -6
  88. package/app/pages/product-detail/index.jsx +96 -81
  89. package/app/pages/product-detail/index.test.js +103 -19
  90. package/app/pages/product-list/index.jsx +3 -1
  91. package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
  92. package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
  93. package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
  94. package/app/ssr.js +1 -5
  95. package/app/static/translations/compiled/en-GB.json +316 -30
  96. package/app/static/translations/compiled/en-US.json +316 -30
  97. package/app/static/translations/compiled/en-XA.json +673 -75
  98. package/app/utils/address-utils.js +112 -0
  99. package/app/utils/address-utils.test.js +484 -0
  100. package/app/utils/product-utils.js +17 -5
  101. package/app/utils/product-utils.test.js +17 -8
  102. package/app/utils/sfdc-user-agent-utils.js +32 -0
  103. package/app/utils/sfdc-user-agent-utils.test.js +82 -0
  104. package/app/utils/shipment-utils.js +196 -0
  105. package/app/utils/shipment-utils.test.js +458 -0
  106. package/app/utils/test-utils.js +4 -4
  107. package/app/utils/utils.js +6 -1
  108. package/config/default.js +4 -1
  109. package/config/mocks/default.js +3 -1
  110. package/package.json +9 -9
  111. package/translations/en-GB.json +127 -10
  112. package/translations/en-US.json +127 -10
  113. package/app/pages/checkout/partials/shipping-options.jsx +0 -269
@@ -0,0 +1,259 @@
1
+ /*
2
+ * Copyright (c) 2025, 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
+ import React, {useState} from 'react'
8
+ import {useIntl} from 'react-intl'
9
+ import PropTypes from 'prop-types'
10
+ import {useProducts} from '@salesforce/commerce-sdk-react'
11
+ import {findImageGroupBy} from '@salesforce/retail-react-app/app/utils/image-groups-utils'
12
+ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
13
+ import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
14
+ import {
15
+ Text,
16
+ Button,
17
+ Box,
18
+ VStack,
19
+ Alert,
20
+ AlertIcon,
21
+ AlertTitle,
22
+ AlertDescription,
23
+ Center
24
+ } from '@salesforce/retail-react-app/app/components/shared/ui'
25
+ import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout/util/checkout-context'
26
+ import {useProductAddressAssignment} from '@salesforce/retail-react-app/app/hooks/use-product-address-assignment'
27
+ import {useAddressForm} from '@salesforce/retail-react-app/app/hooks/use-address-form'
28
+ import {useMultiship} from '@salesforce/retail-react-app/app/hooks/use-multiship'
29
+ import ProductShippingAddressCard from '@salesforce/retail-react-app/app/pages/checkout/partials/product-shipping-address-card.jsx'
30
+
31
+ const ShippingMultiAddress = ({basket, submitButtonLabel, noItemsInBasketMessage}) => {
32
+ const {formatMessage} = useIntl()
33
+ const {STEPS, goToStep} = useCheckout()
34
+ const showToast = useToast()
35
+ const productAddressAssignment = useProductAddressAssignment(basket)
36
+ const productIds = productAddressAssignment.deliveryItems
37
+ .map((item) => item.productId)
38
+ .join(',')
39
+
40
+ const {
41
+ data: productsMap,
42
+ isLoading: productsLoading,
43
+ error: productsError
44
+ } = useProducts(
45
+ {parameters: {ids: productIds, allImages: true}},
46
+ {
47
+ enabled: Boolean(productIds),
48
+ select: (data) => {
49
+ return (
50
+ data?.data?.reduce((acc, p) => {
51
+ acc[p.id] = p
52
+ return acc
53
+ }, {}) || {}
54
+ )
55
+ }
56
+ }
57
+ )
58
+ const {data: customer, isLoading: customerLoading} = useCurrentCustomer()
59
+
60
+ const {
61
+ form: addressForm,
62
+ formStateByItemId: showAddAddressForm,
63
+ isSubmitting: isFormSubmitting,
64
+ openForm,
65
+ closeForm,
66
+ handleCreateAddress,
67
+ isAddressFormOpen
68
+ } = useAddressForm(
69
+ productAddressAssignment.addGuestAddress,
70
+ customer?.isGuest,
71
+ productAddressAssignment.setAddressesForItems,
72
+ productAddressAssignment.availableAddresses,
73
+ productAddressAssignment.deliveryItems
74
+ )
75
+
76
+ const {orchestrateShipmentOperations} = useMultiship(basket)
77
+
78
+ const addresses = productAddressAssignment.availableAddresses
79
+ const [isSubmitting, setIsSubmitting] = useState(false)
80
+
81
+ // guests only products loading since they may not have addresses yet
82
+ const isLoading = (customer?.isGuest ? false : customerLoading) || productsLoading
83
+
84
+ const allShipmentsHaveAddress = productAddressAssignment.allItemsHaveAddresses
85
+
86
+ if (!productAddressAssignment.deliveryItems.length) {
87
+ return (
88
+ <Center
89
+ p={8}
90
+ textAlign="center"
91
+ color="gray.500"
92
+ role="status"
93
+ aria-live="polite"
94
+ aria-label={formatMessage(noItemsInBasketMessage)}
95
+ >
96
+ <VStack spacing={4}>
97
+ <Text fontSize="lg" fontWeight="medium">
98
+ {formatMessage(noItemsInBasketMessage)}
99
+ </Text>
100
+ </VStack>
101
+ </Center>
102
+ )
103
+ }
104
+
105
+ if (productsError) {
106
+ return (
107
+ <Alert
108
+ status="error"
109
+ variant="subtle"
110
+ flexDirection="column"
111
+ alignItems="center"
112
+ justifyContent="center"
113
+ textAlign="center"
114
+ height="200px"
115
+ aria-live="assertive"
116
+ >
117
+ <AlertIcon boxSize={5} mr={0} />
118
+ <AlertTitle mr={2}>
119
+ {formatMessage({
120
+ id: 'shipping_multi_address.error.label',
121
+ defaultMessage: 'Something went wrong while loading products.'
122
+ })}
123
+ </AlertTitle>
124
+ <AlertDescription>
125
+ {formatMessage({
126
+ id: 'shipping_multi_address.error.message',
127
+ defaultMessage: 'Something went wrong while loading products. Try again.'
128
+ })}
129
+ </AlertDescription>
130
+ </Alert>
131
+ )
132
+ }
133
+
134
+ if (isLoading) {
135
+ return (
136
+ <Center p={8} textAlign="center" color="gray.500">
137
+ <VStack spacing={4}>
138
+ <Text fontSize="lg" fontWeight="medium">
139
+ {formatMessage({
140
+ id: 'shipping_multi_address.loading.message',
141
+ defaultMessage: 'Loading...'
142
+ })}
143
+ </Text>
144
+ </VStack>
145
+ </Center>
146
+ )
147
+ }
148
+
149
+ const handleSubmit = async () => {
150
+ setIsSubmitting(true)
151
+ try {
152
+ await orchestrateShipmentOperations(
153
+ productAddressAssignment.deliveryItems,
154
+ productAddressAssignment.selectedAddresses,
155
+ addresses,
156
+ productsMap
157
+ )
158
+
159
+ goToStep(STEPS.SHIPPING_OPTIONS)
160
+ } catch (error) {
161
+ showToast({
162
+ title: formatMessage({
163
+ defaultMessage: 'Something went wrong while setting up shipments. Try again.',
164
+ id: 'shipping_multi_address.error.submit_failed'
165
+ }),
166
+ status: 'error'
167
+ })
168
+ } finally {
169
+ setIsSubmitting(false)
170
+ }
171
+ }
172
+
173
+ return (
174
+ <Box>
175
+ <VStack spacing={0}>
176
+ <Box
177
+ border="1px solid"
178
+ borderColor="gray.200"
179
+ borderRadius="md"
180
+ bg="white"
181
+ p={2}
182
+ w="100%"
183
+ >
184
+ <VStack spacing={2} w="100%" h="100%">
185
+ {productAddressAssignment.deliveryItems.map((item) => {
186
+ const productDetail = productsMap?.[item.productId] || {}
187
+ const variant = {...item, ...productDetail}
188
+ const image = findImageGroupBy(productDetail.imageGroups, {
189
+ viewType: 'small',
190
+ selectedVariationAttributes: variant.variationValues
191
+ })?.images?.[0]
192
+ const imageUrl = image?.disBaseLink || image?.link || ''
193
+ const addressKey = item.itemId
194
+
195
+ return (
196
+ <ProductShippingAddressCard
197
+ key={addressKey}
198
+ item={item}
199
+ variant={variant}
200
+ imageUrl={imageUrl}
201
+ addressKey={addressKey}
202
+ selectedAddressId={
203
+ productAddressAssignment.selectedAddresses[addressKey]
204
+ }
205
+ availableAddresses={addresses}
206
+ isGuestUser={customer?.isGuest}
207
+ customerLoading={customerLoading}
208
+ onAddressSelect={productAddressAssignment.setAddressesForItems}
209
+ onAddNewAddress={openForm}
210
+ showAddAddressForm={showAddAddressForm}
211
+ addressForm={addressForm}
212
+ handleCreateAddress={handleCreateAddress}
213
+ closeForm={closeForm}
214
+ />
215
+ )
216
+ })}
217
+ </VStack>
218
+ </Box>
219
+ <Button
220
+ type="button"
221
+ width="full"
222
+ mt={2}
223
+ opacity={!allShipmentsHaveAddress || isAddressFormOpen ? 0.8 : 1}
224
+ cursor={
225
+ !allShipmentsHaveAddress || isAddressFormOpen ? 'not-allowed' : 'pointer'
226
+ }
227
+ isLoading={
228
+ addressForm.formState.isSubmitting || isFormSubmitting || isSubmitting
229
+ }
230
+ isDisabled={!allShipmentsHaveAddress || isAddressFormOpen}
231
+ data-testid="continue-to-shipping-button"
232
+ loadingText={formatMessage({
233
+ id: 'shipping_multi_address.submit.loading',
234
+ defaultMessage: 'Setting up shipments...'
235
+ })}
236
+ aria-label={formatMessage({
237
+ id: 'shipping_multi_address.submit.description',
238
+ defaultMessage: 'Continue to next step with selected delivery addresses'
239
+ })}
240
+ onClick={() => {
241
+ if (!isAddressFormOpen && allShipmentsHaveAddress) {
242
+ handleSubmit()
243
+ }
244
+ }}
245
+ >
246
+ {formatMessage(submitButtonLabel)}
247
+ </Button>
248
+ </VStack>
249
+ </Box>
250
+ )
251
+ }
252
+
253
+ ShippingMultiAddress.propTypes = {
254
+ basket: PropTypes.object.isRequired,
255
+ submitButtonLabel: PropTypes.object.isRequired,
256
+ noItemsInBasketMessage: PropTypes.object.isRequired
257
+ }
258
+
259
+ export default ShippingMultiAddress