@salesforce/retail-react-app 8.3.0-nightly-20251209080224 → 8.3.0-preview.0
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 +2 -1
- package/app/components/address-suggestion-dropdown/index.jsx +189 -0
- package/app/components/address-suggestion-dropdown/index.test.jsx +332 -0
- package/app/components/forms/address-fields.jsx +18 -2
- package/app/components/forms/useAddressFields.jsx +139 -1
- package/app/components/forms/useAddressFields.test.js +310 -0
- package/app/hooks/useAutocompleteSuggestions.js +131 -0
- package/app/hooks/useAutocompleteSuggestions.test.js +296 -0
- package/app/mocks/mock-address-suggestions.js +445 -0
- package/app/pages/checkout/index.jsx +4 -2
- package/app/pages/checkout/index.test.js +206 -0
- package/app/pages/checkout/util/checkout-context.js +4 -1
- package/app/pages/checkout/util/google-api-provider.js +45 -0
- package/app/pages/checkout/util/google-api-provider.test.js +395 -0
- package/app/ssr.js +5 -1
- package/app/static/img/GoogleMaps_Logo_Gray_4x.png +0 -0
- package/app/static/translations/compiled/en-GB.json +6 -0
- package/app/static/translations/compiled/en-US.json +6 -0
- package/app/static/translations/compiled/en-XA.json +15 -1
- package/app/theme/components/project/swatch-group.js +1 -1
- package/app/utils/address-suggestions.js +237 -0
- package/config/default.js +4 -1
- package/package.json +9 -8
- package/translations/en-GB.json +3 -0
- package/translations/en-US.json +3 -0
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
const COUNTRY_CODE_MAP = {
|
|
9
|
+
USA: 'US',
|
|
10
|
+
Canada: 'CA'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve country code from country name
|
|
15
|
+
* @param {string} countryName - Full country name from address
|
|
16
|
+
* @returns {string} Standardized country code
|
|
17
|
+
*/
|
|
18
|
+
const resolveCountryCode = (countryName) => {
|
|
19
|
+
return COUNTRY_CODE_MAP[countryName] || countryName
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert Google Maps API suggestions to our expected format
|
|
24
|
+
* @param {Array} suggestions - Array of suggestions from Google Maps API
|
|
25
|
+
* @returns {Array} Converted suggestions in our expected format
|
|
26
|
+
*/
|
|
27
|
+
export const convertGoogleMapsSuggestions = (suggestions) => {
|
|
28
|
+
return suggestions.map((suggestion) => ({
|
|
29
|
+
description: suggestion.placePrediction.text.text,
|
|
30
|
+
place_id: suggestion.placePrediction.placeId,
|
|
31
|
+
structured_formatting: {
|
|
32
|
+
main_text:
|
|
33
|
+
suggestion.placePrediction.text.text.split(',')[0] ||
|
|
34
|
+
suggestion.placePrediction.text.text,
|
|
35
|
+
secondary_text: suggestion.placePrediction.text.text
|
|
36
|
+
.split(',')
|
|
37
|
+
.slice(1)
|
|
38
|
+
.join(',')
|
|
39
|
+
.trim()
|
|
40
|
+
},
|
|
41
|
+
terms: suggestion.placePrediction.text.text
|
|
42
|
+
.split(',')
|
|
43
|
+
.map((term) => ({value: term.trim()})),
|
|
44
|
+
placePrediction: suggestion.placePrediction
|
|
45
|
+
}))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse address suggestion data to extract individual address fields
|
|
50
|
+
* @param {Object} suggestion - Address suggestion object from the API
|
|
51
|
+
* @returns {Object} Parsed address fields
|
|
52
|
+
*/
|
|
53
|
+
export const parseAddressSuggestion = async (suggestion) => {
|
|
54
|
+
const {structured_formatting, terms} = suggestion
|
|
55
|
+
const {main_text, secondary_text} = structured_formatting
|
|
56
|
+
|
|
57
|
+
const parsedFields = {
|
|
58
|
+
address1: main_text
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const countryTerm = terms[terms.length - 1]?.value || ''
|
|
62
|
+
parsedFields.countryCode = resolveCountryCode(countryTerm)
|
|
63
|
+
|
|
64
|
+
if (!secondary_text) {
|
|
65
|
+
return parsedFields
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* Parse secondary text to extract city, state, and postal code
|
|
70
|
+
* Format examples:
|
|
71
|
+
* "New York, NY 10001, USA"
|
|
72
|
+
* "Toronto, ON M5C 1W4, Canada"
|
|
73
|
+
* "London, UK NW1 6XE"
|
|
74
|
+
* "New York" (single part)
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
const parts = secondary_text.split(',')
|
|
78
|
+
|
|
79
|
+
if (parts.length >= 2) {
|
|
80
|
+
// Extract city (first part)
|
|
81
|
+
parsedFields.city = parts[0].trim()
|
|
82
|
+
|
|
83
|
+
// Extract state and postal code (second part)
|
|
84
|
+
const statePostalPart = parts[1].trim()
|
|
85
|
+
|
|
86
|
+
const statePostalMatch = statePostalPart.match(/^([A-Z]{2})\s+([A-Z0-9\s]+)$/)
|
|
87
|
+
|
|
88
|
+
if (statePostalMatch) {
|
|
89
|
+
parsedFields.stateCode = statePostalMatch[1]
|
|
90
|
+
parsedFields.postalCode = statePostalMatch[2].trim()
|
|
91
|
+
} else {
|
|
92
|
+
// If no state/postal pattern, just use the part as state
|
|
93
|
+
parsedFields.stateCode = statePostalPart
|
|
94
|
+
}
|
|
95
|
+
} else if (parts.length === 1) {
|
|
96
|
+
// Single part - could be just city or just state
|
|
97
|
+
const singlePart = parts[0].trim()
|
|
98
|
+
const stateMatch = singlePart.match(/^[A-Z]{2}$/)
|
|
99
|
+
|
|
100
|
+
if (stateMatch) {
|
|
101
|
+
parsedFields.stateCode = singlePart
|
|
102
|
+
} else {
|
|
103
|
+
parsedFields.city = singlePart
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return parsedFields
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extract address fields from Google Maps place and return structured object
|
|
112
|
+
* @param {Object} place - Google Maps place object
|
|
113
|
+
* @returns {Promise<Object>} Structured address fields
|
|
114
|
+
*/
|
|
115
|
+
export const extractAddressFieldsFromPlace = async (place) => {
|
|
116
|
+
await place.fetchFields({
|
|
117
|
+
fields: ['formattedAddress']
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const formattedAddress = place.formattedAddress || ''
|
|
121
|
+
|
|
122
|
+
// Parse the formatted address to extract individual fields
|
|
123
|
+
return parseFormattedAddress(formattedAddress)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse formatted address string to extract individual address fields
|
|
128
|
+
* @param {string} formattedAddress - Full formatted address string
|
|
129
|
+
* @returns {Object} Structured address fields following adr microformat
|
|
130
|
+
*/
|
|
131
|
+
export const parseFormattedAddress = (formattedAddress) => {
|
|
132
|
+
if (!formattedAddress) {
|
|
133
|
+
return {address1: ''}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Split by comma
|
|
137
|
+
const parts = formattedAddress.split(',').map((part) => part.trim())
|
|
138
|
+
|
|
139
|
+
// Initialize with microformat structure following adr specification
|
|
140
|
+
const addressFields = {
|
|
141
|
+
'street-address': parts[0] || '', // street-address (adr microformat)
|
|
142
|
+
locality: '', // locality (adr microformat)
|
|
143
|
+
region: '', // region (adr microformat)
|
|
144
|
+
'postal-code': '', // postal-code (adr microformat)
|
|
145
|
+
'country-name': '' // country-name (adr microformat)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Map parts to microformat fields based on adr specification
|
|
149
|
+
if (parts.length >= 4) {
|
|
150
|
+
// Format: "123 Main St, New York, NY 10001, USA" OR "123 Main St, New York, CA, USA"
|
|
151
|
+
addressFields['locality'] = parts[1] // City
|
|
152
|
+
const statePostalPart = parts[2]
|
|
153
|
+
const statePostalSplit = statePostalPart.split(' ')
|
|
154
|
+
|
|
155
|
+
if (statePostalSplit.length >= 2) {
|
|
156
|
+
// Has both state and postal code
|
|
157
|
+
addressFields['region'] = statePostalSplit[0] // State (first part)
|
|
158
|
+
addressFields['postal-code'] = statePostalSplit.slice(1).join(' ') // Postal code
|
|
159
|
+
} else {
|
|
160
|
+
// Just state code
|
|
161
|
+
addressFields['region'] = statePostalPart
|
|
162
|
+
}
|
|
163
|
+
addressFields['country-name'] = parts[3]
|
|
164
|
+
} else if (parts.length === 3) {
|
|
165
|
+
// Format: "123 Main St, New York, NY" or "123 Main St, New York, USA"
|
|
166
|
+
addressFields['locality'] = parts[1]
|
|
167
|
+
const lastPart = parts[2]
|
|
168
|
+
|
|
169
|
+
if (COUNTRY_CODE_MAP[lastPart]) {
|
|
170
|
+
addressFields['country-name'] = lastPart
|
|
171
|
+
} else {
|
|
172
|
+
// Parse state and postal code
|
|
173
|
+
const statePostalSplit = lastPart.split(' ')
|
|
174
|
+
if (statePostalSplit.length >= 2) {
|
|
175
|
+
addressFields['region'] = statePostalSplit[0]
|
|
176
|
+
addressFields['postal-code'] = statePostalSplit.slice(1).join(' ')
|
|
177
|
+
} else {
|
|
178
|
+
addressFields['region'] = lastPart
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} else if (parts.length === 2) {
|
|
182
|
+
// Format: "123 Main St, New York"
|
|
183
|
+
addressFields['locality'] = parts[1]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Convert microformat fields to expected format
|
|
187
|
+
return {
|
|
188
|
+
address1: addressFields['street-address'],
|
|
189
|
+
city: addressFields['locality'],
|
|
190
|
+
stateCode: addressFields['region'],
|
|
191
|
+
postalCode: addressFields['postal-code'],
|
|
192
|
+
countryCode: resolveCountryCode(addressFields['country-name'])
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Set address field values in form
|
|
198
|
+
* @param {Function} setValue - Form setValue function
|
|
199
|
+
* @param {string} prefix - Field prefix
|
|
200
|
+
* @param {Object} addressFields - Address fields object
|
|
201
|
+
*/
|
|
202
|
+
export const setAddressFieldValues = (setValue, prefix, addressFields) => {
|
|
203
|
+
setValue(`${prefix}address1`, addressFields.address1)
|
|
204
|
+
if (addressFields.city) {
|
|
205
|
+
setValue(`${prefix}city`, addressFields.city)
|
|
206
|
+
}
|
|
207
|
+
if (addressFields.stateCode) {
|
|
208
|
+
setValue(`${prefix}stateCode`, addressFields.stateCode)
|
|
209
|
+
}
|
|
210
|
+
if (addressFields.postalCode) {
|
|
211
|
+
setValue(`${prefix}postalCode`, addressFields.postalCode)
|
|
212
|
+
}
|
|
213
|
+
if (addressFields.countryCode) {
|
|
214
|
+
setValue(`${prefix}countryCode`, addressFields.countryCode)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Process address suggestion and extract structured address fields
|
|
220
|
+
* This unified method handles both placePrediction.toPlace() and fallback scenarios
|
|
221
|
+
* @param {Object} suggestion - Address suggestion object from the API
|
|
222
|
+
* @returns {Promise<Object>} Structured address fields
|
|
223
|
+
*/
|
|
224
|
+
export const processAddressSuggestion = async (suggestion) => {
|
|
225
|
+
let addressFields
|
|
226
|
+
|
|
227
|
+
// If we have the placePrediction, get detailed place information using toPlace()
|
|
228
|
+
if (suggestion.placePrediction) {
|
|
229
|
+
const place = suggestion.placePrediction.toPlace()
|
|
230
|
+
addressFields = await extractAddressFieldsFromPlace(place)
|
|
231
|
+
} else {
|
|
232
|
+
// Fallback to parsing from structured_formatting when placePrediction is not available
|
|
233
|
+
addressFields = await parseAddressSuggestion(suggestion)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return addressFields
|
|
237
|
+
}
|
package/config/default.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/retail-react-app",
|
|
3
|
-
"version": "8.3.0-
|
|
3
|
+
"version": "8.3.0-preview.0",
|
|
4
4
|
"license": "See license in LICENSE",
|
|
5
5
|
"author": "cc-pwa-kit@salesforce.com",
|
|
6
6
|
"ccExtensibility": {
|
|
@@ -46,16 +46,17 @@
|
|
|
46
46
|
"@loadable/component": "^5.15.3",
|
|
47
47
|
"@peculiar/webcrypto": "^1.4.2",
|
|
48
48
|
"@salesforce/cc-datacloud-typescript": "1.1.2",
|
|
49
|
-
"@salesforce/commerce-sdk-react": "4.3.0-
|
|
50
|
-
"@salesforce/pwa-kit-dev": "3.15.0-
|
|
51
|
-
"@salesforce/pwa-kit-react-sdk": "3.15.0-
|
|
52
|
-
"@salesforce/pwa-kit-runtime": "3.15.0-
|
|
49
|
+
"@salesforce/commerce-sdk-react": "4.3.0-preview.0",
|
|
50
|
+
"@salesforce/pwa-kit-dev": "3.15.0-preview.0",
|
|
51
|
+
"@salesforce/pwa-kit-react-sdk": "3.15.0-preview.0",
|
|
52
|
+
"@salesforce/pwa-kit-runtime": "3.15.0-preview.0",
|
|
53
53
|
"@tanstack/react-query": "^4.28.0",
|
|
54
54
|
"@tanstack/react-query-devtools": "^4.29.1",
|
|
55
55
|
"@testing-library/dom": "^9.0.1",
|
|
56
56
|
"@testing-library/jest-dom": "^5.16.5",
|
|
57
57
|
"@testing-library/react": "^14.0.0",
|
|
58
58
|
"@testing-library/user-event": "14.4.3",
|
|
59
|
+
"@vis.gl/react-google-maps": "^1.5.4",
|
|
59
60
|
"babel-plugin-module-resolver": "5.0.2",
|
|
60
61
|
"base64-arraybuffer": "^0.2.0",
|
|
61
62
|
"bundlesize2": "^0.0.35",
|
|
@@ -100,12 +101,12 @@
|
|
|
100
101
|
"bundlesize": [
|
|
101
102
|
{
|
|
102
103
|
"path": "build/main.js",
|
|
103
|
-
"maxSize": "
|
|
104
|
+
"maxSize": "86 kB"
|
|
104
105
|
},
|
|
105
106
|
{
|
|
106
107
|
"path": "build/vendor.js",
|
|
107
|
-
"maxSize": "
|
|
108
|
+
"maxSize": "363 kB"
|
|
108
109
|
}
|
|
109
110
|
],
|
|
110
|
-
"gitHead": "
|
|
111
|
+
"gitHead": "40542ae8754e3274bc7910e89e57c96089e2d22a"
|
|
111
112
|
}
|
package/translations/en-GB.json
CHANGED
|
@@ -162,6 +162,9 @@
|
|
|
162
162
|
"add_to_cart_modal.recommended_products.title.might_also_like": {
|
|
163
163
|
"defaultMessage": "You Might Also Like"
|
|
164
164
|
},
|
|
165
|
+
"addressSuggestionDropdown.suggested": {
|
|
166
|
+
"defaultMessage": "SUGGESTED"
|
|
167
|
+
},
|
|
165
168
|
"auth_modal.button.close.assistive_msg": {
|
|
166
169
|
"defaultMessage": "Close login form"
|
|
167
170
|
},
|
package/translations/en-US.json
CHANGED
|
@@ -162,6 +162,9 @@
|
|
|
162
162
|
"add_to_cart_modal.recommended_products.title.might_also_like": {
|
|
163
163
|
"defaultMessage": "You Might Also Like"
|
|
164
164
|
},
|
|
165
|
+
"addressSuggestionDropdown.suggested": {
|
|
166
|
+
"defaultMessage": "SUGGESTED"
|
|
167
|
+
},
|
|
165
168
|
"auth_modal.button.close.assistive_msg": {
|
|
166
169
|
"defaultMessage": "Close login form"
|
|
167
170
|
},
|