diff --git a/packages/template-retail-react-app/app/components/_app/index.jsx b/packages/template-retail-react-app/app/components/_app/index.jsx
index 5b8a2f5545..6dfb4178ea 100644
--- a/packages/template-retail-react-app/app/components/_app/index.jsx
+++ b/packages/template-retail-react-app/app/components/_app/index.jsx
@@ -19,7 +19,6 @@ import {
} from '@salesforce/commerce-sdk-react'
import logger from '@salesforce/retail-react-app/app/utils/logger-instance'
import {useAppOrigin} from '@salesforce/retail-react-app/app/hooks/use-app-origin'
-import loadable from '@loadable/component'
// Chakra
import {
@@ -34,7 +33,6 @@ import {SkipNavLink, SkipNavContent} from '@chakra-ui/skip-nav'
// Contexts
import {CurrencyProvider} from '@salesforce/retail-react-app/app/contexts'
-import {StoreLocatorParamsProvider} from '@salesforce/retail-react-app/app/contexts/store-locator-params'
// Local Project Components
import Header from '@salesforce/retail-react-app/app/components/header'
@@ -81,11 +79,6 @@ import {
import Seo from '@salesforce/retail-react-app/app/components/seo'
import {getPathWithLocale} from '@salesforce/retail-react-app/app/utils/url'
-import useExternalSearch from '@salesforce/retail-react-app/app/hooks/use-external-search'
-
-const SeInputHandler = loadable(() =>
- import('@salesforce/retail-react-app/app/components/se-input-handler')
-)
const PlaceholderComponent = () => (
@@ -133,7 +126,6 @@ const App = (props) => {
const {data: categoriesTree} = useCategory({
parameters: {id: CAT_MENU_DEFAULT_ROOT_CATEGORY, levels: CAT_MENU_DEFAULT_NAV_SSR_DEPTH}
})
- useExternalSearch()
const categories = flatten(categoriesTree || {}, 'categories')
const {getTokenWhenReady} = useAccessToken()
const appOrigin = useAppOrigin()
@@ -153,21 +145,6 @@ const App = (props) => {
onClose: onCloseStoreLocator
} = useDisclosure()
- useEffect(() => {
- if (typeof window !== 'undefined' && window.location && window.location.href) {
- const href = window.location.href
- const questionMarks = (href.match(/\?/g) || []).length
-
- if (questionMarks > 1) {
- const parts = href.split('?')
- const fixedUrl = parts[0] + '?' + parts.slice(1).join('&')
- const url = new URL(fixedUrl)
- const newPath = url.pathname + url.search
- history.replace(newPath)
- }
- }
- }, [location, history])
-
const targetLocale = getTargetLocale({
getUserPreferredLocales: () => {
// CONFIG: This function should return an array of preferred locales. They can be
@@ -334,139 +311,125 @@ const App = (props) => {
defaultLocale={DEFAULT_LOCALE}
>
-
-
-
-
-
-
-
-
- {/* Urls for all localized versions of this page (including current page)
- For more details on hrefLang, see https://developers.google.com/search/docs/advanced/crawling/localized-versions */}
- {site.l10n?.supportedLocales.map((locale) => (
-
- ))}
- {/* A general locale as fallback. For example: "en" if default locale is "en-GB" */}
+
+
+
+
+
+
+ {/* Urls for all localized versions of this page (including current page)
+ For more details on hrefLang, see https://developers.google.com/search/docs/advanced/crawling/localized-versions */}
+ {site.l10n?.supportedLocales.map((locale) => (
- {/* A wider fallback for user locales that the app does not support */}
-
-
-
-
-
-
- Skip to Content
-
-
- {!isCheckout ? (
- <>
-
-
- >
- ) : (
-
- )}
-
- {!isOnline && }
-
-
-
+ {/* A wider fallback for user locales that the app does not support */}
+
+
+
+
+
+
+ Skip to Content
+
+
+ {!isCheckout ? (
+ <>
+
+
-
-
- {!isCheckout ? : }
-
-
-
-
+
+
+
+
+
+
+
+
+ >
+ ) : (
+
+ )}
-
+ {!isOnline && }
+
+
+
+
+ {children}
+
+
+
+
+ {!isCheckout ? : }
+
+
+
+
+
diff --git a/packages/template-retail-react-app/app/components/se-input-handler/index.jsx b/packages/template-retail-react-app/app/components/se-input-handler/index.jsx
deleted file mode 100644
index 11b410b2b7..0000000000
--- a/packages/template-retail-react-app/app/components/se-input-handler/index.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (c) 2021, salesforce.com, inc.
- * All rights reserved.
- * SPDX-License-Identifier: BSD-3-Clause
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
- */
-
-import {useEffect, useContext} from 'react'
-import {useLocation, useHistory} from 'react-router-dom'
-import PropTypes from 'prop-types'
-import useSeStoreSelection from '@salesforce/retail-react-app/app/hooks/use-se-store-selection'
-import {useStoreLocatorParams} from '@salesforce/retail-react-app/app/contexts/store-locator-params'
-import useExternalSearch from '@salesforce/retail-react-app/app/hooks/use-external-search'
-import {StoreLocatorContext} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
-
-const SeInputHandler = ({onOpenStoreLocator}) => {
- useExternalSearch()
- const location = useLocation()
- const history = useHistory()
- const {shouldOpenModal, setShouldOpenModal, storeLocatorParams, processSeParameters} =
- useSeStoreSelection()
-
- const {setParams} = useStoreLocatorParams()
- const {setState: setStoreLocatorState} = useContext(StoreLocatorContext) || {}
-
- useEffect(() => {
- const urlParams = new URLSearchParams(location.search)
- processSeParameters(urlParams)
- }, [location.search, processSeParameters])
-
- // Handle store locator params updates
- useEffect(() => {
- if (!storeLocatorParams) return
- setParams(storeLocatorParams)
- const storeName = new URLSearchParams(location.search).get('store')
- if (setStoreLocatorState && !storeName) {
- if (storeLocatorParams.postalCode && storeLocatorParams.countryCode) {
- setStoreLocatorState((prev) => ({
- ...prev,
- mode: 'input',
- formValues: {
- postalCode: storeLocatorParams.postalCode,
- countryCode: storeLocatorParams.countryCode
- }
- }))
- } else if (storeLocatorParams.latitude && storeLocatorParams.longitude) {
- setStoreLocatorState((prev) => ({
- ...prev,
- mode: 'device',
- deviceCoordinates: {
- latitude: storeLocatorParams.latitude,
- longitude: storeLocatorParams.longitude
- }
- }))
- }
- }
- }, [storeLocatorParams, setParams, setStoreLocatorState])
-
- useEffect(() => {
- if (!shouldOpenModal || !storeLocatorParams) return
-
- const urlParams = new URLSearchParams(location.search)
- const hasSeParamKeys = ['lat', 'lng', 'zip', 'city', 'store', 'country']
-
- onOpenStoreLocator()
- setShouldOpenModal(false)
-
- const hasSeParams = hasSeParamKeys.some((key) => urlParams.has(key))
- if (hasSeParams) {
- cleanURLParams(location, history, hasSeParamKeys)
- }
- }, [
- shouldOpenModal,
- storeLocatorParams,
- onOpenStoreLocator,
- setShouldOpenModal,
- location.search,
- location.pathname,
- history
- ])
-
- return null
-}
-
-export const cleanURLParams = (location, history, hasSeParamKeys) => {
- const cleanParams = new URLSearchParams(location.search)
- hasSeParamKeys.forEach((key) => cleanParams.delete(key))
-
- const cleanSearch = cleanParams.toString()
- const newUrl = location.pathname + (cleanSearch ? `?${cleanSearch}` : '')
-
- history.replace(newUrl)
-}
-
-SeInputHandler.propTypes = {
- onOpenStoreLocator: PropTypes.func.isRequired
-}
-
-export default SeInputHandler
diff --git a/packages/template-retail-react-app/app/components/se-input-handler/index.test.jsx b/packages/template-retail-react-app/app/components/se-input-handler/index.test.jsx
deleted file mode 100644
index daac32550b..0000000000
--- a/packages/template-retail-react-app/app/components/se-input-handler/index.test.jsx
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (c) 2024, salesforce.com, inc.
- * All rights reserved.
- * SPDX-License-Identifier: BSD-3-Clause
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
- */
-
-import React from 'react'
-import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
-import {waitFor} from '@testing-library/react'
-import SeInputHandler from '@salesforce/retail-react-app/app/components/se-input-handler'
-import {StoreLocatorContext} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
-import {useStoreLocatorParams} from '@salesforce/retail-react-app/app/contexts/store-locator-params'
-import useSeStoreSelection from '@salesforce/retail-react-app/app/hooks/use-se-store-selection'
-import useExternalSearch from '@salesforce/retail-react-app/app/hooks/use-external-search'
-import PropTypes from 'prop-types'
-
-jest.mock('@salesforce/retail-react-app/app/hooks/use-se-store-selection')
-jest.mock('@salesforce/retail-react-app/app/contexts/store-locator-params')
-jest.mock('@salesforce/retail-react-app/app/hooks/use-external-search')
-
-describe('SeInputHandler', () => {
- const mockOnOpenStoreLocator = jest.fn()
- const mockSetShouldOpenModal = jest.fn()
- const mockProcessSeParameters = jest.fn()
-
- const mockStoreLocatorContext = {
- state: {
- mode: 'input',
- formValues: {},
- deviceCoordinates: {
- latitude: null,
- longitude: null
- }
- },
- setState: jest.fn()
- }
-
- const TestWrapper = ({children}) => (
-
- {children}
-
- )
-
- TestWrapper.propTypes = {
- children: PropTypes.node
- }
-
- const renderComponent = () => {
- return renderWithProviders(
-
-
-
- )
- }
-
- beforeEach(() => {
- jest.clearAllMocks()
- window.history.pushState({}, '', '/test')
-
- useStoreLocatorParams.mockReturnValue({
- params: {},
- setParams: jest.fn(),
- processSeParameters: mockProcessSeParameters
- })
-
- useSeStoreSelection.mockReturnValue({
- shouldOpenModal: false,
- setShouldOpenModal: mockSetShouldOpenModal,
- storeLocatorParams: {},
- processSeParameters: mockProcessSeParameters
- })
-
- useExternalSearch.mockReturnValue()
- })
-
- describe('Store Locator State Management', () => {
- test('updates store locator state for postal code search', async () => {
- window.history.pushState({}, '', '/test?zip=02215&country=US')
-
- useSeStoreSelection.mockReturnValue({
- shouldOpenModal: true,
- setShouldOpenModal: mockSetShouldOpenModal,
- storeLocatorParams: {postalCode: '02215', countryCode: 'US'},
- processSeParameters: mockProcessSeParameters
- })
-
- renderComponent()
-
- await waitFor(() => {
- expect(mockStoreLocatorContext.setState).toHaveBeenCalledWith(expect.any(Function))
- const updateFn = mockStoreLocatorContext.setState.mock.calls[0][0]
- const newState = updateFn({})
- expect(newState).toEqual(
- expect.objectContaining({
- mode: 'input',
- formValues: {
- postalCode: '02215',
- countryCode: 'US'
- }
- })
- )
- })
- })
-
- test('updates store locator state for coordinate search', async () => {
- window.history.pushState({}, '', '/test?lat=42.3601&lng=-71.0589')
-
- useSeStoreSelection.mockReturnValue({
- shouldOpenModal: true,
- setShouldOpenModal: mockSetShouldOpenModal,
- storeLocatorParams: {latitude: '42.3601', longitude: '-71.0589'},
- processSeParameters: mockProcessSeParameters
- })
-
- renderComponent()
-
- await waitFor(() => {
- expect(mockStoreLocatorContext.setState).toHaveBeenCalledWith(expect.any(Function))
- const updateFn = mockStoreLocatorContext.setState.mock.calls[0][0]
- const newState = updateFn({})
- expect(newState).toEqual(
- expect.objectContaining({
- mode: 'device',
- deviceCoordinates: {
- latitude: '42.3601',
- longitude: '-71.0589'
- }
- })
- )
- })
- })
- })
-
- describe('Modal Control', () => {
- test('opens modal when shouldOpenModal becomes true', async () => {
- useSeStoreSelection.mockReturnValue({
- shouldOpenModal: true,
- setShouldOpenModal: mockSetShouldOpenModal,
- storeLocatorParams: {},
- processSeParameters: mockProcessSeParameters
- })
-
- renderComponent()
-
- await waitFor(() => {
- expect(mockOnOpenStoreLocator).toHaveBeenCalled()
- })
- })
-
- test('does not open modal when shouldOpenModal is false', () => {
- useSeStoreSelection.mockReturnValue({
- shouldOpenModal: false,
- setShouldOpenModal: mockSetShouldOpenModal,
- storeLocatorParams: {},
- processSeParameters: mockProcessSeParameters
- })
-
- renderComponent()
-
- expect(mockOnOpenStoreLocator).not.toHaveBeenCalled()
- })
- })
-
- describe('External Search Integration', () => {
- test('no delays modal opening when external search query is present', async () => {
- window.history.pushState({}, '', '/test?lat=42.3601&lng=-71.0589&q=shoes')
-
- useSeStoreSelection.mockReturnValue({
- shouldOpenModal: true,
- setShouldOpenModal: mockSetShouldOpenModal,
- storeLocatorParams: {latitude: '42.3601', longitude: '-71.0589'},
- processSeParameters: mockProcessSeParameters
- })
-
- useExternalSearch.mockReturnValue()
-
- renderComponent()
-
- expect(mockOnOpenStoreLocator).toHaveBeenCalled()
- })
-
- test('opens modal immediately when no external search query is present', async () => {
- window.history.pushState({}, '', '/test?lat=42.3601&lng=-71.0589')
-
- useSeStoreSelection.mockReturnValue({
- shouldOpenModal: true,
- setShouldOpenModal: mockSetShouldOpenModal,
- storeLocatorParams: {latitude: '42.3601', longitude: '-71.0589'},
- processSeParameters: mockProcessSeParameters
- })
-
- renderComponent()
-
- expect(mockOnOpenStoreLocator).toHaveBeenCalled()
- })
- })
-})
diff --git a/packages/template-retail-react-app/app/contexts/store-locator-params.js b/packages/template-retail-react-app/app/contexts/store-locator-params.js
deleted file mode 100644
index 101ad58d23..0000000000
--- a/packages/template-retail-react-app/app/contexts/store-locator-params.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (c) 2025, Salesforce, Inc.
- * All rights reserved.
- * SPDX-License-Identifier: BSD-3-Clause
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
- */
-import React, {createContext, useContext, useState} from 'react'
-import PropTypes from 'prop-types'
-
-const StoreLocatorParamsContext = createContext({
- params: null,
- setParams: () => {}
-})
-
-export const StoreLocatorParamsProvider = ({children}) => {
- const [params, setParams] = useState(null)
- return (
-
- {children}
-
- )
-}
-
-StoreLocatorParamsProvider.propTypes = {
- children: PropTypes.node
-}
-
-export const useStoreLocatorParams = () => useContext(StoreLocatorParamsContext)
diff --git a/packages/template-retail-react-app/app/hooks/use-external-search.js b/packages/template-retail-react-app/app/hooks/use-external-search.js
deleted file mode 100644
index 522dd670ca..0000000000
--- a/packages/template-retail-react-app/app/hooks/use-external-search.js
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (c) 2024, salesforce.com, inc.
- * All rights reserved.
- * SPDX-License-Identifier: BSD-3-Clause
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
- */
-
-import {useEffect, useMemo} from 'react'
-import {useHistory, useLocation} from 'react-router-dom'
-import {useSearchParams} from '@salesforce/retail-react-app/app/hooks/use-search-params'
-import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
-
-/**
- * Routing when external search parameters are present in the URL to the appropriate search results
- */
-const useExternalSearch = () => {
- const history = useHistory()
- const location = useLocation()
- const [searchParams] = useSearchParams()
- const {buildUrl} = useMultiSite()
-
- const hasStoreLocatorParams = useMemo(() => {
- const keys = ['lat', 'lng', 'zip', 'city', 'store', 'country']
- return keys.some((key) => Boolean(searchParams?.[key]))
- }, [searchParams])
-
- const hasExternalSearchParams = useMemo(() => {
- if (typeof window === 'undefined') return false
- const urlParams = new URLSearchParams(location.search)
- return urlParams.has('q') || urlParams.has('search') || urlParams.has('query')
- }, [location.search])
-
- useEffect(() => {
- if (!hasExternalSearchParams) {
- return
- }
-
- if (typeof window === 'undefined') {
- return
- }
-
- if (hasStoreLocatorParams) {
- return
- }
-
- // need to pre-process out filler words like location hints, and handle multi-word searches
- // Handle multi-word searches and any extraneous hints.
- const rawQuery = searchParams?.q ?? searchParams?.search ?? searchParams?.query
- const query = (typeof rawQuery === 'string' ? rawQuery : '').trim()
-
- if (!query) {
- return
- }
-
- if (location?.pathname?.startsWith('/search')) {
- return
- }
-
- if (!history || !history.push) {
- return
- }
-
- const searchUrl = buildUrl(`/search?q=${encodeURIComponent(query)}`)
-
- try {
- history.push(searchUrl)
- } catch (error) {
- console.warn(error)
- }
- }, [
- hasExternalSearchParams,
- location?.pathname,
- searchParams?.q,
- searchParams?.search,
- searchParams?.query,
- history,
- hasStoreLocatorParams,
- buildUrl
- ])
-}
-
-export default useExternalSearch
diff --git a/packages/template-retail-react-app/app/hooks/use-external-search.test.js b/packages/template-retail-react-app/app/hooks/use-external-search.test.js
deleted file mode 100644
index 27794399fa..0000000000
--- a/packages/template-retail-react-app/app/hooks/use-external-search.test.js
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (c) 2024, salesforce.com, inc.
- * All rights reserved.
- * SPDX-License-Identifier: BSD-3-Clause
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
- */
-
-import React from 'react'
-import {waitFor} from '@testing-library/react'
-import PropTypes from 'prop-types'
-import useExternalSearch from '@salesforce/retail-react-app/app/hooks/use-external-search'
-import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
-
-// Mock the multi-site hook
-const mockBuildUrl = jest.fn((path) => path)
-jest.mock('@salesforce/retail-react-app/app/hooks/use-multi-site', () => {
- return jest.fn().mockImplementation(() => ({
- buildUrl: mockBuildUrl
- }))
-})
-
-const MockComponent = ({expectRedirect = false}) => {
- useExternalSearch()
- return (
-
- {expectRedirect ? 'should re-direct' : 'should not re-direct'}
-
- )
-}
-
-MockComponent.propTypes = {
- expectRedirect: PropTypes.bool
-}
-
-const originalConsoleWarn = console.warn
-
-beforeEach(() => {
- console.warn = jest.fn()
- mockBuildUrl.mockClear()
-})
-
-afterEach(() => {
- console.warn = originalConsoleWarn
-})
-
-describe('useExternalSearch', () => {
- describe('when query parameter is present', () => {
- test('re-directs to search page when "q" parameter is present', async () => {
- window.history.pushState({}, '', '/?q=test+query')
- renderWithProviders()
-
- await waitFor(() => {
- expect(mockBuildUrl).toHaveBeenCalledWith('/search?q=test%20query')
- })
-
- expect(window.location.pathname).toBe('/search')
- expect(window.location.search).toBe('?q=test%20query')
- })
-
- test('re-directs to search page when "search" parameter present', async () => {
- window.history.pushState({}, '', '/?search=another+query')
- renderWithProviders()
-
- await waitFor(() => {
- expect(mockBuildUrl).toHaveBeenCalledWith('/search?q=another%20query')
- })
-
- expect(window.location.pathname).toBe('/search')
- expect(window.location.search).toBe('?q=another%20query')
- })
-
- test('re-directs to search page when "query" parameter is present', async () => {
- window.history.pushState({}, '', '/?query=third+query')
- renderWithProviders()
-
- await waitFor(() => {
- expect(mockBuildUrl).toHaveBeenCalledWith('/search?q=third%20query')
- })
-
- expect(window.location.pathname).toBe('/search')
- expect(window.location.search).toBe('?q=third%20query')
- })
-
- test('trims whitespace from query parameter', async () => {
- window.history.pushState({}, '', '/?q=%20%20trimmed%20query%20%20')
- renderWithProviders()
- await waitFor(() => {
- expect(mockBuildUrl).toHaveBeenCalledWith('/search?q=trimmed%20query')
- })
- })
- })
-
- describe('does not redirect when', () => {
- test('query is empty string', () => {
- window.history.pushState({}, '', '/?q=')
- renderWithProviders()
- expect(mockBuildUrl).not.toHaveBeenCalled()
- expect(window.location.pathname).toBe('/')
- })
-
- test('query is only whitespace', () => {
- window.history.pushState({}, '', '/?q=%20%20%20')
- renderWithProviders()
- expect(mockBuildUrl).not.toHaveBeenCalled()
- expect(window.location.pathname).toBe('/')
- })
-
- test('no query parameters are present', () => {
- window.history.pushState({}, '', '/')
- renderWithProviders()
- expect(mockBuildUrl).not.toHaveBeenCalled()
- expect(window.location.pathname).toBe('/')
- })
-
- test('already on search page', () => {
- window.history.pushState({}, '', '/search?q=existing-query')
- renderWithProviders()
- expect(mockBuildUrl).not.toHaveBeenCalled()
- expect(window.location.pathname).toBe('/search')
- })
-
- test('on nested search page', () => {
- window.history.pushState({}, '', '/search/category?q=existing-query')
- renderWithProviders()
- expect(mockBuildUrl).not.toHaveBeenCalled()
- expect(window.location.pathname).toBe('/search/category')
- })
- })
-
- describe('multiple query parameters with different formats', () => {
- test('handles URL encoded query parameters', async () => {
- window.history.pushState({}, '', '/?q=search%20with%20spaces%20and%20%26%20symbols')
- renderWithProviders()
-
- await waitFor(() => {
- expect(mockBuildUrl).toHaveBeenCalledWith(
- '/search?q=search%20with%20spaces%20and%20%26%20symbols'
- )
- })
- })
-
- test('handles plus-encoded spaces', async () => {
- window.history.pushState({}, '', '/?q=search+with+plus+spaces')
- renderWithProviders()
-
- await waitFor(() => {
- expect(mockBuildUrl).toHaveBeenCalledWith('/search?q=search%20with%20plus%20spaces')
- })
- })
- })
-
- describe('utility function', () => {
- test('calls buildUrl with correct query parameter', async () => {
- window.history.pushState({}, '', '/?q=utility+test')
- renderWithProviders()
- await waitFor(() => {
- expect(mockBuildUrl).toHaveBeenCalledWith('/search?q=utility%20test')
- expect(mockBuildUrl).toHaveBeenCalledTimes(1)
- })
- })
- })
-})
diff --git a/packages/template-retail-react-app/app/hooks/use-se-store-selection.js b/packages/template-retail-react-app/app/hooks/use-se-store-selection.js
deleted file mode 100644
index 0788f85921..0000000000
--- a/packages/template-retail-react-app/app/hooks/use-se-store-selection.js
+++ /dev/null
@@ -1,469 +0,0 @@
-/*
- * Copyright (c) 2021, salesforce.com, inc.
- * All rights reserved.
- * SPDX-License-Identifier: BSD-3-Clause
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
- */
-
-import {useState, useEffect, useCallback, useMemo, useContext} from 'react'
-import {useSearchStores} from '@salesforce/commerce-sdk-react'
-import {useIntl} from 'react-intl'
-import {StoreLocatorContext} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
-import {
- STORE_LOCATOR_RADIUS,
- STORE_LOCATOR_RADIUS_UNIT,
- STORE_LOCATOR_DEFAULT_COUNTRY_CODE
-} from '@salesforce/retail-react-app/app/constants'
-import {cleanURLParams} from '@salesforce/retail-react-app/app/components/se-input-handler'
-import {useLocation, useHistory} from 'react-router-dom'
-import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
-
-const useSeStoreSelection = () => {
- const intl = useIntl()
- const storeLocatorContext = useContext(StoreLocatorContext)
- const [locationData, setLocationData] = useState(null)
- const [isProcessing, setIsProcessing] = useState(false)
- const [shouldOpenModal, setShouldOpenModal] = useState(false)
- const [storeLocatorParams, setStoreLocatorParams] = useState(null)
- const [enableCoordinateSearch, setEnableCoordinateSearch] = useState(false)
- const location = useLocation()
- const history = useHistory()
- const {derivedData} = useCurrentBasket()
- const hasItemsInBasket = derivedData?.totalItems > 0
-
- const getCountryForPostalSearch = useCallback((zipcode, explicitCountry) => {
- return explicitCountry && explicitCountry !== 'none'
- ? explicitCountry
- : STORE_LOCATOR_DEFAULT_COUNTRY_CODE
- }, [])
-
- const {data: coordinateStoreData, isLoading: isLoadingCoordinateStores} = useSearchStores({
- parameters: {
- latitude: locationData?.latitude,
- longitude: locationData?.longitude,
- locale: intl.locale,
- maxDistance: STORE_LOCATOR_RADIUS,
- limit: 200,
- distanceUnit: STORE_LOCATOR_RADIUS_UNIT
- },
- enabled:
- enableCoordinateSearch && Boolean(locationData?.latitude && locationData?.longitude)
- })
-
- const countryCodeToUse = getCountryForPostalSearch(
- locationData?.zipcode,
- locationData?.countryCode
- )
-
- const {data: postalCodeStoreData, isLoading: isLoadingPostalStores} = useSearchStores({
- parameters: {
- postalCode: locationData?.zipcode,
- countryCode: countryCodeToUse,
- locale: intl.locale,
- maxDistance: STORE_LOCATOR_RADIUS,
- limit: 200,
- distanceUnit: STORE_LOCATOR_RADIUS_UNIT
- },
- enabled: Boolean(locationData?.zipcode && !locationData?.latitude)
- })
-
- const getGlobalSearchParams = useCallback(() => {
- const baseDistance = STORE_LOCATOR_RADIUS * 200
- const storeNameDistance =
- locationData?.storeName && locationData?.countryCode ? baseDistance * 2 : baseDistance
-
- return {
- latitude: 0,
- longitude: 0,
- locale: intl.locale,
- maxDistance: storeNameDistance,
- limit: 200,
- distanceUnit: STORE_LOCATOR_RADIUS_UNIT
- }
- }, [locationData?.storeName, locationData?.countryCode, intl.locale])
-
- const {data: allStoresData, isLoading: isLoadingAllStores} = useSearchStores({
- parameters: getGlobalSearchParams(),
- enabled: Boolean(
- (locationData?.city && !locationData?.latitude && !locationData?.zipcode) ||
- (locationData?.storeName &&
- locationData?.countryCode &&
- !locationData?.zipcode &&
- !locationData?.latitude)
- )
- })
-
- const needsFallbackSearch = Boolean(
- locationData?.storeName &&
- locationData?.countryCode &&
- !locationData?.zipcode &&
- !locationData?.latitude &&
- !isLoadingAllStores &&
- (!allStoresData?.data?.length ||
- (allStoresData?.data?.length > 0 &&
- !allStoresData.data.some((store) => {
- const sName = (store.name || '').toLowerCase().trim()
- const searchName = locationData.storeName.toLowerCase().trim()
- const countryMatch =
- (store.countryCode || STORE_LOCATOR_DEFAULT_COUNTRY_CODE) ===
- locationData.countryCode
- return (sName === searchName || sName.includes(searchName)) && countryMatch
- })))
- )
-
- const getFallbackSearchParams = useCallback(() => {
- const maxDistance = STORE_LOCATOR_RADIUS * 500
-
- return {
- latitude: 0,
- longitude: 0,
- locale: intl.locale,
- maxDistance,
- limit: 200,
- distanceUnit: STORE_LOCATOR_RADIUS_UNIT
- }
- }, [intl.locale])
-
- const {data: fallbackStoresData, isLoading: isLoadingFallbackStores} = useSearchStores({
- parameters: getFallbackSearchParams(),
- enabled: needsFallbackSearch
- })
-
- const combinedStoresData = allStoresData?.data?.length ? allStoresData : fallbackStoresData
- const getCityCoordinatesFromStores = useCallback(
- (cityName, countryCode) => {
- const stores = combinedStoresData?.data || []
- if (stores.length === 0) return null
-
- const cityKey = cityName.toLowerCase().trim()
- const defaultCountry = countryCode || STORE_LOCATOR_DEFAULT_COUNTRY_CODE
-
- let cityStores = stores.filter((store) => {
- const storeCity = (store.city || '').toLowerCase().trim()
- const storeCountry = store.countryCode || STORE_LOCATOR_DEFAULT_COUNTRY_CODE
- const cityMatch =
- storeCity === cityKey ||
- storeCity.includes(cityKey) ||
- cityKey.includes(storeCity)
-
- return (
- cityMatch &&
- storeCountry === defaultCountry &&
- store.latitude &&
- store.longitude
- )
- })
-
- if (cityStores.length === 0) {
- cityStores = stores.filter((store) => {
- const storeCity = (store.city || '').toLowerCase().trim()
- const cityMatch =
- storeCity === cityKey ||
- storeCity.includes(cityKey) ||
- cityKey.includes(storeCity)
-
- return cityMatch && store.latitude && store.longitude
- })
- }
-
- if (cityStores.length === 0) return null
-
- const firstStore = cityStores[0]
- return {
- lat: firstStore.latitude,
- lng: firstStore.longitude,
- country: firstStore.countryCode,
- city: firstStore.city,
- postalCode: firstStore.postalCode,
- storeCount: cityStores.length,
- storeName: firstStore.name
- }
- },
- [combinedStoresData]
- )
-
- const cityCoords = useMemo(() => {
- if (!locationData?.city || isLoadingAllStores) return null
- return getCityCoordinatesFromStores(locationData.city, locationData?.countryCode)
- }, [
- locationData?.city,
- locationData?.countryCode,
- getCityCoordinatesFromStores,
- isLoadingAllStores
- ])
-
- const {data: cityStoreData, isLoading: isLoadingCityStores} = useSearchStores({
- parameters: {
- latitude: cityCoords?.lat,
- longitude: cityCoords?.lng,
- locale: intl.locale,
- maxDistance: STORE_LOCATOR_RADIUS * 5,
- limit: 200,
- distanceUnit: STORE_LOCATOR_RADIUS_UNIT
- },
- enabled: Boolean(
- cityCoords &&
- locationData?.city &&
- !locationData?.latitude &&
- !locationData?.zipcode &&
- !isLoadingAllStores
- )
- })
-
- useEffect(() => {
- setEnableCoordinateSearch(Boolean(locationData?.latitude && locationData?.longitude))
- }, [locationData?.latitude, locationData?.longitude])
-
- const getStoreSearchData = () => {
- if (coordinateStoreData) return coordinateStoreData
-
- if (postalCodeStoreData && locationData?.zipcode) return postalCodeStoreData
-
- if (cityStoreData) return cityStoreData
-
- if (
- locationData?.storeName &&
- locationData?.countryCode &&
- !locationData?.zipcode &&
- !locationData?.city &&
- combinedStoresData
- ) {
- return combinedStoresData
- }
- return null
- }
-
- const storeSearchData = getStoreSearchData()
- const isLoadingStores =
- isLoadingCoordinateStores ||
- isLoadingPostalStores ||
- isLoadingCityStores ||
- isLoadingAllStores ||
- isLoadingFallbackStores
- const findMatchingStore = useCallback((stores, searchCriteria) => {
- if (!stores || stores.length === 0) return null
-
- const {storeName, zipcode, city, countryCode} = searchCriteria
-
- if (storeName) {
- const searchNameLower = storeName.toLowerCase().trim()
- const filters = [
- [
- countryCode,
- (s) => (s.countryCode || STORE_LOCATOR_DEFAULT_COUNTRY_CODE) === countryCode
- ],
- [zipcode, (s) => (s.postalCode || s.address?.postalCode) === zipcode],
- [
- city,
- (s) =>
- (s.city || s.address?.city || '').toLowerCase().includes(city.toLowerCase())
- ]
- ]
-
- let exactMatches = stores.filter((store) => {
- const storeNameLower = (store.name || '').toLowerCase().trim()
- return storeNameLower === searchNameLower
- })
-
- if (exactMatches.length > 0) {
- let nameMatches = exactMatches
- for (const [condition, filterFn] of filters) {
- if (condition && nameMatches.length > 0) {
- const filtered = nameMatches.filter(filterFn)
- if (filtered.length > 0) nameMatches = filtered
- }
- }
-
- if (nameMatches.length > 0) return nameMatches[0]
- }
-
- let nameMatches = stores.filter((store) => {
- const storeNameLower = (store.name || '').toLowerCase().trim()
- return (
- storeNameLower.includes(searchNameLower) ||
- searchNameLower.includes(storeNameLower)
- )
- })
-
- for (const [condition, filterFn] of filters) {
- if (condition && nameMatches.length > 0) {
- const filtered = nameMatches.filter(filterFn)
- if (filtered.length > 0) nameMatches = filtered
- }
- }
-
- if (nameMatches.length > 0) return nameMatches[0]
- }
-
- if (zipcode) {
- const zipMatches = stores.filter((store) => {
- const storeZip = store.postalCode || store.address?.postalCode
- return storeZip === zipcode
- })
- if (zipMatches.length > 0) return zipMatches[0]
- }
-
- if (city) {
- const cityMatches = stores.filter((store) => {
- const storeCity = (store.city || store.address?.city || '').toLowerCase()
- return storeCity.includes(city.toLowerCase())
- })
- if (cityMatches.length > 0) return cityMatches[0]
- }
-
- return stores.length > 0 ? stores[0] : null
- }, [])
-
- useEffect(() => {
- if (hasItemsInBasket) {
- const urlParams = new URLSearchParams(location.search)
- const hasSeParamsList = ['lat', 'lng', 'zip', 'city', 'store', 'country']
- const hasSeParams = hasSeParamsList.some((key) => urlParams.has(key))
- if (hasSeParams) {
- cleanURLParams(location, history, hasSeParamsList)
- setLocationData(null)
- }
- return
- }
- if (storeSearchData?.data && locationData && isProcessing) {
- const countryCode = getCountryForPostalSearch(
- locationData.zipcode,
- locationData.countryCode
- )
- const searchCriteria = {
- ...locationData,
- countryCode: locationData.countryCode || countryCode
- }
-
- let selectedStore = null
- if (locationData.storeName) {
- selectedStore = findMatchingStore(storeSearchData.data, searchCriteria)
- }
-
- if (!selectedStore) {
- selectedStore =
- findMatchingStore(storeSearchData.data, searchCriteria) ||
- storeSearchData.data[0]
- }
-
- if (selectedStore) {
- if (storeLocatorContext?.setState) {
- storeLocatorContext.setState((prevState) => ({
- ...prevState,
- selectedStoreId: selectedStore.id,
- isSeSelection: true,
- mode: 'input',
- formValues: locationData.zipcode
- ? {
- countryCode: locationData.countryCode || countryCode,
- postalCode: locationData.zipcode
- }
- : {
- countryCode: selectedStore.countryCode,
- postalCode: selectedStore.postalCode
- }
- }))
- }
-
- if (locationData.storeName) {
- setStoreLocatorParams({
- postalCode: locationData.zipcode || selectedStore.postalCode,
- countryCode: locationData.countryCode || selectedStore.countryCode,
- limit: 50
- })
- } else if (locationData.latitude && locationData.longitude) {
- setStoreLocatorParams({
- latitude: locationData.latitude,
- longitude: locationData.longitude,
- postalCode: locationData.zipcode || selectedStore?.postalCode,
- countryCode: locationData.countryCode || selectedStore?.countryCode,
- limit: 50
- })
- } else if (locationData.zipcode) {
- setStoreLocatorParams({
- postalCode: locationData.zipcode,
- countryCode: countryCode,
- limit: 50
- })
- } else if (locationData.city) {
- setStoreLocatorParams({
- postalCode: selectedStore?.postalCode,
- countryCode: selectedStore?.countryCode || countryCode,
- limit: 50
- })
- }
-
- setShouldOpenModal(true)
- }
-
- setIsProcessing(false)
- }
- }, [storeSearchData, locationData, isProcessing, findMatchingStore, getCountryForPostalSearch])
-
- const processSeParameters = useCallback(
- (urlParams) => {
- const hasSeParams = ['lat', 'lng', 'zip', 'city', 'store', 'country'].some((p) =>
- urlParams.has(p)
- )
-
- if (!hasSeParams) {
- if (storeLocatorContext?.state?.isSeSelection) {
- return
- }
- return
- }
-
- const lat = urlParams.get('lat')
- const lng = urlParams.get('lng')
- const storeName = urlParams.get('store') || urlParams.get('Store')
- const zipcode = urlParams.get('zip')
- const city = urlParams.get('city')
- const country = urlParams.get('country')
-
- let parsedLat = null,
- parsedLng = null
-
- if (lat && lng) {
- parsedLat = parseFloat(lat)
- parsedLng = parseFloat(lng)
- }
-
- if (storeName) {
- setLocationData({
- storeName,
- zipcode,
- countryCode: country || STORE_LOCATOR_DEFAULT_COUNTRY_CODE
- })
- } else if (parsedLat && parsedLng) {
- setLocationData({
- latitude: parsedLat,
- longitude: parsedLng,
- countryCode: country
- })
- } else if (zipcode) {
- setLocationData({
- zipcode,
- countryCode: country
- })
- } else if (city) {
- setLocationData({
- city,
- countryCode: country
- })
- }
-
- setIsProcessing(true)
- },
- [storeLocatorContext?.state?.isSeSelection]
- )
-
- return {
- isProcessing: isProcessing || isLoadingStores,
- shouldOpenModal,
- setShouldOpenModal,
- storeLocatorParams,
- processSeParameters
- }
-}
-
-export default useSeStoreSelection
diff --git a/packages/template-retail-react-app/app/hooks/use-se-store-selection.test.js b/packages/template-retail-react-app/app/hooks/use-se-store-selection.test.js
deleted file mode 100644
index ca869d7d74..0000000000
--- a/packages/template-retail-react-app/app/hooks/use-se-store-selection.test.js
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
- * Copyright (c) 2024, salesforce.com, inc.
- * All rights reserved.
- * SPDX-License-Identifier: BSD-3-Clause
- * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
- */
-
-import {renderHook, act} from '@testing-library/react'
-import useSeStoreSelection from '@salesforce/retail-react-app/app/hooks/use-se-store-selection'
-import React from 'react'
-import {IntlProvider} from 'react-intl'
-import {BrowserRouter} from 'react-router-dom'
-import PropTypes from 'prop-types'
-import {StoreLocatorContext} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
-import {STORE_LOCATOR_DEFAULT_COUNTRY_CODE} from '../constants'
-import {useSearchStores} from '@salesforce/commerce-sdk-react'
-import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
-
-jest.mock('@salesforce/commerce-sdk-react', () => ({
- useSearchStores: jest.fn()
-}))
-
-jest.mock('@salesforce/retail-react-app/app/hooks/use-current-basket', () => ({
- useCurrentBasket: jest.fn(() => ({
- derivedData: {
- totalItems: 0
- }
- }))
-}))
-
-const mockStoreLocatorContext = {
- state: {
- selectedStoreId: null,
- isSeSelection: false,
- mode: 'input',
- formValues: {}
- },
- setState: jest.fn((callback) => {
- const newState =
- typeof callback === 'function' ? callback(mockStoreLocatorContext.state) : callback
- mockStoreLocatorContext.state = {...mockStoreLocatorContext.state, ...newState}
- return mockStoreLocatorContext.state
- })
-}
-
-const TestWrapper = ({children}) => (
-
-
-
- {children}
-
-
-
-)
-
-TestWrapper.propTypes = {
- children: PropTypes.node.isRequired
-}
-
-describe('useSeStoreSelection', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- mockStoreLocatorContext.setState.mockClear()
- mockStoreLocatorContext.state = {
- selectedStoreId: null,
- isSeSelection: false,
- mode: 'input',
- formValues: {}
- }
- useCurrentBasket.mockImplementation(() => ({
- derivedData: {
- totalItems: 0
- }
- }))
- useSearchStores.mockImplementation(() => ({
- data: [],
- isLoading: false,
- error: null
- }))
- })
-
- test('initializes with default values', () => {
- const {result} = renderHook(() => useSeStoreSelection(), {
- wrapper: TestWrapper
- })
-
- expect(result.current).toEqual({
- isProcessing: false,
- shouldOpenModal: false,
- setShouldOpenModal: expect.any(Function),
- storeLocatorParams: null,
- processSeParameters: expect.any(Function)
- })
- })
-
- test('handles coordinate-based search', async () => {
- useSearchStores.mockImplementation(() => ({
- data: {
- data: [
- {
- id: 'store1',
- name: 'Test Store',
- latitude: 37.7749,
- longitude: -122.4194,
- postalCode: '94105',
- countryCode: 'US'
- }
- ]
- },
- isLoading: false,
- error: null
- }))
-
- const {result} = renderHook(() => useSeStoreSelection(), {
- wrapper: TestWrapper
- })
-
- await act(async () => {
- const urlParams = new URLSearchParams('?lat=37.7749&lng=-122.4194')
- await result.current.processSeParameters(urlParams)
- })
-
- expect(mockStoreLocatorContext.state).toEqual({
- selectedStoreId: 'store1',
- isSeSelection: true,
- mode: 'input',
- formValues: {
- countryCode: 'US',
- postalCode: '94105'
- }
- })
- })
-
- test('handles postal code search', async () => {
- useSearchStores.mockImplementation(() => ({
- data: {
- data: [
- {
- id: 'store2',
- name: 'Test Store 2',
- postalCode: '94105',
- countryCode: STORE_LOCATOR_DEFAULT_COUNTRY_CODE
- }
- ]
- },
- isLoading: false,
- error: null
- }))
-
- const {result} = renderHook(() => useSeStoreSelection(), {
- wrapper: TestWrapper
- })
-
- await act(async () => {
- const urlParams = new URLSearchParams('?zip=94105')
- await result.current.processSeParameters(urlParams)
- })
-
- expect(mockStoreLocatorContext.state).toEqual({
- selectedStoreId: 'store2',
- isSeSelection: true,
- mode: 'input',
- formValues: {
- countryCode: STORE_LOCATOR_DEFAULT_COUNTRY_CODE,
- postalCode: '94105'
- }
- })
- })
-
- test('handles city search', async () => {
- useSearchStores.mockImplementation(() => ({
- data: {
- data: [
- {
- id: 'store3',
- name: 'Test Store 3',
- city: 'San Francisco',
- postalCode: '94105',
- countryCode: STORE_LOCATOR_DEFAULT_COUNTRY_CODE
- }
- ]
- },
- isLoading: false,
- error: null
- }))
-
- const {result} = renderHook(() => useSeStoreSelection(), {
- wrapper: TestWrapper
- })
-
- await act(async () => {
- const urlParams = new URLSearchParams('?city=San Francisco')
- await result.current.processSeParameters(urlParams)
- })
-
- expect(mockStoreLocatorContext.state).toEqual({
- selectedStoreId: 'store3',
- isSeSelection: true,
- mode: 'input',
- formValues: {
- countryCode: STORE_LOCATOR_DEFAULT_COUNTRY_CODE,
- postalCode: '94105'
- }
- })
- })
-
- test('handles empty store data', async () => {
- useSearchStores.mockImplementation(() => ({
- data: {
- data: []
- },
- isLoading: false,
- error: null
- }))
-
- const {result} = renderHook(() => useSeStoreSelection(), {
- wrapper: TestWrapper
- })
-
- await act(async () => {
- const urlParams = new URLSearchParams('?zip=94105')
- await result.current.processSeParameters(urlParams)
- })
-
- expect(mockStoreLocatorContext.state).toEqual({
- selectedStoreId: null,
- isSeSelection: false,
- mode: 'input',
- formValues: {}
- })
- })
-
- test('handles loading state', async () => {
- useSearchStores.mockImplementation(() => ({
- data: null,
- isLoading: true,
- error: null
- }))
-
- const {result} = renderHook(() => useSeStoreSelection(), {
- wrapper: TestWrapper
- })
-
- await act(async () => {
- const urlParams = new URLSearchParams('?zip=94105')
- await result.current.processSeParameters(urlParams)
- })
-
- expect(result.current.isProcessing).toBe(true)
- })
-
- test('handles error case', async () => {
- jest.spyOn(console, 'error').mockImplementation(() => {})
- useSearchStores.mockImplementation(() => ({
- data: null,
- isLoading: false,
- error: new Error('API Error')
- }))
-
- const {result} = renderHook(() => useSeStoreSelection(), {
- wrapper: TestWrapper
- })
-
- await act(async () => {
- const urlParams = new URLSearchParams('?zip=94105')
- await result.current.processSeParameters(urlParams)
- })
-
- expect(mockStoreLocatorContext.state).toEqual({
- selectedStoreId: null,
- isSeSelection: false,
- mode: 'input',
- formValues: {}
- })
- console.error.mockRestore()
- })
-
- test('handles basket integration', async () => {
- useCurrentBasket.mockImplementation(() => ({
- derivedData: {
- totalItems: 2
- }
- }))
-
- useSearchStores.mockImplementation(() => ({
- data: {
- data: [
- {
- id: 'store1',
- name: 'Test Store',
- postalCode: '94105',
- countryCode: STORE_LOCATOR_DEFAULT_COUNTRY_CODE
- }
- ]
- },
- isLoading: false,
- error: null
- }))
-
- const {result} = renderHook(() => useSeStoreSelection(), {
- wrapper: TestWrapper
- })
-
- await act(async () => {
- const urlParams = new URLSearchParams('?zip=94105')
- await result.current.processSeParameters(urlParams)
- result.current.setShouldOpenModal(true)
- })
-
- expect(result.current.shouldOpenModal).toBe(true)
- })
-})