@salesforce/retail-react-app 7.1.0-preview.1 → 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 -2
  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 -1
  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,32 @@
1
+ /*
2
+ * Copyright (c) 2025, 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
+
8
+ /**
9
+ * Generates the sfdc_user_agent header value containing SDK version information.
10
+ * This header helps identify and track SDK versions invoking SCAPI for debugging
11
+ * and metrics purposes.
12
+ *
13
+ * @returns {string} The sfdc_user_agent header value in format: pwa-kit-react-sdk@version commerce-sdk-react@version
14
+ */
15
+ export const generateSfdcUserAgent = () => {
16
+ try {
17
+ // Using require here because this runs at initialization time when version info is static
18
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
19
+ const retailAppPkg = require('../../package.json')
20
+
21
+ const commerceSdkVersion =
22
+ retailAppPkg.dependencies?.['@salesforce/commerce-sdk-react'] || 'unknown'
23
+ const pwaKitVersion =
24
+ retailAppPkg.dependencies?.['@salesforce/pwa-kit-react-sdk'] || 'unknown'
25
+
26
+ // Using @ format to align with NPM package@version conventions for better tooling compatibility
27
+ return `pwa-kit-react-sdk@${pwaKitVersion} commerce-sdk-react@${commerceSdkVersion}`.trim()
28
+ } catch (error) {
29
+ console.warn('Unable to generate sfdc_user_agent header:', error)
30
+ return 'pwa-kit-react-sdk@unknown commerce-sdk-react@unknown'
31
+ }
32
+ }
@@ -0,0 +1,82 @@
1
+ /*
2
+ * Copyright (c) 2025, 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
+
8
+ import {generateSfdcUserAgent} from '@salesforce/retail-react-app/app/utils/sfdc-user-agent-utils'
9
+
10
+ describe('sfdc-user-agent-utils', () => {
11
+ describe('generateSfdcUserAgent', () => {
12
+ test('should generate correct sfdc_user_agent header value', () => {
13
+ const userAgent = generateSfdcUserAgent()
14
+
15
+ expect(userAgent).toMatch(/^pwa-kit-react-sdk@[\d\w.-]+ commerce-sdk-react@[\d\w.-]+$/)
16
+ expect(userAgent).toContain('pwa-kit-react-sdk@')
17
+ expect(userAgent).toContain('commerce-sdk-react@')
18
+ })
19
+
20
+ test('should return proper NPM-style package@version format', () => {
21
+ const userAgent = generateSfdcUserAgent()
22
+
23
+ const parts = userAgent.split(' ')
24
+ expect(parts).toHaveLength(2)
25
+
26
+ parts.forEach((part) => {
27
+ expect(part).toMatch(/^[\w-]+@[\d\w.-]+$/)
28
+ expect(part).toContain('@')
29
+ })
30
+ })
31
+
32
+ test('should not return null or undefined', () => {
33
+ const userAgent = generateSfdcUserAgent()
34
+
35
+ expect(userAgent).toBeDefined()
36
+ expect(userAgent).not.toBeNull()
37
+ expect(typeof userAgent).toBe('string')
38
+ expect(userAgent.length).toBeGreaterThan(0)
39
+ })
40
+
41
+ test('should handle error cases gracefully', () => {
42
+ expect(() => generateSfdcUserAgent()).not.toThrow()
43
+
44
+ const userAgent = generateSfdcUserAgent()
45
+ expect(userAgent).toBeDefined()
46
+ expect(typeof userAgent).toBe('string')
47
+ expect(userAgent.length).toBeGreaterThan(0)
48
+ })
49
+
50
+ test('should return valid package identifiers', () => {
51
+ // This test verifies that the function returns the expected package identifiers
52
+ const userAgent = generateSfdcUserAgent()
53
+
54
+ // Should always return a valid string
55
+ expect(typeof userAgent).toBe('string')
56
+ expect(userAgent.length).toBeGreaterThan(0)
57
+
58
+ // Should contain expected package identifiers
59
+ expect(userAgent).toMatch(/pwa-kit-react-sdk@/)
60
+ expect(userAgent).toMatch(/commerce-sdk-react@/)
61
+ })
62
+
63
+ test('should be valid HTTP header value', () => {
64
+ const userAgent = generateSfdcUserAgent()
65
+
66
+ // HTTP headers must contain only ASCII printable characters
67
+ const isValidHTTPHeader = /^[\x20-\x7E]*$/.test(userAgent)
68
+ expect(isValidHTTPHeader).toBe(true)
69
+
70
+ // Prevent excessively long headers that might cause issues with proxies/servers
71
+ expect(userAgent.length).toBeLessThan(500)
72
+ })
73
+
74
+ test('should use @ symbol for NPM convention alignment', () => {
75
+ const userAgent = generateSfdcUserAgent()
76
+
77
+ // Ensure both packages use @ format for consistency with NPM tooling
78
+ const atSymbolCount = (userAgent.match(/@/g) || []).length
79
+ expect(atSymbolCount).toBe(2)
80
+ })
81
+ })
82
+ })
@@ -0,0 +1,196 @@
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
+
8
+ import {
9
+ cleanAddressForOrder,
10
+ areAddressesEqual,
11
+ isAddressEmpty
12
+ } from '@salesforce/retail-react-app/app/utils/address-utils'
13
+ import {DEFAULT_SHIPMENT_ID} from '@salesforce/retail-react-app/app/constants'
14
+
15
+ /**
16
+ * Pure utility functions for shipment operations
17
+ * No side effects, easily testable
18
+ */
19
+
20
+ /**
21
+ * Checks if a shipping method is a pickup method
22
+ * @param {Object} shippingMethod - The shipping method object
23
+ * @returns {boolean} True if the shipping method is a pickup method
24
+ */
25
+ export const isPickupMethod = (shippingMethod) => {
26
+ return shippingMethod?.c_storePickupEnabled === true
27
+ }
28
+
29
+ /**
30
+ * Checks if a shipment is configured for pickup-in-store
31
+ * @param {object} shipment the shipment to check. can be null.
32
+ * @returns {boolean} true if the shipment is configured for pickup-in-store.
33
+ */
34
+ export const isPickupShipment = (shipment) => {
35
+ return isPickupMethod(shipment?.shippingMethod)
36
+ }
37
+
38
+ /**
39
+ * Gets items that belong to a specific shipment
40
+ * @param {Object} basket - The basket object
41
+ * @param {string} shipmentId - The shipment ID
42
+ * @returns {Array} Array of product items
43
+ */
44
+ export const getItemsForShipment = (basket, shipmentId) => {
45
+ if (!basket?.productItems || !shipmentId) return []
46
+ return basket.productItems.filter((item) => item.shipmentId === shipmentId)
47
+ }
48
+
49
+ /**
50
+ * Finds shipments that have no items assigned to them
51
+ * @param {Object} basket - The basket object
52
+ * @returns {Array} Array of empty shipments
53
+ */
54
+ export const findEmptyShipments = (basket) => {
55
+ if (!basket?.shipments?.length) {
56
+ return []
57
+ }
58
+
59
+ return basket.shipments.filter((shipment) => {
60
+ const hasItems = basket.productItems?.some(
61
+ (item) => item.shipmentId === shipment.shipmentId
62
+ )
63
+ return !hasItems
64
+ })
65
+ }
66
+
67
+ /**
68
+ * Groups items by their address using a provided function to get the address for each item
69
+ * @param {Array} items - Array of items to group
70
+ * @param {Function} getAddressForItem - Function that returns the address for a given item
71
+ * @returns {Object} Object with addresses as keys and arrays of items as values
72
+ */
73
+ export const groupItemsByAddress = (items, getAddressForItem) => {
74
+ if (!items?.length || typeof getAddressForItem !== 'function') {
75
+ return {}
76
+ }
77
+
78
+ return items.reduce((groups, item) => {
79
+ const address = getAddressForItem(item)
80
+ if (!address) return groups
81
+
82
+ // Create a key for the address
83
+ const addressKey = JSON.stringify(cleanAddressForOrder(address))
84
+
85
+ if (!groups[addressKey]) {
86
+ groups[addressKey] = []
87
+ }
88
+ groups[addressKey].push(item)
89
+
90
+ return groups
91
+ }, {})
92
+ }
93
+
94
+ /**
95
+ * Finds the first existing delivery shipment (not pickup)
96
+ * @param {Object} basket - The basket object
97
+ * @returns {Object|null} The delivery shipment object or null if not found
98
+ */
99
+ export const findExistingDeliveryShipment = (basket) => {
100
+ if (!basket?.shipments) return null
101
+
102
+ return basket.shipments.find((shipment) => !isPickupMethod(shipment.shippingMethod)) || null
103
+ }
104
+
105
+ /**
106
+ * Finds the first existing pickup shipment for a specific store
107
+ * @param {Object} basket - The basket object
108
+ * @param {string} storeId - The store ID to search for
109
+ * @returns {Object|null} The pickup shipment object or null if not found
110
+ */
111
+ export const findExistingPickupShipment = (basket, storeId) => {
112
+ if (!basket?.shipments || !storeId) return null
113
+
114
+ return (
115
+ basket.shipments.find(
116
+ (shipment) =>
117
+ isPickupMethod(shipment.shippingMethod) && shipment.c_fromStoreId === storeId
118
+ ) || null
119
+ )
120
+ }
121
+
122
+ /**
123
+ * Finds the first delivery shipment that is not in the provided list of shipment IDs
124
+ * @param {Object} basket - The basket object
125
+ * @param {Array} usedShipmentIds - Array of shipment IDs to exclude from search
126
+ * @returns {Object|null} The unused delivery shipment object or null if not found
127
+ */
128
+ export const findUnusedDeliveryShipment = (basket, usedShipmentIds = []) => {
129
+ if (!basket?.shipments) return null
130
+
131
+ return (
132
+ basket.shipments.find(
133
+ (shipment) =>
134
+ !isPickupMethod(shipment.shippingMethod) &&
135
+ !usedShipmentIds.includes(shipment.shipmentId)
136
+ ) || null
137
+ )
138
+ }
139
+
140
+ /**
141
+ * Finds the first existing delivery shipment with matching address
142
+ * @param {Object} basket - The basket object
143
+ * @param {Object} address - The address to match
144
+ * @returns {Object|null} The shipment object with matching address or null if not found
145
+ */
146
+ export const findDeliveryShipmentWithSameAddress = (basket, address) => {
147
+ if (!basket?.shipments || !address) return null
148
+
149
+ const foundShipment = basket.shipments.find((shipment) => {
150
+ // Must be a delivery shipment (not pickup)
151
+ if (isPickupMethod(shipment.shippingMethod)) {
152
+ return false
153
+ }
154
+
155
+ // Check if shipment has a shipping address that matches
156
+ return shipment.shippingAddress && areAddressesEqual(shipment.shippingAddress, address)
157
+ })
158
+ return foundShipment || null
159
+ }
160
+
161
+ /**
162
+ * Finds the best non-empty shipment to consolidate into the default shipment
163
+ * @param {Object} basket - The basket object
164
+ * @returns {Object|null} The shipment to consolidate or null if none found
165
+ */
166
+ export const findShipmentToConsolidate = (basket) => {
167
+ if (!basket?.shipments?.length) {
168
+ return null
169
+ }
170
+
171
+ return (
172
+ basket.shipments.find((shipment) => {
173
+ const hasItems = basket.productItems?.some(
174
+ (item) => item.shipmentId === shipment.shipmentId
175
+ )
176
+ return hasItems && shipment.shipmentId !== DEFAULT_SHIPMENT_ID
177
+ }) || null
178
+ )
179
+ }
180
+
181
+ /**
182
+ * Checks if the default shipment is empty
183
+ * @param {Object} basket - The basket object
184
+ * @returns {boolean} True if the default shipment is empty
185
+ */
186
+ export const isDefaultShipmentEmpty = (basket) => {
187
+ if (!basket?.shipments) return true
188
+
189
+ const defaultShipment = basket.shipments.find(
190
+ (shipment) => shipment.shipmentId === DEFAULT_SHIPMENT_ID
191
+ )
192
+
193
+ if (!defaultShipment) return true
194
+
195
+ return !basket.productItems?.some((item) => item.shipmentId === DEFAULT_SHIPMENT_ID)
196
+ }