@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.
- package/CHANGELOG.md +8 -2
- package/app/components/_app/index.jsx +9 -7
- package/app/components/_app/index.test.js +2 -2
- package/app/components/_app-config/index.jsx +9 -3
- package/app/components/drawer-menu/drawer-menu.jsx +3 -1
- package/app/components/footer/index.jsx +3 -1
- package/app/components/header/index.jsx +3 -1
- package/app/components/header/index.test.js +2 -2
- package/app/components/island/README.md +1 -1
- package/app/components/island/index.jsx +3 -1
- package/app/components/island/index.test.js +94 -5
- package/app/components/item-variant/item-attributes.jsx +12 -3
- package/app/components/multiship/multiship-order-summary.jsx +137 -0
- package/app/components/multiship/multiship-order-summary.test.js +121 -0
- package/app/components/order-summary/index.jsx +2 -4
- package/app/components/pickup-or-delivery/index.jsx +80 -0
- package/app/components/pickup-or-delivery/index.test.jsx +182 -0
- package/app/components/product-item/index.jsx +26 -16
- package/app/components/product-item/index.test.js +29 -2
- package/app/components/product-item-list/index.jsx +10 -0
- package/app/components/product-item-list/index.test.jsx +14 -0
- package/app/components/product-view/index.jsx +9 -6
- package/app/components/product-view/index.test.js +25 -21
- package/app/components/quantity-picker/index.test.jsx +12 -12
- package/app/components/reset-password/index.test.js +1 -1
- package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
- package/app/components/shared/ui/index.jsx +1 -0
- package/app/components/store-display/index.jsx +28 -4
- package/app/components/store-display/index.test.js +71 -0
- package/app/components/store-locator/form.test.jsx +16 -4
- package/app/components/store-locator/list.jsx +9 -4
- package/app/components/toggle-card/index.jsx +14 -0
- package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
- package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
- package/app/constants.js +20 -6
- package/app/contexts/store-locator-provider.jsx +7 -1
- package/app/contexts/store-locator-provider.test.jsx +36 -1
- package/app/hooks/use-address-form.js +155 -0
- package/app/hooks/use-address-form.test.js +501 -0
- package/app/hooks/use-auth-modal.js +2 -6
- package/app/hooks/use-current-basket.js +71 -2
- package/app/hooks/use-current-basket.test.js +37 -1
- package/app/hooks/use-dnt-notification.js +4 -4
- package/app/hooks/use-dnt-notification.test.js +5 -5
- package/app/hooks/use-item-shipment-management.js +233 -0
- package/app/hooks/use-item-shipment-management.test.js +696 -0
- package/app/hooks/use-multiship.js +589 -0
- package/app/hooks/use-multiship.test.js +776 -0
- package/app/hooks/use-pickup-shipment.js +70 -106
- package/app/hooks/use-pickup-shipment.test.js +345 -209
- package/app/hooks/use-product-address-assignment.js +280 -0
- package/app/hooks/use-product-address-assignment.test.js +414 -0
- package/app/hooks/use-product-inventory.js +100 -0
- package/app/hooks/use-product-inventory.test.js +254 -0
- package/app/hooks/use-shipment-operations.js +168 -0
- package/app/hooks/use-shipment-operations.test.js +385 -0
- package/app/hooks/use-store-locator.js +24 -2
- package/app/hooks/use-store-locator.test.jsx +109 -1
- package/app/pages/account/index.test.js +1 -1
- package/app/pages/account/profile.test.js +0 -2
- package/app/pages/cart/index.jsx +397 -157
- package/app/pages/cart/index.test.js +353 -2
- package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
- package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
- package/app/pages/cart/partials/order-type-display.jsx +68 -0
- package/app/pages/cart/partials/order-type-display.test.js +241 -0
- package/app/pages/checkout/confirmation.jsx +79 -158
- package/app/pages/checkout/index.jsx +34 -9
- package/app/pages/checkout/index.test.js +245 -118
- package/app/pages/checkout/partials/contact-info.jsx +2 -6
- package/app/pages/checkout/partials/contact-info.test.js +93 -7
- package/app/pages/checkout/partials/payment.jsx +19 -5
- package/app/pages/checkout/partials/pickup-address.jsx +340 -70
- package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
- package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
- package/app/pages/checkout/partials/shipment-details.jsx +209 -0
- package/app/pages/checkout/partials/shipment-details.test.js +246 -0
- package/app/pages/checkout/partials/shipping-address.jsx +156 -68
- package/app/pages/checkout/partials/shipping-address.test.js +673 -0
- package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
- package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
- package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
- package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
- package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
- package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
- package/app/pages/checkout/util/checkout-context.js +25 -18
- package/app/pages/login/index.jsx +2 -6
- package/app/pages/product-detail/index.jsx +96 -81
- package/app/pages/product-detail/index.test.js +103 -19
- package/app/pages/product-list/index.jsx +3 -1
- package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
- package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
- package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
- package/app/ssr.js +1 -1
- package/app/static/translations/compiled/en-GB.json +316 -30
- package/app/static/translations/compiled/en-US.json +316 -30
- package/app/static/translations/compiled/en-XA.json +673 -75
- package/app/utils/address-utils.js +112 -0
- package/app/utils/address-utils.test.js +484 -0
- package/app/utils/product-utils.js +17 -5
- package/app/utils/product-utils.test.js +17 -8
- package/app/utils/sfdc-user-agent-utils.js +32 -0
- package/app/utils/sfdc-user-agent-utils.test.js +82 -0
- package/app/utils/shipment-utils.js +196 -0
- package/app/utils/shipment-utils.test.js +458 -0
- package/app/utils/test-utils.js +4 -4
- package/app/utils/utils.js +6 -1
- package/config/default.js +4 -1
- package/config/mocks/default.js +3 -1
- package/package.json +9 -9
- package/translations/en-GB.json +127 -10
- package/translations/en-US.json +127 -10
- 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
|
+
}
|