Skip to content

Commit 7b3d142

Browse files
@W-18902360 @W-18902533 BOPIS less content switch with store selection
1 parent c59c6ea commit 7b3d142

File tree

6 files changed

+99
-51
lines changed

6 files changed

+99
-51
lines changed

packages/template-retail-react-app/app/components/store-locator/form.jsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,14 @@ import {useForm, Controller} from 'react-hook-form'
1919
import {useStoreLocator} from '@salesforce/retail-react-app/app/hooks/use-store-locator'
2020
import {useGeolocation} from '@salesforce/retail-react-app/app/hooks/use-geo-location'
2121
import {useSelectedStore} from '@salesforce/retail-react-app/app/hooks/use-selected-store'
22-
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
2322

2423
export const StoreLocatorForm = () => {
2524
const {config, formValues, setFormValues, setDeviceCoordinates} = useStoreLocator()
2625
const {coordinates, error, refresh} = useGeolocation()
2726
const {store: selectedStore} = useSelectedStore()
28-
const {derivedData} = useCurrentBasket()
2927
const initialLoadDone = useRef(false)
3028
const shouldUseLocation = useRef(false)
3129

32-
const hasItemsInBasket = derivedData?.totalItems > 0
33-
3430
const form = useForm({
3531
mode: 'onChange',
3632
reValidateMode: 'onChange',
@@ -88,7 +84,7 @@ export const StoreLocatorForm = () => {
8884
void form.handleSubmit(submitForm)(e)
8985
}}
9086
>
91-
<Box as="fieldset" disabled={hasItemsInBasket} opacity={hasItemsInBasket ? 0.5 : 1}>
87+
<Box as="fieldset">
9288
<InputGroup>
9389
{showCountrySelector && (
9490
<Controller

packages/template-retail-react-app/app/components/store-locator/list.jsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ export const StoreLocatorList = () => {
1818
const {store: selectedStore} = useSelectedStore()
1919
const {derivedData} = useCurrentBasket()
2020
const [page, setPage] = useState(1)
21+
const [initialSelectedStoreId, setInitialSelectedStoreId] = useState(selectedStoreId)
2122

2223
const hasItemsInBasket = derivedData?.totalItems > 0
2324

2425
useEffect(() => {
2526
setPage(1)
27+
// Capture the selected store on each page load
28+
setInitialSelectedStoreId(selectedStoreId)
2629
}, [data])
2730

2831
const handleChange = (selectedStoreId) => {
@@ -36,7 +39,7 @@ export const StoreLocatorList = () => {
3639
if (!data?.data?.length && !selectedStore)
3740
return 'Sorry, there are no locations in this area'
3841
if (hasItemsInBasket) {
39-
return 'Sorry, you have items in your basket. Please remove them to continue.'
42+
return 'Sorry, you have items in your basket. Please remove them change the selected store.'
4043
}
4144

4245
if (mode === 'input') {
@@ -58,25 +61,32 @@ export const StoreLocatorList = () => {
5861

5962
const sortedStores = useMemo(() => {
6063
const stores = []
64+
const storeIds = new Set()
6165

62-
if (selectedStore && (!data?.data || !data.data.find((s) => s.id === selectedStore.id))) {
63-
stores.push(selectedStore)
66+
// Add all stores from search results first
67+
if (data?.data) {
68+
data.data.forEach((store) => {
69+
stores.push(store)
70+
storeIds.add(store.id)
71+
})
6472
}
6573

66-
if (data?.data) {
67-
stores.push(...data.data)
74+
// Add selected store that isn't already in search results
75+
if (selectedStore && !storeIds.has(selectedStore.id)) {
76+
stores.push(selectedStore)
77+
storeIds.add(selectedStore.id)
6878
}
6979

7080
return stores.sort((a, b) => {
71-
if (a.id === selectedStoreId) return -1
72-
if (b.id === selectedStoreId) return 1
81+
if (a.id === initialSelectedStoreId) return -1
82+
if (b.id === initialSelectedStoreId) return 1
7383

7484
if (a.distance && b.distance) {
7585
return a.distance - b.distance
7686
}
7787
return 0
7888
})
79-
}, [data?.data, selectedStoreId, selectedStore])
89+
}, [data?.data, selectedStore, initialSelectedStoreId])
8090

8191
const showNumberOfStores = page * config.defaultPageSize
8292
const showLoadMoreButton = sortedStores.length > showNumberOfStores
@@ -98,7 +108,7 @@ export const StoreLocatorList = () => {
98108
{displayStoreLocatorStatusMessage()}
99109
</Box>
100110

101-
<Box as="fieldset" disabled={hasItemsInBasket} opacity={hasItemsInBasket ? 0.5 : 1}>
111+
<Box as="fieldset">
102112
<Accordion allowMultiple flex={[1, 1, 1, 5]}>
103113
<AccordionItem>
104114
<RadioGroup onChange={handleChange} value={selectedStoreId} width="100%">
@@ -109,7 +119,8 @@ export const StoreLocatorList = () => {
109119
radioProps={{
110120
value: store.id,
111121
isChecked: selectedStoreId === store.id,
112-
'aria-describedby': `store-info-${store.id}`
122+
'aria-describedby': `store-info-${store.id}`,
123+
isDisabled: !store.inventoryId || hasItemsInBasket
113124
}}
114125
/>
115126
))}

packages/template-retail-react-app/app/contexts/store-locator-provider.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export const StoreLocatorProvider = ({config, children}) => {
2222
// remember the shopper's preferred store for the current site
2323
// TODO: Change this to `useLocalStorage` hook when localStorage detection is more robust
2424
const {site} = useMultiSite()
25-
const siteId = `selectedStore_${site?.id}`
26-
const selectedStoreId = readValue(siteId)
25+
const selectedStoreBySiteId = `selectedStore_${site?.id}`
26+
const selectedStoreId = readValue(selectedStoreBySiteId)
2727

2828
const [state, setState] = useState({
2929
mode: 'input',
@@ -36,13 +36,12 @@ export const StoreLocatorProvider = ({config, children}) => {
3636
longitude: null
3737
},
3838
selectedStoreId,
39-
isSeSelection: false,
4039
config
4140
})
4241

4342
useEffect(() => {
4443
if (typeof window !== 'undefined' && state.selectedStoreId) {
45-
window.localStorage.setItem(siteId, state.selectedStoreId)
44+
window.localStorage.setItem(selectedStoreBySiteId, state.selectedStoreId)
4645
}
4746
}, [state.selectedStoreId])
4847

packages/template-retail-react-app/app/contexts/store-locator-provider.test.jsx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ import {
1313
} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
1414
import {MultiSiteProvider} from '@salesforce/retail-react-app/app/contexts'
1515

16+
// Mock useMultiSite hook
17+
jest.mock('@salesforce/retail-react-app/app/hooks/use-multi-site', () => ({
18+
__esModule: true,
19+
default: () => ({
20+
site: {
21+
id: 'RefArch',
22+
alias: 'us'
23+
},
24+
locale: {
25+
id: 'en-US',
26+
preferredCurrency: 'USD'
27+
},
28+
buildUrl: (path) => path
29+
})
30+
}))
31+
1632
describe('StoreLocatorProvider', () => {
1733
const mockConfig = {
1834
defaultCountryCode: 'US',
@@ -24,6 +40,11 @@ describe('StoreLocatorProvider', () => {
2440
alias: 'us'
2541
}
2642

43+
beforeEach(() => {
44+
// Clear localStorage before each test
45+
window.localStorage.clear()
46+
})
47+
2748
const TestWrapper = ({children}) => (
2849
<MultiSiteProvider site={mockSite}>
2950
<StoreLocatorProvider config={mockConfig}>{children}</StoreLocatorProvider>
@@ -59,12 +80,30 @@ describe('StoreLocatorProvider', () => {
5980
longitude: null
6081
},
6182
selectedStoreId: null,
62-
isSeSelection: false,
6383
config: mockConfig
6484
})
6585
expect(typeof contextValue?.setState).toBe('function')
6686
})
6787

88+
it('initializes with stored selectedStoreId from localStorage', () => {
89+
// Set a value in localStorage before rendering
90+
window.localStorage.setItem('selectedStore_RefArch', 'store123')
91+
92+
let contextValue
93+
const TestComponent = () => {
94+
contextValue = React.useContext(StoreLocatorContext)
95+
return null
96+
}
97+
98+
render(
99+
<TestWrapper>
100+
<TestComponent />
101+
</TestWrapper>
102+
)
103+
104+
expect(contextValue?.state.selectedStoreId).toBe('store123')
105+
})
106+
68107
it('updates state correctly when setState is called', () => {
69108
let contextValue
70109
const TestComponent = () => {
@@ -96,6 +135,29 @@ describe('StoreLocatorProvider', () => {
96135
})
97136
})
98137

138+
it('updates localStorage when selectedStoreId changes', () => {
139+
let contextValue
140+
const TestComponent = () => {
141+
contextValue = React.useContext(StoreLocatorContext)
142+
return null
143+
}
144+
145+
render(
146+
<TestWrapper>
147+
<TestComponent />
148+
</TestWrapper>
149+
)
150+
151+
act(() => {
152+
contextValue?.setState((prev) => ({
153+
...prev,
154+
selectedStoreId: 'store456'
155+
}))
156+
})
157+
158+
expect(window.localStorage.getItem('selectedStore_RefArch')).toBe('store456')
159+
})
160+
99161
it('renders children correctly', () => {
100162
const TestChild = () => <div data-testid="test-child">Test Child</div>
101163

packages/template-retail-react-app/app/hooks/use-store-locator.js

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import {useContext, useEffect} from 'react'
8+
import {useContext} from 'react'
99
import {useSearchStores} from '@salesforce/commerce-sdk-react'
1010
import {StoreLocatorContext} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
1111
import {STORE_LOCATOR_NUM_STORES_PER_REQUEST_API_MAX} from '@salesforce/retail-react-app/app/constants'
@@ -55,23 +55,11 @@ export const useStoreLocator = () => {
5555
const {state, setState} = context
5656
const {data, isLoading} = useStores(state)
5757

58-
useEffect(() => {
59-
if (data?.data?.length > 0 && !state.isSeSelection) {
60-
const nearestStore = data.data[0]
61-
setState((prev) => ({
62-
...prev,
63-
selectedStoreId: nearestStore.id,
64-
isSeSelection: true
65-
}))
66-
}
67-
}, [data?.data, state.isSeSelection])
68-
6958
const setFormValues = (formValues) => {
7059
setState((prev) => ({
7160
...prev,
7261
formValues,
73-
mode: 'input',
74-
isSeSelection: false
62+
mode: 'input'
7563
}))
7664
}
7765

@@ -80,16 +68,14 @@ export const useStoreLocator = () => {
8068
...prev,
8169
deviceCoordinates: coordinates,
8270
mode: 'device',
83-
formValues: {countryCode: '', postalCode: ''},
84-
isSeSelection: false
71+
formValues: {countryCode: '', postalCode: ''}
8572
}))
8673
}
8774

8875
const setSelectedStoreId = (selectedStoreId) => {
8976
setState((prev) => ({
9077
...prev,
91-
selectedStoreId,
92-
isSeSelection: true // Mark manual store selection as SE selection
78+
selectedStoreId
9379
}))
9480
}
9581

packages/template-retail-react-app/app/hooks/use-store-locator.test.jsx

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ describe('useStoreLocator', () => {
7171
longitude: null
7272
},
7373
selectedStoreId: null,
74-
isSeSelection: false,
7574
config
7675
})
7776
})
@@ -89,7 +88,6 @@ describe('useStoreLocator', () => {
8988
countryCode: 'US',
9089
postalCode: '94105'
9190
})
92-
expect(result.current.isSeSelection).toBe(false)
9391
})
9492

9593
it('updates device coordinates and switches to device mode', () => {
@@ -109,7 +107,6 @@ describe('useStoreLocator', () => {
109107
countryCode: '',
110108
postalCode: ''
111109
})
112-
expect(result.current.isSeSelection).toBe(false)
113110
})
114111

115112
it('calls useSearchStores with correct parameters in input mode', () => {
@@ -160,13 +157,11 @@ describe('useStoreLocator', () => {
160157
)
161158
})
162159

163-
it('automatically selects nearest store when data is received in input mode', () => {
164-
const mockStores = {
165-
data: [
166-
{id: 'store1', name: 'Store 1'},
167-
{id: 'store2', name: 'Store 2'}
168-
]
169-
}
160+
it('receives store data when form values are set in input mode', () => {
161+
const mockStores = [
162+
{id: 'store1', name: 'Store 1'},
163+
{id: 'store2', name: 'Store 2'}
164+
]
170165
useSearchStores.mockReturnValue({
171166
data: mockStores,
172167
isLoading: false
@@ -180,8 +175,8 @@ describe('useStoreLocator', () => {
180175
})
181176
})
182177

183-
expect(result.current.selectedStoreId).toBe('store1')
184-
expect(result.current.isSeSelection).toBe(true)
178+
expect(result.current.data).toEqual(mockStores)
179+
expect(result.current.selectedStoreId).toBeNull()
185180
})
186181

187182
it('allows manual store selection', () => {
@@ -190,7 +185,6 @@ describe('useStoreLocator', () => {
190185
result.current.setSelectedStoreId('store123')
191186
})
192187
expect(result.current.selectedStoreId).toBe('store123')
193-
expect(result.current.isSeSelection).toBe(true)
194188
})
195189

196190
it('handles loading state', () => {

0 commit comments

Comments
 (0)