diff --git a/packages/template-retail-react-app/app/components/_app-config/index.jsx b/packages/template-retail-react-app/app/components/_app-config/index.jsx
index e28be6db6a..242c95a60e 100644
--- a/packages/template-retail-react-app/app/components/_app-config/index.jsx
+++ b/packages/template-retail-react-app/app/components/_app-config/index.jsx
@@ -21,7 +21,7 @@ import {ChakraProvider} from '@salesforce/retail-react-app/app/components/shared
import 'focus-visible/dist/focus-visible'
import theme from '@salesforce/retail-react-app/app/theme'
-import {MultiSiteProvider} from '@salesforce/retail-react-app/app/contexts'
+import {MultiSiteProvider, StoreLocatorProvider} from '@salesforce/retail-react-app/app/contexts'
import {useAppOrigin} from '@salesforce/retail-react-app/app/hooks/use-app-origin'
import {
resolveSiteFromUrl,
@@ -35,7 +35,17 @@ import {CommerceApiProvider} from '@salesforce/commerce-sdk-react'
import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/components/with-react-query'
import {useCorrelationId} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
import {ReactQueryDevtools} from '@tanstack/react-query-devtools'
-import {DEFAULT_DNT_STATE} from '@salesforce/retail-react-app/app/constants'
+import {
+ DEFAULT_DNT_STATE,
+ STORE_LOCATOR_RADIUS,
+ STORE_LOCATOR_RADIUS_UNIT,
+ STORE_LOCATOR_DEFAULT_COUNTRY,
+ STORE_LOCATOR_DEFAULT_COUNTRY_CODE,
+ STORE_LOCATOR_DEFAULT_POSTAL_CODE,
+ STORE_LOCATOR_DEFAULT_PAGE_SIZE,
+ STORE_LOCATOR_SUPPORTED_COUNTRIES
+} from '@salesforce/retail-react-app/app/constants'
+
/**
* Use the AppConfig component to inject extra arguments into the getProps
* methods for all Route Components in the app – typically you'd want to do this
@@ -56,6 +66,16 @@ const AppConfig = ({children, locals = {}}) => {
const passwordlessCallback = locals.appConfig.login?.passwordless?.callbackURI
+ const storeLocatorConfig = {
+ radius: STORE_LOCATOR_RADIUS,
+ radiusUnit: STORE_LOCATOR_RADIUS_UNIT,
+ defaultCountry: STORE_LOCATOR_DEFAULT_COUNTRY,
+ defaultCountryCode: STORE_LOCATOR_DEFAULT_COUNTRY_CODE,
+ defaultPostalCode: STORE_LOCATOR_DEFAULT_POSTAL_CODE,
+ defaultPageSize: STORE_LOCATOR_DEFAULT_PAGE_SIZE,
+ supportedCountries: STORE_LOCATOR_SUPPORTED_COUNTRIES
+ }
+
return (
{
logger={createLogger({packageName: 'commerce-sdk-react'})}
>
- {children}
+
+ {children}
+
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 a841ef4930..6745dfb92e 100644
--- a/packages/template-retail-react-app/app/components/_app/index.jsx
+++ b/packages/template-retail-react-app/app/components/_app/index.jsx
@@ -46,7 +46,7 @@ import {DrawerMenu} from '@salesforce/retail-react-app/app/components/drawer-men
import {ListMenu, ListMenuContent} from '@salesforce/retail-react-app/app/components/list-menu'
import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive'
import AboveHeader from '@salesforce/retail-react-app/app/components/_app/partials/above-header'
-import StoreLocatorModal from '@salesforce/retail-react-app/app/components/store-locator-modal'
+import {StoreLocatorModal} from '@salesforce/retail-react-app/app/components/store-locator'
// Hooks
import {AuthModal, useAuthModal} from '@salesforce/retail-react-app/app/hooks/use-auth-modal'
import {
diff --git a/packages/template-retail-react-app/app/components/store-locator-modal/index.jsx b/packages/template-retail-react-app/app/components/store-locator-modal/index.jsx
deleted file mode 100644
index 8bb5920cbb..0000000000
--- a/packages/template-retail-react-app/app/components/store-locator-modal/index.jsx
+++ /dev/null
@@ -1,102 +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 React, {useState, createContext} from 'react'
-import PropTypes from 'prop-types'
-
-// Components
-import {
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- useBreakpointValue
-} from '@salesforce/retail-react-app/app/components/shared/ui'
-import StoreLocatorContent from '@salesforce/retail-react-app/app/components/store-locator-modal/store-locator-content'
-
-// Others
-import {
- DEFAULT_STORE_LOCATOR_COUNTRY,
- DEFAULT_STORE_LOCATOR_POSTAL_CODE,
- STORE_LOCATOR_NUM_STORES_PER_LOAD
-} from '@salesforce/retail-react-app/app/constants'
-
-export const StoreLocatorContext = createContext()
-export const useStoreLocator = () => {
- const [userHasSetManualGeolocation, setUserHasSetManualGeolocation] = useState(false)
- const [automaticGeolocationHasFailed, setAutomaticGeolocationHasFailed] = useState(false)
- const [userWantsToShareLocation, setUserWantsToShareLocation] = useState(false)
-
- const [searchStoresParams, setSearchStoresParams] = useState({
- countryCode: DEFAULT_STORE_LOCATOR_COUNTRY.countryCode,
- postalCode: DEFAULT_STORE_LOCATOR_POSTAL_CODE,
- limit: STORE_LOCATOR_NUM_STORES_PER_LOAD
- })
-
- return {
- userHasSetManualGeolocation,
- setUserHasSetManualGeolocation,
- automaticGeolocationHasFailed,
- setAutomaticGeolocationHasFailed,
- userWantsToShareLocation,
- setUserWantsToShareLocation,
- searchStoresParams,
- setSearchStoresParams
- }
-}
-
-const StoreLocatorModal = ({isOpen, onClose}) => {
- const storeLocator = useStoreLocator()
- const isDesktopView = useBreakpointValue({base: false, lg: true})
-
- return (
-
- {isDesktopView ? (
-
-
-
-
-
-
-
-
- ) : (
-
-
-
-
-
-
-
-
- )}
-
- )
-}
-
-StoreLocatorModal.propTypes = {
- isOpen: PropTypes.bool,
- onClose: PropTypes.func
-}
-
-export default StoreLocatorModal
diff --git a/packages/template-retail-react-app/app/components/store-locator-modal/index.test.jsx b/packages/template-retail-react-app/app/components/store-locator-modal/index.test.jsx
deleted file mode 100644
index 2fadf4cb89..0000000000
--- a/packages/template-retail-react-app/app/components/store-locator-modal/index.test.jsx
+++ /dev/null
@@ -1,212 +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 React from 'react'
-import StoreLocatorModal from '@salesforce/retail-react-app/app/components/store-locator-modal/index'
-import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
-import {rest} from 'msw'
-
-const mockStoresData = [
- {
- address1: '162 University Ave',
- city: 'Palo Alto',
- countryCode: 'US',
- distance: 0.0,
- distanceUnit: 'km',
- id: '00041',
- latitude: 37.189396,
- longitude: -121.705327,
- name: 'Palo Alto Store',
- posEnabled: false,
- postalCode: '94301',
- stateCode: 'CA',
- storeHours: 'THIS IS ENGLISH STORE HOURS',
- storeLocatorEnabled: true,
- c_countryCodeValue: 'US'
- },
- {
- address1: 'Holstenstraße 1',
- city: 'Kiel',
- countryCode: 'DE',
- distance: 8847.61,
- distanceUnit: 'km',
- id: '00031',
- inventoryId: 'inventory_m_store_store23',
- latitude: 54.3233,
- longitude: 10.1394,
- name: 'Kiel Electronics Store',
- phone: '+49 431 123456',
- posEnabled: false,
- postalCode: '24103',
- storeHours:
- 'Monday 9 AM–7 PM\nTuesday 9 AM–7 PM\nWednesday 9 AM–7 PM\nThursday 9 AM–8 PM\nFriday 9 AM–7 PM\nSaturday 9 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Heiligengeiststraße 2',
- city: 'Oldenburg',
- countryCode: 'DE',
- distance: 8873.75,
- distanceUnit: 'km',
- id: '00036',
- inventoryId: 'inventory_m_store_store28',
- latitude: 53.1445,
- longitude: 8.2146,
- name: 'Oldenburg Tech Depot',
- phone: '+49 441 876543',
- posEnabled: false,
- postalCode: '26121',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Obernstraße 2',
- city: 'Bremen',
- countryCode: 'DE',
- distance: 8904.18,
- distanceUnit: 'km',
- id: '00011',
- inventoryId: 'inventory_m_store_store2',
- latitude: 53.0765,
- longitude: 8.8085,
- name: 'Bremen Tech Store',
- phone: '+49 421 234567',
- posEnabled: false,
- postalCode: '28195',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Sögestraße 40',
- city: 'Bremen',
- countryCode: 'DE',
- distance: 8904.19,
- distanceUnit: 'km',
- id: '00026',
- inventoryId: 'inventory_m_store_store18',
- latitude: 53.0758,
- longitude: 8.8072,
- name: 'Bremen Tech World',
- phone: '+49 421 567890',
- posEnabled: false,
- postalCode: '28195',
- storeHours:
- 'Monday 9 AM–8 PM\nTuesday 9 AM–8 PM\nWednesday 9 AM–8 PM\nThursday 9 AM–9 PM\nFriday 9 AM–8 PM\nSaturday 9 AM–7 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Jungfernstieg 12',
- city: 'Hamburg',
- countryCode: 'DE',
- distance: 8910.05,
- distanceUnit: 'km',
- id: '00005',
- inventoryId: 'inventory_m_store_store5',
- latitude: 53.553405,
- longitude: 9.992196,
- name: 'Hamburg Electronics Outlet',
- phone: '+49 40 444444444',
- posEnabled: false,
- postalCode: '20354',
- storeHours:
- 'Monday 10 AM–8 PM\nTuesday 10 AM–8 PM\nWednesday 10 AM–8 PM\nThursday 10 AM–9 PM\nFriday 10 AM–8 PM\nSaturday 10 AM–7 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Große Straße 40',
- city: 'Osnabrück',
- countryCode: 'DE',
- distance: 8942.1,
- distanceUnit: 'km',
- id: '00037',
- inventoryId: 'inventory_m_store_store29',
- latitude: 52.2799,
- longitude: 8.0472,
- name: 'Osnabrück Tech Mart',
- phone: '+49 541 654321',
- posEnabled: false,
- postalCode: '49074',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Kröpeliner Straße 48',
- city: 'Rostock',
- countryCode: 'DE',
- distance: 8945.47,
- distanceUnit: 'km',
- id: '00032',
- inventoryId: 'inventory_m_store_store24',
- latitude: 54.0899,
- longitude: 12.1349,
- name: 'Rostock Tech Store',
- phone: '+49 381 234567',
- posEnabled: false,
- postalCode: '18055',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Kennedyplatz 7',
- city: 'Essen',
- countryCode: 'DE',
- distance: 8969.09,
- distanceUnit: 'km',
- id: '00013',
- inventoryId: 'inventory_m_store_store4',
- latitude: 51.4566,
- longitude: 7.0125,
- name: 'Essen Electronics Depot',
- phone: '+49 201 456789',
- posEnabled: false,
- postalCode: '45127',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Kettwiger Straße 17',
- city: 'Essen',
- countryCode: 'DE',
- distance: 8969.13,
- distanceUnit: 'km',
- id: '00030',
- inventoryId: 'inventory_m_store_store22',
- latitude: 51.4556,
- longitude: 7.0116,
- name: 'Essen Tech Hub',
- phone: '+49 201 654321',
- posEnabled: false,
- postalCode: '45127',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- }
-]
-const mockStores = {
- limit: 10,
- data: mockStoresData,
- offset: 0,
- total: 30
-}
-
-describe('StoreLocatorModal', () => {
- test('renders without crashing', () => {
- global.server.use(
- rest.get('*/shopper-stores/v1/organizations/*', (req, res, ctx) => {
- return res(ctx.delay(0), ctx.status(200), ctx.json(mockStores))
- })
- )
- expect(() => {
- renderWithProviders()
- }).not.toThrow()
- })
-})
diff --git a/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-content.jsx b/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-content.jsx
deleted file mode 100644
index 10bf5b80f2..0000000000
--- a/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-content.jsx
+++ /dev/null
@@ -1,213 +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 React, {useState, useContext} from 'react'
-import {useIntl} from 'react-intl'
-
-// Components
-import {
- Heading,
- Accordion,
- AccordionItem,
- Box,
- Button
-} from '@salesforce/retail-react-app/app/components/shared/ui'
-import StoresList from '@salesforce/retail-react-app/app/components/store-locator-modal/stores-list'
-import StoreLocatorInput from '@salesforce/retail-react-app/app/components/store-locator-modal/store-locator-input'
-
-// Others
-import {
- SUPPORTED_STORE_LOCATOR_COUNTRIES,
- DEFAULT_STORE_LOCATOR_COUNTRY,
- STORE_LOCATOR_DISTANCE,
- STORE_LOCATOR_NUM_STORES_PER_LOAD,
- STORE_LOCATOR_DISTANCE_UNIT
-} from '@salesforce/retail-react-app/app/constants'
-
-//This is an API limit and is therefore not configurable
-const NUM_STORES_PER_REQUEST_API_MAX = 200
-
-// Hooks
-import {useSearchStores} from '@salesforce/commerce-sdk-react'
-import {useForm} from 'react-hook-form'
-
-import {StoreLocatorContext} from '@salesforce/retail-react-app/app/components/store-locator-modal/index'
-
-const StoreLocatorContent = () => {
- const {
- searchStoresParams,
- setSearchStoresParams,
- userHasSetManualGeolocation,
- setUserHasSetManualGeolocation
- } = useContext(StoreLocatorContext)
- const {countryCode, postalCode, latitude, longitude, limit} = searchStoresParams
- const intl = useIntl()
- const form = useForm({
- mode: 'onChange',
- reValidateMode: 'onChange',
- defaultValues: {
- countryCode: userHasSetManualGeolocation ? countryCode : '',
- postalCode: userHasSetManualGeolocation ? postalCode : ''
- }
- })
-
- const [numStoresToShow, setNumStoresToShow] = useState(limit)
- // Either the countryCode & postalCode or latitude & longitude are defined, never both
- const {
- data: searchStoresData,
- isLoading,
- refetch,
- isFetching
- } = useSearchStores({
- parameters: {
- countryCode: countryCode,
- postalCode: postalCode,
- latitude: latitude,
- longitude: longitude,
- locale: intl.locale,
- maxDistance: STORE_LOCATOR_DISTANCE,
- limit: NUM_STORES_PER_REQUEST_API_MAX,
- distanceUnit: STORE_LOCATOR_DISTANCE_UNIT
- }
- })
-
- const storesInfo =
- isLoading || isFetching
- ? undefined
- : searchStoresData?.data?.slice(0, numStoresToShow) || []
- const numStores = searchStoresData?.total || 0
-
- const submitForm = async (formData) => {
- const {postalCode, countryCode} = formData
- if (postalCode !== '') {
- if (countryCode !== '') {
- setSearchStoresParams({
- postalCode: postalCode,
- countryCode: countryCode,
- limit: STORE_LOCATOR_NUM_STORES_PER_LOAD
- })
- setUserHasSetManualGeolocation(true)
- } else {
- if (SUPPORTED_STORE_LOCATOR_COUNTRIES.length === 0) {
- setSearchStoresParams({
- postalCode: postalCode,
- countryCode: DEFAULT_STORE_LOCATOR_COUNTRY.countryCode,
- limit: STORE_LOCATOR_NUM_STORES_PER_LOAD
- })
- setUserHasSetManualGeolocation(true)
- }
- }
- }
- // Reset the number of stores in the UI
- setNumStoresToShow(STORE_LOCATOR_NUM_STORES_PER_LOAD)
-
- // Ensures API call is made regardless of caching to provide UX feedback on click
- refetch()
- }
-
- const displayStoreLocatorStatusMessage = () => {
- if (storesInfo === undefined)
- return intl.formatMessage({
- id: 'store_locator.description.loading_locations',
- defaultMessage: 'Loading locations...'
- })
- if (storesInfo.length === 0)
- return intl.formatMessage({
- id: 'store_locator.description.no_locations',
- defaultMessage: 'Sorry, there are no locations in this area'
- })
- if (searchStoresParams.postalCode !== undefined)
- return `${intl.formatMessage(
- {
- id: 'store_locator.description.viewing_near_postal_code',
- defaultMessage:
- 'Viewing stores within {distance}{distanceUnit} of {postalCode} in '
- },
- {
- distance: STORE_LOCATOR_DISTANCE,
- distanceUnit: STORE_LOCATOR_DISTANCE_UNIT,
- postalCode: searchStoresParams.postalCode
- }
- )}
- ${
- SUPPORTED_STORE_LOCATOR_COUNTRIES.length !== 0
- ? intl.formatMessage(
- SUPPORTED_STORE_LOCATOR_COUNTRIES.find(
- (o) => o.countryCode === searchStoresParams.countryCode
- ).countryName
- )
- : intl.formatMessage(DEFAULT_STORE_LOCATOR_COUNTRY.countryName)
- }`
- else
- return intl.formatMessage({
- id: 'store_locator.description.viewing_near_your_location',
- defaultMessage: 'Viewing stores near your location'
- })
- }
-
- return (
- <>
-
- {intl.formatMessage({
- id: 'store_locator.title',
- defaultMessage: 'Find a Store'
- })}
-
-
-
- {/* Details */}
-
-
- {displayStoreLocatorStatusMessage()}
-
-
-
-
- {!isFetching &&
- numStoresToShow < numStores &&
- numStoresToShow < NUM_STORES_PER_REQUEST_API_MAX ? (
-
-
-
- ) : (
- ''
- )}
- >
- )
-}
-
-StoreLocatorContent.propTypes = {}
-
-export default StoreLocatorContent
diff --git a/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-content.test.jsx b/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-content.test.jsx
deleted file mode 100644
index 38e8c8458c..0000000000
--- a/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-content.test.jsx
+++ /dev/null
@@ -1,399 +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 React, {useEffect} from 'react'
-import StoreLocatorContent from '@salesforce/retail-react-app/app/components/store-locator-modal/store-locator-content'
-import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
-import {waitFor, screen} from '@testing-library/react'
-import PropTypes from 'prop-types'
-import {STORE_LOCATOR_NUM_STORES_PER_LOAD} from '@salesforce/retail-react-app/app/constants'
-import {rest} from 'msw'
-import {StoreLocatorContext} from '@salesforce/retail-react-app/app/components/store-locator-modal/index'
-import {useStoreLocator} from '@salesforce/retail-react-app/app/components/store-locator-modal/index'
-const mockStoresData = [
- {
- address1: '162 University Ave',
- city: 'Palo Alto',
- countryCode: 'US',
- distance: 0.0,
- distanceUnit: 'km',
- id: '00041',
- latitude: 37.189396,
- longitude: -121.705327,
- name: 'Palo Alto Store',
- posEnabled: false,
- postalCode: '94301',
- stateCode: 'CA',
- storeHours: 'THIS IS ENGLISH STORE HOURS',
- storeLocatorEnabled: true,
- c_countryCodeValue: 'US'
- },
- {
- address1: 'Holstenstraße 1',
- city: 'Kiel',
- countryCode: 'DE',
- distance: 8847.61,
- distanceUnit: 'km',
- id: '00031',
- inventoryId: 'inventory_m_store_store23',
- latitude: 54.3233,
- longitude: 10.1394,
- name: 'Kiel Electronics Store',
- phone: '+49 431 123456',
- posEnabled: false,
- postalCode: '24103',
- storeHours:
- 'Monday 9 AM–7 PM\nTuesday 9 AM–7 PM\nWednesday 9 AM–7 PM\nThursday 9 AM–8 PM\nFriday 9 AM–7 PM\nSaturday 9 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Heiligengeiststraße 2',
- city: 'Oldenburg',
- countryCode: 'DE',
- distance: 8873.75,
- distanceUnit: 'km',
- id: '00036',
- inventoryId: 'inventory_m_store_store28',
- latitude: 53.1445,
- longitude: 8.2146,
- name: 'Oldenburg Tech Depot',
- phone: '+49 441 876543',
- posEnabled: false,
- postalCode: '26121',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Obernstraße 2',
- city: 'Bremen',
- countryCode: 'DE',
- distance: 8904.18,
- distanceUnit: 'km',
- id: '00011',
- inventoryId: 'inventory_m_store_store2',
- latitude: 53.0765,
- longitude: 8.8085,
- name: 'Bremen Tech Store',
- phone: '+49 421 234567',
- posEnabled: false,
- postalCode: '28195',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Sögestraße 40',
- city: 'Bremen',
- countryCode: 'DE',
- distance: 8904.19,
- distanceUnit: 'km',
- id: '00026',
- inventoryId: 'inventory_m_store_store18',
- latitude: 53.0758,
- longitude: 8.8072,
- name: 'Bremen Tech World',
- phone: '+49 421 567890',
- posEnabled: false,
- postalCode: '28195',
- storeHours:
- 'Monday 9 AM–8 PM\nTuesday 9 AM–8 PM\nWednesday 9 AM–8 PM\nThursday 9 AM–9 PM\nFriday 9 AM–8 PM\nSaturday 9 AM–7 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Jungfernstieg 12',
- city: 'Hamburg',
- countryCode: 'DE',
- distance: 8910.05,
- distanceUnit: 'km',
- id: '00005',
- inventoryId: 'inventory_m_store_store5',
- latitude: 53.553405,
- longitude: 9.992196,
- name: 'Hamburg Electronics Outlet',
- phone: '+49 40 444444444',
- posEnabled: false,
- postalCode: '20354',
- storeHours:
- 'Monday 10 AM–8 PM\nTuesday 10 AM–8 PM\nWednesday 10 AM–8 PM\nThursday 10 AM–9 PM\nFriday 10 AM–8 PM\nSaturday 10 AM–7 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Große Straße 40',
- city: 'Osnabrück',
- countryCode: 'DE',
- distance: 8942.1,
- distanceUnit: 'km',
- id: '00037',
- inventoryId: 'inventory_m_store_store29',
- latitude: 52.2799,
- longitude: 8.0472,
- name: 'Osnabrück Tech Mart',
- phone: '+49 541 654321',
- posEnabled: false,
- postalCode: '49074',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Kröpeliner Straße 48',
- city: 'Rostock',
- countryCode: 'DE',
- distance: 8945.47,
- distanceUnit: 'km',
- id: '00032',
- inventoryId: 'inventory_m_store_store24',
- latitude: 54.0899,
- longitude: 12.1349,
- name: 'Rostock Tech Store',
- phone: '+49 381 234567',
- posEnabled: false,
- postalCode: '18055',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Kennedyplatz 7',
- city: 'Essen',
- countryCode: 'DE',
- distance: 8969.09,
- distanceUnit: 'km',
- id: '00013',
- inventoryId: 'inventory_m_store_store4',
- latitude: 51.4566,
- longitude: 7.0125,
- name: 'Essen Electronics Depot',
- phone: '+49 201 456789',
- posEnabled: false,
- postalCode: '45127',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Kettwiger Straße 17',
- city: 'Essen',
- countryCode: 'DE',
- distance: 8969.13,
- distanceUnit: 'km',
- id: '00030',
- inventoryId: 'inventory_m_store_store22',
- latitude: 51.4556,
- longitude: 7.0116,
- name: 'Essen Tech Hub',
- phone: '+49 201 654321',
- posEnabled: false,
- postalCode: '45127',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- }
-]
-const mockStoresTotalIsHigherThanLimit = {
- limit: 10,
- data: mockStoresData,
- offset: 0,
- total: 30
-}
-
-const mockStoresTotalIsEqualToLimit = {
- limit: 10,
- data: mockStoresData,
- offset: 0,
- total: 10
-}
-
-const mockNoStores = {
- limit: 0,
- total: 0
-}
-
-const WrapperComponent = ({searchStoresParams, userHasSetManualGeolocation}) => {
- const storeLocator = useStoreLocator()
- useEffect(() => {
- storeLocator.setSearchStoresParams(searchStoresParams)
- storeLocator.setUserHasSetManualGeolocation(userHasSetManualGeolocation)
- }, [])
- return (
-
-
-
- )
-}
-WrapperComponent.propTypes = {
- storesInfo: PropTypes.array,
- userHasSetManualGeolocation: PropTypes.bool,
- searchStoresParams: PropTypes.object
-}
-
-describe('StoreLocatorContent', () => {
- test('renders without crashing', () => {
- global.server.use(
- rest.get('*/shopper-stores/v1/organizations/*', (req, res, ctx) => {
- return res(
- ctx.delay(0),
- ctx.status(200),
- ctx.json(mockStoresTotalIsHigherThanLimit)
- )
- })
- )
- expect(() => {
- renderWithProviders(
-
- )
- }).not.toThrow()
- })
-
- test('Expected information exists', async () => {
- global.server.use(
- rest.get('*/shopper-stores/v1/organizations/*', (req, res, ctx) => {
- return res(
- ctx.delay(0),
- ctx.status(200),
- ctx.json(mockStoresTotalIsHigherThanLimit)
- )
- })
- )
- renderWithProviders(
-
- )
-
- await waitFor(async () => {
- const findButton = screen.getByRole('button', {name: /Find/i})
- const useMyLocationButton = screen.getByRole('button', {name: /Use My Location/i})
- const descriptionFindAStore = screen.getByText(/Find a Store/i)
- const viewing = screen.getByText(/Viewing stores within 100km of 10178 in Germany/i)
-
- expect(findButton).toBeInTheDocument()
- expect(useMyLocationButton).toBeInTheDocument()
- expect(descriptionFindAStore).toBeInTheDocument()
- expect(viewing).toBeInTheDocument()
- })
- })
-
- test('No stores text exists', async () => {
- global.server.use(
- rest.get('*/shopper-stores/v1/organizations/*', (req, res, ctx) => {
- return res(ctx.delay(0), ctx.status(200), ctx.json(mockNoStores))
- })
- )
- renderWithProviders(
-
- )
-
- await waitFor(async () => {
- const noLocations = screen.getByText(/Sorry, there are no locations in this area/i)
-
- expect(noLocations).toBeInTheDocument()
- })
- })
-
- test('Near your location text exists', async () => {
- global.server.use(
- rest.get('*/shopper-stores/v1/organizations/*', (req, res, ctx) => {
- return res(
- ctx.delay(0),
- ctx.status(200),
- ctx.json(mockStoresTotalIsHigherThanLimit)
- )
- })
- )
-
- renderWithProviders(
-
- )
-
- await waitFor(async () => {
- const nearYourLocation = screen.getByText(/Viewing stores near your location/i)
-
- expect(nearYourLocation).toBeInTheDocument()
- })
- })
-
- test('Load More button exists when total stores is higher than display limit', async () => {
- global.server.use(
- rest.get('*/shopper-stores/v1/organizations/*', (req, res, ctx) => {
- return res(
- ctx.delay(0),
- ctx.status(200),
- ctx.json(mockStoresTotalIsHigherThanLimit)
- )
- })
- )
-
- renderWithProviders(
-
- )
- await waitFor(async () => {
- const findButton = screen.getByRole('button', {name: /Find/i})
- const aStore = screen.getByText(/162 University Ave/i)
- const loadMore = screen.getByText(/Load More/i)
- expect(findButton).toBeInTheDocument()
- expect(aStore).toBeInTheDocument()
- expect(loadMore).toBeInTheDocument()
- })
- })
-
- test('Load More button doesnt exist when total stores is equal to display limit', async () => {
- global.server.use(
- rest.get('*/shopper-stores/v1/organizations/*', (req, res, ctx) => {
- return res(ctx.delay(0), ctx.status(200), ctx.json(mockStoresTotalIsEqualToLimit))
- })
- )
- renderWithProviders(
-
- )
- await waitFor(() => {
- const loadMore = screen.queryByText(/Load More/i)
- expect(loadMore).not.toBeInTheDocument()
- })
- })
-})
diff --git a/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-input.jsx b/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-input.jsx
deleted file mode 100644
index c7c4bc6321..0000000000
--- a/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-input.jsx
+++ /dev/null
@@ -1,230 +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 React, {useEffect, useContext} from 'react'
-import {useIntl} from 'react-intl'
-import PropTypes from 'prop-types'
-
-// Components
-import {
- Button,
- InputGroup,
- Select,
- Box,
- Input,
- FormControl,
- FormErrorMessage
-} from '@salesforce/retail-react-app/app/components/shared/ui'
-import {AlertIcon} from '@salesforce/retail-react-app/app/components/icons'
-import {Controller} from 'react-hook-form'
-
-// Others
-import {
- SUPPORTED_STORE_LOCATOR_COUNTRIES,
- STORE_LOCATOR_NUM_STORES_PER_LOAD
-} from '@salesforce/retail-react-app/app/constants'
-import {StoreLocatorContext} from '@salesforce/retail-react-app/app/components/store-locator-modal/index'
-
-const useGeolocation = () => {
- const {
- setSearchStoresParams,
- setAutomaticGeolocationHasFailed,
- setUserHasSetManualGeolocation,
- userHasSetManualGeolocation
- } = useContext(StoreLocatorContext)
-
- const getGeolocationError = () => {
- setAutomaticGeolocationHasFailed(true)
- }
- const getGeolocationSuccess = (position) => {
- setAutomaticGeolocationHasFailed(false)
- setSearchStoresParams({
- latitude: position.coords.latitude,
- longitude: position.coords.longitude,
- limit: STORE_LOCATOR_NUM_STORES_PER_LOAD
- })
- }
-
- const getUserGeolocation = () => {
- if (navigator?.geolocation) {
- navigator.geolocation.getCurrentPosition(getGeolocationSuccess, getGeolocationError)
- setUserHasSetManualGeolocation(false)
- } else {
- console.log('Geolocation not supported')
- }
- }
-
- useEffect(() => {
- if (!userHasSetManualGeolocation) getUserGeolocation()
- }, [])
-
- return getUserGeolocation
-}
-
-const StoreLocatorInput = ({form, submitForm}) => {
- const {
- searchStoresParams,
- userHasSetManualGeolocation,
- automaticGeolocationHasFailed,
- setUserWantsToShareLocation,
- userWantsToShareLocation
- } = useContext(StoreLocatorContext)
-
- const getUserGeolocation = useGeolocation()
- const {control} = form
- const intl = useIntl()
- return (
-
- )
-}
-
-StoreLocatorInput.propTypes = {
- form: PropTypes.object,
- submitForm: PropTypes.func
-}
-
-export default StoreLocatorInput
diff --git a/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-input.test.jsx b/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-input.test.jsx
deleted file mode 100644
index 03d1c6880b..0000000000
--- a/packages/template-retail-react-app/app/components/store-locator-modal/store-locator-input.test.jsx
+++ /dev/null
@@ -1,75 +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 React, {useEffect} from 'react'
-import StoreLocatorInput from '@salesforce/retail-react-app/app/components/store-locator-modal/store-locator-input'
-import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
-import {waitFor, screen} from '@testing-library/react'
-import {useForm} from 'react-hook-form'
-import PropTypes from 'prop-types'
-import {StoreLocatorContext} from '@salesforce/retail-react-app/app/components/store-locator-modal/index'
-import {useStoreLocator} from '@salesforce/retail-react-app/app/components/store-locator-modal/index'
-import {STORE_LOCATOR_NUM_STORES_PER_LOAD} from '@salesforce/retail-react-app/app/constants'
-
-const WrapperComponent = ({userHasSetManualGeolocation}) => {
- const form = useForm({
- mode: 'onChange',
- reValidateMode: 'onChange',
- defaultValues: {
- countryCode: 'DE',
- postalCode: '10178'
- }
- })
- const storeLocator = useStoreLocator()
- useEffect(() => {
- storeLocator.setUserHasSetManualGeolocation(userHasSetManualGeolocation)
- storeLocator.setSearchStoresParams({
- postalCode: '10178',
- countryCode: 'DE',
- limit: STORE_LOCATOR_NUM_STORES_PER_LOAD
- })
- }, [])
-
- return (
-
-
-
- )
-}
-WrapperComponent.propTypes = {
- storesInfo: PropTypes.array,
- userHasSetManualGeolocation: PropTypes.bool,
- getUserGeolocation: PropTypes.func
-}
-
-describe('StoreLocatorInput', () => {
- afterEach(() => {
- jest.clearAllMocks()
- jest.resetModules()
- })
- test('Renders without crashing', () => {
- expect(() => {
- renderWithProviders(
-
- )
- }).not.toThrow()
- })
-
- test('Expected information exists', async () => {
- renderWithProviders(
-
- )
-
- await waitFor(async () => {
- const findButton = screen.getByRole('button', {name: /Find/i})
- const useMyLocationButton = screen.getByRole('button', {name: /Use My Location/i})
-
- expect(findButton).toBeInTheDocument()
- expect(useMyLocationButton).toBeInTheDocument()
- })
- })
-})
diff --git a/packages/template-retail-react-app/app/components/store-locator-modal/stores-list.jsx b/packages/template-retail-react-app/app/components/store-locator-modal/stores-list.jsx
deleted file mode 100644
index a3fbd0a289..0000000000
--- a/packages/template-retail-react-app/app/components/store-locator-modal/stores-list.jsx
+++ /dev/null
@@ -1,131 +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 React, {useEffect, useState} from 'react'
-import {useIntl} from 'react-intl'
-import PropTypes from 'prop-types'
-
-// Components
-import {
- AccordionItem,
- AccordionButton,
- AccordionIcon,
- AccordionPanel,
- Box,
- HStack,
- Radio,
- RadioGroup
-} from '@salesforce/retail-react-app/app/components/shared/ui'
-
-// Hooks
-import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
-
-const StoresList = ({storesInfo}) => {
- const intl = useIntl()
- const {site} = useMultiSite()
- const storeInfoKey = `store_${site.id}`
- const [selectedStore, setSelectedStore] = useState('')
-
- useEffect(() => {
- setSelectedStore(JSON.parse(window.localStorage.getItem(storeInfoKey))?.id || '')
- }, [storeInfoKey])
-
- const handleChange = (storeId) => {
- setSelectedStore(storeId)
- const store = storesInfo.find((store) => store.id === storeId)
- window.localStorage.setItem(
- storeInfoKey,
- JSON.stringify({
- id: storeId,
- name: store.name || null,
- inventoryId: store.inventoryId || null
- })
- )
- }
-
- return (
-
- {storesInfo?.map((store, index) => {
- return (
-
-
-
-
- {store.name && {store.name}}
-
- {store.address1}
-
-
- {store.city}, {store.stateCode ? store.stateCode : ''}{' '}
- {store.postalCode}
-
- {store.distance !== undefined && (
- <>
-
-
- {store.distance} {store.distanceUnit}{' '}
- {intl.formatMessage({
- id: 'store_locator.description.away',
- defaultMessage: 'away'
- })}
-
- >
- )}
- {store.phone && (
- <>
-
-
- {intl.formatMessage({
- id: 'store_locator.description.phone',
- defaultMessage: 'Phone:'
- })}{' '}
- {store.phone}
-
- >
- )}
- {store.storeHours && (
- <>
- {' '}
-
-
- {intl.formatMessage({
- id: 'store_locator.action.viewMore',
- defaultMessage: 'View More'
- })}
-
-
-
-
-
- {' '}
- >
- )}
-
-
-
- )
- })}
-
- )
-}
-
-StoresList.propTypes = {
- storesInfo: PropTypes.array
-}
-
-export default StoresList
diff --git a/packages/template-retail-react-app/app/components/store-locator-modal/stores-list.test.jsx b/packages/template-retail-react-app/app/components/store-locator-modal/stores-list.test.jsx
deleted file mode 100644
index a326ef38c0..0000000000
--- a/packages/template-retail-react-app/app/components/store-locator-modal/stores-list.test.jsx
+++ /dev/null
@@ -1,199 +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 React from 'react'
-import StoresList from '@salesforce/retail-react-app/app/components/store-locator-modal/stores-list'
-import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
-import {waitFor, screen, fireEvent} from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import {Accordion} from '@salesforce/retail-react-app/app/components/shared/ui'
-import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
-
-const mockSearchStoresData = [
- {
- address1: 'Kirchgasse 12',
- city: 'Wiesbaden',
- countryCode: 'DE',
- distance: 0.74,
- distanceUnit: 'km',
- id: '00019',
- inventoryId: 'inventory_m_store_store11',
- latitude: 50.0826,
- longitude: 8.24,
- name: 'Wiesbaden Tech Depot',
- phone: '+49 611 876543',
- posEnabled: false,
- postalCode: '65185',
- storeHours: 'Monday 9 AM to 7 PM',
- storeLocatorEnabled: true
- },
- {
- address1: 'Schaumainkai 63',
- city: 'Frankfurt am Main',
- countryCode: 'DE',
- distance: 30.78,
- distanceUnit: 'km',
- id: '00002',
- inventoryId: 'inventory_m_store_store4',
- latitude: 50.097416,
- longitude: 8.669059,
- name: 'Frankfurt Electronics Store',
- phone: '+49 69 111111111',
- posEnabled: false,
- postalCode: '60596',
- storeHours:
- 'Monday 10 AM–6 PM\nTuesday 10 AM–6 PM\nWednesday 10 AM–6 PM\nThursday 10 AM–9 PM\nFriday 10 AM–6 PM\nSaturday 10 AM–6 PM\nSunday 10 AM–6 PM',
- storeLocatorEnabled: true
- },
- {
- address1: 'Löhrstraße 87',
- city: 'Koblenz',
- countryCode: 'DE',
- distance: 55.25,
- distanceUnit: 'km',
- id: '00035',
- inventoryId: 'inventory_m_store_store27',
- latitude: 50.3533,
- longitude: 7.5946,
- name: 'Koblenz Electronics Store',
- phone: '+49 261 123456',
- posEnabled: false,
- postalCode: '56068',
- storeHours:
- 'Monday 9 AM–7 PM\nTuesday 9 AM–7 PM\nWednesday 9 AM–7 PM\nThursday 9 AM–8 PM\nFriday 9 AM–7 PM\nSaturday 9 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Hauptstraße 47',
- city: 'Heidelberg',
- countryCode: 'DE',
- distance: 81.1,
- distanceUnit: 'km',
- id: '00021',
- latitude: 49.4077,
- longitude: 8.6908,
- name: 'Store with no inventoryId',
- phone: '+49 6221 123456',
- posEnabled: false,
- postalCode: '69117',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- }
-]
-
-describe('StoresList', () => {
- test('renders without crashing', () => {
- expect(() => {
- renderWithProviders(
-
-
-
- )
- }).not.toThrow()
- })
-
- test('Expected information exists', async () => {
- renderWithProviders(
-
-
-
- )
-
- expect(screen.queryAllByRole('radio')).toHaveLength(mockSearchStoresData.length)
-
- await waitFor(async () => {
- mockSearchStoresData.forEach((store) => {
- const storeName = screen.getByText(store.name)
- const storeAddress = screen.getByText(store.address1)
- const storeCityAndPostalCode = screen.getByText(
- `${store.city}, ${store.postalCode}`
- )
- const storeDistance = screen.getByText(
- `${store.distance} ${store.distanceUnit} away`
- )
- const storePhoneNumber = screen.getByText(`Phone: ${store.phone}`)
-
- expect(storeName).toBeInTheDocument()
- expect(storeAddress).toBeInTheDocument()
- expect(storeCityAndPostalCode).toBeInTheDocument()
- expect(storeDistance).toBeInTheDocument()
- expect(storePhoneNumber).toBeInTheDocument()
- })
- })
- })
-
- test('Clicking View More opens store hours', async () => {
- renderWithProviders(
-
-
-
- )
-
- await waitFor(async () => {
- const viewMoreButtons = screen.getAllByRole('button', {name: /View More/i})
-
- // Click on the first button
- await userEvent.click(viewMoreButtons[0])
-
- const aStoreOpenHours = screen.getByText(/Monday\s*9\s*AM\s*to\s*7\s*PM/i)
- expect(aStoreOpenHours).toBeInTheDocument()
- })
- })
-
- test('Is sorted by distance away', async () => {
- renderWithProviders(
-
-
-
- )
- await waitFor(async () => {
- const numbers = [
- screen.getByText(/\s*0\.74\s*km\s*away\s*/),
- screen.getByText(/\s*30\.78\s*km\s*away\s*/),
- screen.getByText(/\s*55\.25\s*km\s*away\s*/),
- screen.getByText(/\s*81\.1\s*km\s*away\s*/)
- ]
-
- // Check that the numbers are in the document
- numbers.forEach((number) => {
- expect(number).toBeInTheDocument()
- })
-
- // Check that the numbers are in the correct order
- const numberTexts = numbers.map((number) => number.textContent)
- expect(numberTexts).toEqual([
- '0.74 km away',
- '30.78 km away',
- '55.25 km away',
- '81.1 km away'
- ])
- // Check that the numbers are in the correct visual order
- const positions = numbers.map((number) => number.getBoundingClientRect().top)
- expect(positions).toEqual([...positions].sort((a, b) => a - b))
- })
- })
-
- test('Can select store', async () => {
- renderWithProviders(
-
-
-
- )
-
- await waitFor(async () => {
- const {id, name, inventoryId} = mockSearchStoresData[1]
- const radioButton = screen.getByDisplayValue(id)
- fireEvent.click(radioButton)
-
- const expectedStoreInfo = {id, name, inventoryId}
- expect(localStorage.getItem(`store_${mockConfig.app.defaultSite}`)).toEqual(
- JSON.stringify(expectedStoreInfo)
- )
- })
- })
-})
diff --git a/packages/template-retail-react-app/app/components/store-locator/diagram.png b/packages/template-retail-react-app/app/components/store-locator/diagram.png
new file mode 100644
index 0000000000..5cc09de330
Binary files /dev/null and b/packages/template-retail-react-app/app/components/store-locator/diagram.png differ
diff --git a/packages/template-retail-react-app/app/components/store-locator/form.jsx b/packages/template-retail-react-app/app/components/store-locator/form.jsx
new file mode 100644
index 0000000000..c4c602fafb
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/form.jsx
@@ -0,0 +1,157 @@
+/*
+ * 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, {useEffect} from 'react'
+import {
+ Box,
+ Button,
+ InputGroup,
+ Select,
+ FormControl,
+ FormErrorMessage,
+ Input
+} from '@chakra-ui/react'
+import {useForm, Controller} from 'react-hook-form'
+import {useStoreLocator} from '@salesforce/retail-react-app/app/hooks/use-store-locator'
+import {useGeolocation} from '@salesforce/retail-react-app/app/hooks/use-geo-location'
+
+export const StoreLocatorForm = () => {
+ const {config, formValues, setFormValues, setDeviceCoordinates} = useStoreLocator()
+ const {coordinates, error, refresh} = useGeolocation()
+ const form = useForm({
+ mode: 'onChange',
+ reValidateMode: 'onChange',
+ defaultValues: {
+ countryCode: formValues.countryCode,
+ postalCode: formValues.postalCode
+ }
+ })
+ const {control} = form
+ useEffect(() => {
+ if (coordinates.latitude && coordinates.longitude) {
+ setDeviceCoordinates(coordinates)
+ }
+ }, [coordinates])
+
+ const showCountrySelector = config.supportedCountries.length > 0
+
+ const submitForm = (formValues) => {
+ setFormValues(formValues)
+ }
+
+ const clearForm = () => {
+ form.reset()
+ setFormValues({
+ countryCode: '',
+ postalCode: ''
+ })
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/template-retail-react-app/app/components/store-locator/form.test.jsx b/packages/template-retail-react-app/app/components/store-locator/form.test.jsx
new file mode 100644
index 0000000000..9382508945
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/form.test.jsx
@@ -0,0 +1,150 @@
+/*
+ * 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 {render, screen} from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import {StoreLocatorForm} from '@salesforce/retail-react-app/app/components/store-locator/form'
+import {useStoreLocator} from '@salesforce/retail-react-app/app/hooks/use-store-locator'
+import {useGeolocation} from '@salesforce/retail-react-app/app/hooks/use-geo-location'
+
+jest.mock('@salesforce/retail-react-app/app/hooks/use-store-locator', () => ({
+ useStoreLocator: jest.fn()
+}))
+
+jest.mock('@salesforce/retail-react-app/app/hooks/use-geo-location', () => ({
+ useGeolocation: jest.fn()
+}))
+
+describe('StoreLocatorForm', () => {
+ const mockConfig = {
+ supportedCountries: [
+ {countryCode: 'US', countryName: 'United States'},
+ {countryCode: 'CA', countryName: 'Canada'}
+ ]
+ }
+
+ const mockSetFormValues = jest.fn()
+ const mockSetDeviceCoordinates = jest.fn()
+ let user
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ user = userEvent.setup()
+
+ useStoreLocator.mockImplementation(() => ({
+ config: mockConfig,
+ formValues: {countryCode: '', postalCode: ''},
+ setFormValues: mockSetFormValues,
+ setDeviceCoordinates: mockSetDeviceCoordinates
+ }))
+
+ useGeolocation.mockImplementation(() => ({
+ coordinates: {latitude: null, longitude: null},
+ error: null,
+ refresh: jest.fn()
+ }))
+ })
+
+ it('renders postal code input field', () => {
+ render()
+ const postalCodeInput = screen.queryByPlaceholderText('Enter postal code')
+ expect(postalCodeInput).not.toBeNull()
+ })
+
+ it('renders country selector when supportedCountries exist', () => {
+ render()
+ const countrySelect = screen.queryByText('Select a country')
+ expect(countrySelect).not.toBeNull()
+ })
+
+ it('renders "Use My Location" button', () => {
+ render()
+ const locationButton = screen.queryByText('Use My Location')
+ expect(locationButton).not.toBeNull()
+ })
+
+ it('submits form with entered values', async () => {
+ render()
+
+ const countrySelect = screen.getByRole('combobox')
+ const postalCodeInput = screen.getByPlaceholderText('Enter postal code')
+
+ await user.selectOptions(countrySelect, 'US')
+ await user.type(postalCodeInput, '12345')
+
+ const findButton = screen.getByText('Find')
+ await user.click(findButton)
+
+ expect(mockSetFormValues).toHaveBeenCalledWith({
+ countryCode: 'US',
+ postalCode: '12345'
+ })
+ })
+
+ it('shows validation error for empty postal code', async () => {
+ render()
+
+ const findButton = screen.getByText('Find')
+ await user.click(findButton)
+
+ const errorMessage = screen.queryByText('Please enter a postal code.')
+ expect(errorMessage).not.toBeNull()
+ })
+
+ it('clears form when "Use My Location" is clicked', async () => {
+ const mockRefresh = jest.fn()
+ useGeolocation.mockImplementation(() => ({
+ coordinates: {latitude: null, longitude: null},
+ error: null,
+ refresh: mockRefresh
+ }))
+
+ render()
+
+ const countrySelect = screen.getByRole('combobox')
+ const postalCodeInput = screen.getByPlaceholderText('Enter postal code')
+
+ await user.selectOptions(countrySelect, 'US')
+ await user.type(postalCodeInput, '12345')
+
+ const locationButton = screen.getByText('Use My Location')
+ await user.click(locationButton)
+
+ expect(mockSetFormValues).toHaveBeenCalledWith({
+ countryCode: '',
+ postalCode: ''
+ })
+ expect(mockRefresh).toHaveBeenCalled()
+ })
+
+ it('updates device coordinates when geolocation is successful', () => {
+ const mockCoordinates = {latitude: 37.7749, longitude: -122.4194}
+ useGeolocation.mockImplementation(() => ({
+ coordinates: mockCoordinates,
+ error: null,
+ refresh: jest.fn()
+ }))
+
+ render()
+
+ expect(mockSetDeviceCoordinates).toHaveBeenCalledWith(mockCoordinates)
+ })
+
+ it('shows geolocation error message when permission is denied', () => {
+ useGeolocation.mockImplementation(() => ({
+ coordinates: {latitude: null, longitude: null},
+ error: new Error('Geolocation permission denied'),
+ refresh: jest.fn()
+ }))
+
+ render()
+
+ const errorMessage = screen.queryByText('Please agree to share your location')
+ expect(errorMessage).not.toBeNull()
+ })
+})
diff --git a/packages/template-retail-react-app/app/components/store-locator/heading.jsx b/packages/template-retail-react-app/app/components/store-locator/heading.jsx
new file mode 100644
index 0000000000..19e5184a1a
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/heading.jsx
@@ -0,0 +1,19 @@
+/*
+ * 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 {Heading} from '@chakra-ui/react'
+
+export const StoreLocatorHeading = () => {
+ return (
+ <>
+
+ Find a Store
+
+ >
+ )
+}
diff --git a/packages/template-retail-react-app/app/components/store-locator/heading.test.jsx b/packages/template-retail-react-app/app/components/store-locator/heading.test.jsx
new file mode 100644
index 0000000000..15e769bda3
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/heading.test.jsx
@@ -0,0 +1,19 @@
+/*
+ * 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 {render, screen} from '@testing-library/react'
+import {StoreLocatorHeading} from '@salesforce/retail-react-app/app/components/store-locator/heading'
+
+describe('StoreLocatorHeading', () => {
+ test('renders heading with correct text', () => {
+ render()
+
+ const heading = screen.getByText('Find a Store')
+ expect(heading).toBeTruthy()
+ })
+})
diff --git a/packages/template-retail-react-app/app/components/store-locator/index.js b/packages/template-retail-react-app/app/components/store-locator/index.js
new file mode 100644
index 0000000000..9ff04d62ff
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/index.js
@@ -0,0 +1,9 @@
+/*
+ * 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
+ */
+
+export {StoreLocator} from './main'
+export {StoreLocatorModal} from './modal'
diff --git a/packages/template-retail-react-app/app/components/store-locator/list-item.jsx b/packages/template-retail-react-app/app/components/store-locator/list-item.jsx
new file mode 100644
index 0000000000..489650447e
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/list-item.jsx
@@ -0,0 +1,73 @@
+/*
+ * 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 PropTypes from 'prop-types'
+import {AccordionItem, AccordionButton, AccordionIcon, AccordionPanel, Box} from '@chakra-ui/react'
+
+export const StoreLocatorListItem = ({store}) => {
+ return (
+
+
+ {store.name && {store.name}}
+
+ {store.address1}
+
+
+ {store.city}, {store.stateCode ? store.stateCode : ''} {store.postalCode}
+
+ {store.distance !== undefined && (
+ <>
+
+
+ {store.distance} {store.distanceUnit}
+ {' away'}
+
+ >
+ )}
+ {store.phone && (
+ <>
+
+
+ {'Phone: '}
+ {store.phone}
+
+ >
+ )}
+ {store.storeHours && (
+ <>
+
+ View More
+
+
+
+
+
+ >
+ )}
+
+
+ )
+}
+
+StoreLocatorListItem.propTypes = {
+ store: PropTypes.shape({
+ name: PropTypes.string,
+ address1: PropTypes.string.isRequired,
+ city: PropTypes.string.isRequired,
+ stateCode: PropTypes.string,
+ postalCode: PropTypes.string.isRequired,
+ distance: PropTypes.number,
+ distanceUnit: PropTypes.string,
+ phone: PropTypes.string,
+ storeHours: PropTypes.string
+ }).isRequired
+}
diff --git a/packages/template-retail-react-app/app/components/store-locator/list-item.test.jsx b/packages/template-retail-react-app/app/components/store-locator/list-item.test.jsx
new file mode 100644
index 0000000000..95c2a71744
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/list-item.test.jsx
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2024, 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 from 'react'
+import {screen} from '@testing-library/react'
+import {Accordion} from '@chakra-ui/react'
+import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
+import {StoreLocatorListItem} from '@salesforce/retail-react-app/app/components/store-locator/list-item'
+
+describe('StoreLocatorListItem', () => {
+ const mockStore = {
+ name: 'Test Store',
+ address1: '123 Test St',
+ city: 'San Francisco',
+ stateCode: 'CA',
+ postalCode: '94105',
+ phone: '555-1234',
+ distance: 0.5,
+ distanceUnit: 'mi',
+ storeHours: 'Mon-Fri: 9AM-9PM
'
+ }
+
+ const renderWithAccordion = (component) => {
+ return renderWithProviders({component})
+ }
+
+ it('renders store information correctly', () => {
+ renderWithAccordion()
+
+ expect(screen.getByText('Test Store')).toBeTruthy()
+ expect(screen.getByText('123 Test St')).toBeTruthy()
+ expect(screen.getByText(/San Francisco, CA 94105/)).toBeTruthy()
+ expect(screen.getByText('0.5 mi away')).toBeTruthy()
+ expect(screen.getByText('Phone: 555-1234')).toBeTruthy()
+ expect(screen.getByText('View More')).toBeTruthy()
+ })
+
+ it('handles missing optional fields', () => {
+ const storeWithMissingFields = {
+ name: 'Basic Store',
+ address1: '789 Basic St',
+ city: 'Simple City',
+ postalCode: '12345'
+ }
+
+ renderWithAccordion()
+
+ expect(screen.getByText('Basic Store')).toBeTruthy()
+ expect(screen.getByText('789 Basic St')).toBeTruthy()
+ expect(screen.getByText(/Simple City/)).toBeTruthy()
+ expect(screen.queryByText(/away/)).toBeNull()
+ expect(screen.queryByText(/Phone:/)).toBeNull()
+ expect(screen.queryByText('View More')).toBeNull()
+ })
+})
diff --git a/packages/template-retail-react-app/app/components/store-locator/list.jsx b/packages/template-retail-react-app/app/components/store-locator/list.jsx
new file mode 100644
index 0000000000..432f3a665f
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/list.jsx
@@ -0,0 +1,83 @@
+/*
+ * 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, {useEffect, useState} from 'react'
+import {Accordion, AccordionItem, Box, Button} from '@chakra-ui/react'
+import {StoreLocatorListItem} from '@salesforce/retail-react-app/app/components/store-locator/list-item'
+import {useStoreLocator} from '@salesforce/retail-react-app/app/hooks/use-store-locator'
+
+export const StoreLocatorList = () => {
+ const {data, isLoading, config, formValues, mode} = useStoreLocator()
+ const [page, setPage] = useState(1)
+ useEffect(() => {
+ setPage(1)
+ }, [data])
+
+ const displayStoreLocatorStatusMessage = () => {
+ if (isLoading) return 'Loading locations...'
+ if (data?.total === 0) return 'Sorry, there are no locations in this area'
+
+ if (mode === 'input') {
+ const countryName =
+ config.supportedCountries.length !== 0
+ ? config.supportedCountries.find(
+ (o) => o.countryCode === formValues.countryCode
+ )?.countryName || config.defaultCountry
+ : config.defaultCountry
+
+ return `Viewing stores within ${String(config.radius)}${String(
+ config.radiusUnit
+ )} of ${String(data?.data[0].postalCode)} in ${String(countryName)}`
+ }
+
+ return 'Viewing stores near your location'
+ }
+
+ const showNumberOfStores = page * config.defaultPageSize
+ const showLoadMoreButton = data?.total > showNumberOfStores
+ const storesToShow = data?.data?.slice(0, showNumberOfStores) || []
+
+ return (
+ <>
+
+
+
+ {displayStoreLocatorStatusMessage()}
+
+
+ {storesToShow?.map((store, index) => (
+
+ ))}
+
+ {showLoadMoreButton && (
+
+
+
+ )}
+ >
+ )
+}
diff --git a/packages/template-retail-react-app/app/components/store-locator/list.test.jsx b/packages/template-retail-react-app/app/components/store-locator/list.test.jsx
new file mode 100644
index 0000000000..b4fb00ea0f
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/list.test.jsx
@@ -0,0 +1,141 @@
+/*
+ * 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 {render, screen, fireEvent} from '@testing-library/react'
+import {StoreLocatorList} from '@salesforce/retail-react-app/app/components/store-locator/list'
+import {useStoreLocator} from '@salesforce/retail-react-app/app/hooks/use-store-locator'
+
+// Mock the useStoreLocator hook
+jest.mock('@salesforce/retail-react-app/app/hooks/use-store-locator')
+
+// Mock store data
+const mockStores = {
+ total: 3,
+ data: [
+ {
+ name: 'Store 1',
+ address1: '123 Main St',
+ city: 'Boston',
+ stateCode: 'MA',
+ postalCode: '02108',
+ phone: '555-0123'
+ },
+ {
+ name: 'Store 2',
+ address1: '456 Oak St',
+ city: 'Boston',
+ stateCode: 'MA',
+ postalCode: '02109',
+ phone: '555-0124'
+ },
+ {
+ name: 'Store 3',
+ address1: '789 Pine St',
+ city: 'Boston',
+ stateCode: 'MA',
+ postalCode: '02110',
+ phone: '555-0125'
+ }
+ ]
+}
+
+const defaultConfig = {
+ radius: 10,
+ radiusUnit: 'mi',
+ defaultPageSize: 2,
+ defaultCountry: 'United States',
+ supportedCountries: [{countryCode: 'US', countryName: 'United States'}]
+}
+
+describe('StoreLocatorList', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ test('renders loading state', () => {
+ useStoreLocator.mockReturnValue({
+ isLoading: true,
+ data: null,
+ config: defaultConfig,
+ formValues: {},
+ mode: 'input'
+ })
+
+ render()
+ expect(screen.getByText('Loading locations...')).toBeTruthy()
+ })
+
+ test('renders no locations message', () => {
+ useStoreLocator.mockReturnValue({
+ isLoading: false,
+ data: {total: 0, data: []},
+ config: defaultConfig,
+ formValues: {},
+ mode: 'input'
+ })
+
+ render()
+ expect(screen.getByText('Sorry, there are no locations in this area')).toBeTruthy()
+ })
+
+ test('renders stores with pagination', () => {
+ useStoreLocator.mockReturnValue({
+ isLoading: false,
+ data: mockStores,
+ config: defaultConfig,
+ formValues: {countryCode: 'US'},
+ mode: 'input'
+ })
+
+ render()
+
+ // Initially shows only first 2 stores (defaultPageSize)
+ expect(screen.getByText('Store 1')).toBeTruthy()
+ expect(screen.getByText('Store 2')).toBeTruthy()
+ expect(screen.queryByText('Store 3')).toBeNull()
+
+ // Load more button should be visible
+ const loadMoreButton = screen.getByText('Load More')
+ expect(loadMoreButton).toBeTruthy()
+
+ // Click load more
+ fireEvent.click(loadMoreButton)
+
+ // Should now show all 3 stores
+ expect(screen.getByText('Store 1')).toBeTruthy()
+ expect(screen.getByText('Store 2')).toBeTruthy()
+ expect(screen.getByText('Store 3')).toBeTruthy()
+ })
+
+ test('renders correct status message for input mode', () => {
+ useStoreLocator.mockReturnValue({
+ isLoading: false,
+ data: mockStores,
+ config: defaultConfig,
+ formValues: {countryCode: 'US'},
+ mode: 'input'
+ })
+
+ render()
+ expect(
+ screen.getByText(/Viewing stores within 10mi of 02108 in United States/)
+ ).toBeTruthy()
+ })
+
+ test('renders correct status message for geolocation mode', () => {
+ useStoreLocator.mockReturnValue({
+ isLoading: false,
+ data: mockStores,
+ config: defaultConfig,
+ formValues: {},
+ mode: 'geolocation'
+ })
+
+ render()
+ expect(screen.getByText('Viewing stores near your location')).toBeTruthy()
+ })
+})
diff --git a/packages/template-retail-react-app/app/components/store-locator/main.jsx b/packages/template-retail-react-app/app/components/store-locator/main.jsx
new file mode 100644
index 0000000000..71750db949
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/main.jsx
@@ -0,0 +1,21 @@
+/*
+ * 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 {StoreLocatorList} from '@salesforce/retail-react-app/app/components/store-locator/list'
+import {StoreLocatorForm} from '@salesforce/retail-react-app/app/components/store-locator/form'
+import {StoreLocatorHeading} from '@salesforce/retail-react-app/app/components/store-locator/heading'
+
+export const StoreLocator = () => {
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/packages/template-retail-react-app/app/components/store-locator/main.test.jsx b/packages/template-retail-react-app/app/components/store-locator/main.test.jsx
new file mode 100644
index 0000000000..00d2e011ca
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/main.test.jsx
@@ -0,0 +1,33 @@
+/*
+ * 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 {render, screen} from '@testing-library/react'
+import {StoreLocator} from '@salesforce/retail-react-app/app/components/store-locator/main'
+
+jest.mock('./list', () => ({
+ StoreLocatorList: () => Store List Mock
+}))
+
+jest.mock('./form', () => ({
+ StoreLocatorForm: () => Store Form Mock
+}))
+
+jest.mock('./heading', () => ({
+ StoreLocatorHeading: () => Store Heading Mock
+}))
+
+describe('StoreLocatorContent', () => {
+ it('renders all child components', () => {
+ render()
+
+ // Verify that all child components are rendered
+ expect(screen.queryByTestId('store-locator-heading')).not.toBeNull()
+ expect(screen.queryByTestId('store-locator-form')).not.toBeNull()
+ expect(screen.queryByTestId('store-locator-list')).not.toBeNull()
+ })
+})
diff --git a/packages/template-retail-react-app/app/components/store-locator/modal.jsx b/packages/template-retail-react-app/app/components/store-locator/modal.jsx
new file mode 100644
index 0000000000..eeb90b5d86
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/modal.jsx
@@ -0,0 +1,56 @@
+/*
+ * 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 PropTypes from 'prop-types'
+import {
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ useBreakpointValue
+} from '@chakra-ui/react'
+import {StoreLocator} from '@salesforce/retail-react-app/app/components/store-locator/main'
+
+export const StoreLocatorModal = ({isOpen, onClose}) => {
+ const isDesktopView = useBreakpointValue({base: false, lg: true})
+
+ return isDesktopView ? (
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+ )
+}
+
+StoreLocatorModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired
+}
diff --git a/packages/template-retail-react-app/app/components/store-locator/modal.test.jsx b/packages/template-retail-react-app/app/components/store-locator/modal.test.jsx
new file mode 100644
index 0000000000..ea33e91dc2
--- /dev/null
+++ b/packages/template-retail-react-app/app/components/store-locator/modal.test.jsx
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2024, 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 from 'react'
+import {screen} from '@testing-library/react'
+import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
+import {StoreLocatorModal} from '@salesforce/retail-react-app/app/components/store-locator/modal'
+
+const mockUseBreakpointValue = jest.fn()
+jest.mock('@chakra-ui/react', () => {
+ const originalModule = jest.requireActual('@chakra-ui/react')
+ return {
+ ...originalModule,
+ useBreakpointValue: () => mockUseBreakpointValue
+ }
+})
+
+jest.mock('./main', () => ({
+ StoreLocator: () => Store Locator Content
+}))
+
+describe('StoreLocatorModal', () => {
+ const mockProps = {
+ isOpen: true,
+ onClose: jest.fn()
+ }
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ mockUseBreakpointValue.mockReturnValue(true) // Default to desktop view
+ })
+
+ it('renders desktop view correctly', () => {
+ mockUseBreakpointValue.mockReturnValue(true) // Desktop view
+ renderWithProviders()
+
+ expect(screen.getByText('Store Locator Content')).toBeTruthy()
+ expect(screen.getByTestId('store-locator-content')).toBeTruthy()
+ })
+
+ it('renders mobile view correctly', () => {
+ mockUseBreakpointValue.mockReturnValue(false) // Mobile view
+ renderWithProviders()
+
+ expect(screen.getByText('Store Locator Content')).toBeTruthy()
+ expect(screen.getByTestId('store-locator-content')).toBeTruthy()
+ })
+
+ it('does not render when closed', () => {
+ renderWithProviders()
+
+ expect(screen.queryByText('Store Locator Content')).toBeNull()
+ expect(screen.queryByTestId('store-locator-content')).toBeNull()
+ })
+
+ it('calls onClose when close button is clicked', () => {
+ const onClose = jest.fn()
+ renderWithProviders()
+
+ const closeButton = screen.getByLabelText('Close')
+ closeButton.click()
+ expect(onClose).toHaveBeenCalled()
+ })
+})
diff --git a/packages/template-retail-react-app/app/constants.js b/packages/template-retail-react-app/app/constants.js
index ad8602e346..7db2216f5f 100644
--- a/packages/template-retail-react-app/app/constants.js
+++ b/packages/template-retail-react-app/app/constants.js
@@ -185,35 +185,24 @@ export const REMOVE_UNAVAILABLE_CART_ITEM_DIALOG_CONFIG = {
onPrimaryAction: noop
}
-export const SUPPORTED_STORE_LOCATOR_COUNTRIES = [
+export const STORE_LOCATOR_IS_ENABLED = true
+export const STORE_LOCATOR_SUPPORTED_COUNTRIES = [
{
countryCode: 'US',
- countryName: defineMessage({
- defaultMessage: 'United States',
- id: 'store_locator.dropdown.united_states'
- })
+ countryName: 'United States'
},
{
countryCode: 'DE',
- countryName: defineMessage({
- defaultMessage: 'Germany',
- id: 'store_locator.dropdown.germany'
- })
+ countryName: 'Germany'
}
]
-
-export const DEFAULT_STORE_LOCATOR_COUNTRY = {
- countryCode: 'DE',
- countryName: defineMessage({
- defaultMessage: 'Germany',
- id: 'store_locator.dropdown.germany'
- })
-}
-export const DEFAULT_STORE_LOCATOR_POSTAL_CODE = '10178'
-export const STORE_LOCATOR_DISTANCE = 100
-export const STORE_LOCATOR_NUM_STORES_PER_LOAD = 10
-export const STORE_LOCATOR_DISTANCE_UNIT = 'km'
-export const STORE_LOCATOR_IS_ENABLED = true
+export const STORE_LOCATOR_DEFAULT_POSTAL_CODE = '10178'
+export const STORE_LOCATOR_RADIUS = 100
+export const STORE_LOCATOR_RADIUS_UNIT = 'km'
+export const STORE_LOCATOR_DEFAULT_COUNTRY = 'DE'
+export const STORE_LOCATOR_DEFAULT_COUNTRY_CODE = 'DE'
+export const STORE_LOCATOR_DEFAULT_PAGE_SIZE = 10
+export const STORE_LOCATOR_NUM_STORES_PER_REQUEST_API_MAX = 200 // This is an API limit and is therefore not configurable
export const DEFAULT_DNT_STATE = true
// Constants for shopper context
diff --git a/packages/template-retail-react-app/app/contexts/index.js b/packages/template-retail-react-app/app/contexts/index.js
index 622a78b5e8..d542c27cfb 100644
--- a/packages/template-retail-react-app/app/contexts/index.js
+++ b/packages/template-retail-react-app/app/contexts/index.js
@@ -7,6 +7,10 @@
import React, {useState} from 'react'
import PropTypes from 'prop-types'
+export {
+ StoreLocatorContext,
+ StoreLocatorProvider
+} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
/**
* This is the global state for the multiples sites and locales supported in the App.
diff --git a/packages/template-retail-react-app/app/contexts/store-locator-provider.jsx b/packages/template-retail-react-app/app/contexts/store-locator-provider.jsx
new file mode 100644
index 0000000000..51a8a88331
--- /dev/null
+++ b/packages/template-retail-react-app/app/contexts/store-locator-provider.jsx
@@ -0,0 +1,41 @@
+/*
+ * 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, {useState, createContext} from 'react'
+import PropTypes from 'prop-types'
+
+export const StoreLocatorContext = createContext(null)
+
+export const StoreLocatorProvider = ({config, children}) => {
+ const [state, setState] = useState({
+ mode: 'input',
+ formValues: {
+ countryCode: config.defaultCountryCode,
+ postalCode: config.defaultPostalCode
+ },
+ deviceCoordinates: {
+ latitude: null,
+ longitude: null
+ },
+ config
+ })
+
+ const value = {
+ state,
+ setState
+ }
+
+ return {children}
+}
+
+StoreLocatorProvider.propTypes = {
+ config: PropTypes.shape({
+ defaultCountryCode: PropTypes.string.isRequired,
+ defaultPostalCode: PropTypes.string.isRequired
+ }).isRequired,
+ children: PropTypes.node
+}
diff --git a/packages/template-retail-react-app/app/contexts/store-locator-provider.test.jsx b/packages/template-retail-react-app/app/contexts/store-locator-provider.test.jsx
new file mode 100644
index 0000000000..43c0eca3af
--- /dev/null
+++ b/packages/template-retail-react-app/app/contexts/store-locator-provider.test.jsx
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2024, 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 from 'react'
+import {render, act} from '@testing-library/react'
+import {
+ StoreLocatorProvider,
+ StoreLocatorContext
+} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
+
+describe('StoreLocatorProvider', () => {
+ const mockConfig = {
+ defaultCountryCode: 'US',
+ defaultPostalCode: '10178'
+ }
+
+ it('provides the expected context value', () => {
+ let contextValue
+ const TestComponent = () => {
+ contextValue = React.useContext(StoreLocatorContext)
+ return null
+ }
+
+ render(
+
+
+
+ )
+
+ expect(contextValue).toBeTruthy()
+ expect(contextValue?.state).toEqual({
+ mode: 'input',
+ formValues: {
+ countryCode: mockConfig.defaultCountryCode,
+ postalCode: mockConfig.defaultPostalCode
+ },
+ deviceCoordinates: {
+ latitude: null,
+ longitude: null
+ },
+ config: mockConfig
+ })
+ expect(typeof contextValue?.setState).toBe('function')
+ })
+
+ it('updates state correctly when setState is called', () => {
+ let contextValue
+ const TestComponent = () => {
+ contextValue = React.useContext(StoreLocatorContext)
+ return null
+ }
+
+ render(
+
+
+
+ )
+
+ act(() => {
+ contextValue?.setState((prev) => ({
+ ...prev,
+ mode: 'device',
+ formValues: {
+ countryCode: 'US',
+ postalCode: '94105'
+ }
+ }))
+ })
+
+ expect(contextValue?.state.mode).toBe('device')
+ expect(contextValue?.state.formValues).toEqual({
+ countryCode: 'US',
+ postalCode: '94105'
+ })
+ })
+
+ it('renders children correctly', () => {
+ const TestChild = () => Test Child
+
+ const {getByText} = render(
+
+
+
+ )
+
+ expect(getByText('Test Child')).toBeTruthy()
+ })
+})
diff --git a/packages/template-retail-react-app/app/hooks/use-geo-location.js b/packages/template-retail-react-app/app/hooks/use-geo-location.js
new file mode 100644
index 0000000000..5e6bab8e16
--- /dev/null
+++ b/packages/template-retail-react-app/app/hooks/use-geo-location.js
@@ -0,0 +1,56 @@
+/*
+ * 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, useState} from 'react'
+
+export function useGeolocation(options = {}) {
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [coordinates, setCoordinates] = useState({
+ latitude: null,
+ longitude: null
+ })
+
+ const getLocation = () => {
+ setLoading(true)
+ setError(null)
+
+ try {
+ if (!navigator.geolocation) {
+ throw new Error('Geolocation is not supported by this browser.')
+ }
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ setCoordinates({
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude
+ })
+ setLoading(false)
+ },
+ (err) => {
+ setError(err)
+ setLoading(false)
+ },
+ options
+ )
+ } catch (err) {
+ setError(err)
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ getLocation()
+ }, [])
+
+ return {
+ coordinates,
+ loading,
+ error,
+ refresh: getLocation
+ }
+}
diff --git a/packages/template-retail-react-app/app/hooks/use-geo-location.test.js b/packages/template-retail-react-app/app/hooks/use-geo-location.test.js
new file mode 100644
index 0000000000..8f122940a4
--- /dev/null
+++ b/packages/template-retail-react-app/app/hooks/use-geo-location.test.js
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2024, 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 {renderHook, act} from '@testing-library/react'
+import {useGeolocation} from '@salesforce/retail-react-app/app/hooks/use-geo-location'
+
+describe('useGeolocation', () => {
+ const mockGeolocation = {
+ getCurrentPosition: jest.fn()
+ }
+
+ beforeEach(() => {
+ // Mock GeolocationPositionError if it's not defined
+ if (!global.GeolocationPositionError) {
+ global.GeolocationPositionError = function () {
+ this.code = 1
+ this.message = ''
+ this.PERMISSION_DENIED = 1
+ this.POSITION_UNAVAILABLE = 2
+ this.TIMEOUT = 3
+ }
+ }
+
+ // Setup mock for navigator.geolocation
+ Object.defineProperty(global.navigator, 'geolocation', {
+ value: mockGeolocation,
+ writable: true
+ })
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('initializes with default values', () => {
+ const {result} = renderHook(() => useGeolocation())
+
+ expect(result.current).toEqual({
+ coordinates: {latitude: null, longitude: null},
+ loading: true,
+ error: null,
+ refresh: expect.any(Function)
+ })
+ })
+
+ it('updates coordinates on successful geolocation', async () => {
+ const mockPosition = {
+ coords: {
+ latitude: 37.7749,
+ longitude: -122.4194,
+ accuracy: 0,
+ altitude: null,
+ altitudeAccuracy: null,
+ heading: null,
+ speed: null
+ },
+ timestamp: Date.now()
+ }
+
+ mockGeolocation.getCurrentPosition.mockImplementation((success) => {
+ success(mockPosition)
+ })
+
+ const {result} = renderHook(() => useGeolocation())
+
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0))
+ })
+
+ expect(result.current).toEqual({
+ coordinates: {
+ latitude: 37.7749,
+ longitude: -122.4194
+ },
+ loading: false,
+ error: null,
+ refresh: expect.any(Function)
+ })
+ })
+
+ it('handles geolocation errors', async () => {
+ const mockError = new global.GeolocationPositionError()
+ mockError.code = 1
+ mockError.message = 'User denied geolocation'
+
+ mockGeolocation.getCurrentPosition.mockImplementation((_success, error) => {
+ error(mockError)
+ })
+
+ const {result} = renderHook(() => useGeolocation())
+
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0))
+ })
+
+ expect(result.current).toEqual({
+ coordinates: {latitude: null, longitude: null},
+ loading: false,
+ error: mockError,
+ refresh: expect.any(Function)
+ })
+ })
+
+ it('handles refresh function call', async () => {
+ const mockPosition = {
+ coords: {
+ latitude: 37.7749,
+ longitude: -122.4194,
+ accuracy: 0,
+ altitude: null,
+ altitudeAccuracy: null,
+ heading: null,
+ speed: null
+ },
+ timestamp: Date.now()
+ }
+
+ mockGeolocation.getCurrentPosition.mockImplementation((success) => {
+ success(mockPosition)
+ })
+
+ const {result} = renderHook(() => useGeolocation())
+
+ act(() => {
+ result.current.refresh()
+ })
+
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0))
+ })
+
+ expect(mockGeolocation.getCurrentPosition).toHaveBeenCalledTimes(2)
+ expect(result.current.coordinates).toEqual({
+ latitude: 37.7749,
+ longitude: -122.4194
+ })
+ })
+
+ it('handles case when geolocation is not supported', async () => {
+ // First clear the mock
+ Object.defineProperty(global.navigator, 'geolocation', {
+ value: undefined,
+ writable: true
+ })
+
+ const {result} = renderHook(() => useGeolocation())
+
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0))
+ })
+
+ // Just check that we're in an error state with null coordinates
+ expect(result.current.loading).toBe(false)
+ expect(result.current.coordinates).toEqual({
+ latitude: null,
+ longitude: null
+ })
+ })
+})
diff --git a/packages/template-retail-react-app/app/hooks/use-store-locator.js b/packages/template-retail-react-app/app/hooks/use-store-locator.js
new file mode 100644
index 0000000000..4c05057eec
--- /dev/null
+++ b/packages/template-retail-react-app/app/hooks/use-store-locator.js
@@ -0,0 +1,85 @@
+/*
+ * 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 {useContext} from 'react'
+import {useSearchStores} from '@salesforce/commerce-sdk-react'
+import {StoreLocatorContext} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
+
+const useStores = (state) => {
+ //This is an API limit and is therefore not configurable
+ const NUM_STORES_PER_REQUEST_API_MAX = 200
+ const apiParameters =
+ state.mode === 'input'
+ ? {
+ countryCode: state.formValues.countryCode,
+ postalCode: state.formValues.postalCode,
+ maxDistance: state.config.radius,
+ limit: NUM_STORES_PER_REQUEST_API_MAX,
+ distanceUnit: state.config.radiusUnit
+ }
+ : {
+ latitude: state.deviceCoordinates.latitude,
+ longitude: state.deviceCoordinates.longitude,
+ maxDistance: state.config.radius,
+ limit: NUM_STORES_PER_REQUEST_API_MAX,
+ distanceUnit: state.config.radiusUnit
+ }
+ const shouldFetchStores =
+ Boolean(
+ state.mode === 'input' && state.formValues.countryCode && state.formValues.postalCode
+ ) ||
+ Boolean(
+ state.mode === 'device' &&
+ state.deviceCoordinates.latitude &&
+ state.deviceCoordinates.longitude
+ )
+ return useSearchStores(
+ {
+ parameters: apiParameters
+ },
+ {
+ enabled: shouldFetchStores
+ }
+ )
+}
+
+export const useStoreLocator = () => {
+ const context = useContext(StoreLocatorContext)
+ if (!context) {
+ throw new Error('useStoreLocator must be used within a StoreLocatorProvider')
+ }
+
+ const {state, setState} = context
+ const {data, isLoading} = useStores(state)
+
+ // There are two modes, input and device.
+ // The input mode is when the user is searching for a store
+ // by entering a postal code and country code.
+ // The device mode is when the user is searching for a store by sharing their location.
+ // The mode is implicitly set by user's action.
+ const setFormValues = (formValues) => {
+ setState((prev) => ({...prev, formValues, mode: 'input'}))
+ }
+
+ const setDeviceCoordinates = (coordinates) => {
+ setState((prev) => ({
+ ...prev,
+ deviceCoordinates: coordinates,
+ mode: 'device',
+ formValues: {countryCode: '', postalCode: ''}
+ }))
+ }
+
+ return {
+ ...state,
+ data,
+ isLoading,
+ // Actions
+ setFormValues,
+ setDeviceCoordinates
+ }
+}
diff --git a/packages/template-retail-react-app/app/hooks/use-store-locator.test.jsx b/packages/template-retail-react-app/app/hooks/use-store-locator.test.jsx
new file mode 100644
index 0000000000..b899a49ba7
--- /dev/null
+++ b/packages/template-retail-react-app/app/hooks/use-store-locator.test.jsx
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2024, 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 from 'react'
+import {renderHook, act} from '@testing-library/react'
+import {useStoreLocator} from '@salesforce/retail-react-app/app/hooks/use-store-locator'
+import {StoreLocatorProvider} from '@salesforce/retail-react-app/app/contexts/store-locator-provider'
+import {useSearchStores} from '@salesforce/commerce-sdk-react'
+
+// Mock the commerce-sdk-react hook
+jest.mock('@salesforce/commerce-sdk-react', () => ({
+ useSearchStores: jest.fn()
+}))
+
+const config = {
+ radius: 100,
+ radiusUnit: 'mi',
+ defaultCountryCode: 'US',
+ defaultPostalCode: '10178'
+}
+
+const wrapper = ({children}) => {
+ return {children}
+}
+
+describe('useStoreLocator', () => {
+ beforeEach(() => {
+ useSearchStores.mockReset()
+ // Default mock implementation
+ useSearchStores.mockReturnValue({
+ data: undefined,
+ isLoading: false
+ })
+ })
+
+ it('throws error when used outside provider', () => {
+ let error
+ try {
+ renderHook(() => useStoreLocator())
+ } catch (err) {
+ error = err
+ }
+
+ expect(error).toEqual(Error('useStoreLocator must be used within a StoreLocatorProvider'))
+ })
+
+ it('initializes with default values', () => {
+ const {result} = renderHook(() => useStoreLocator(), {wrapper})
+
+ expect(result.current).toMatchObject({
+ mode: 'input',
+ formValues: {
+ countryCode: config.defaultCountryCode,
+ postalCode: config.defaultPostalCode
+ },
+ deviceCoordinates: {latitude: null, longitude: null},
+ isLoading: false,
+ data: undefined
+ })
+ })
+
+ it('updates form values and switches to input mode', () => {
+ const {result} = renderHook(() => useStoreLocator(), {wrapper})
+
+ act(() => {
+ result.current.setFormValues({
+ countryCode: 'US',
+ postalCode: '94105'
+ })
+ })
+
+ expect(result.current.mode).toBe('input')
+ expect(result.current.formValues).toEqual({
+ countryCode: 'US',
+ postalCode: '94105'
+ })
+ })
+
+ it('updates device coordinates and switches to device mode', () => {
+ const {result} = renderHook(() => useStoreLocator(), {wrapper})
+
+ act(() => {
+ result.current.setDeviceCoordinates({
+ latitude: 37.7749,
+ longitude: -122.4194
+ })
+ })
+
+ expect(result.current.mode).toBe('device')
+ expect(result.current.deviceCoordinates).toEqual({
+ latitude: 37.7749,
+ longitude: -122.4194
+ })
+ // Should reset form values when switching to device mode
+ expect(result.current.formValues).toEqual({
+ countryCode: '',
+ postalCode: ''
+ })
+ })
+
+ it('calls useSearchStores with correct parameters in input mode', () => {
+ const {result} = renderHook(() => useStoreLocator(), {wrapper})
+
+ act(() => {
+ result.current.setFormValues({
+ countryCode: 'US',
+ postalCode: '94105'
+ })
+ })
+
+ expect(useSearchStores).toHaveBeenCalledWith(
+ {
+ parameters: {
+ countryCode: 'US',
+ postalCode: '94105',
+ maxDistance: 100,
+ limit: 200,
+ distanceUnit: 'mi'
+ }
+ },
+ {
+ enabled: true
+ }
+ )
+ })
+
+ it('calls useSearchStores with correct parameters in device mode', () => {
+ const {result} = renderHook(() => useStoreLocator(), {wrapper})
+
+ act(() => {
+ result.current.setDeviceCoordinates({
+ latitude: 37.7749,
+ longitude: -122.4194
+ })
+ })
+
+ expect(useSearchStores).toHaveBeenCalledWith(
+ {
+ parameters: {
+ latitude: 37.7749,
+ longitude: -122.4194,
+ maxDistance: 100,
+ limit: 200,
+ distanceUnit: 'mi'
+ }
+ },
+ {
+ enabled: true
+ }
+ )
+ })
+
+ it('handles loading state', () => {
+ useSearchStores.mockReturnValue({
+ data: undefined,
+ isLoading: true
+ })
+
+ const {result} = renderHook(() => useStoreLocator(), {wrapper})
+
+ expect(result.current.isLoading).toBe(true)
+ })
+
+ it('handles store data', () => {
+ const mockStoreData = [
+ {
+ id: '1',
+ name: 'Test Store',
+ address: {
+ address1: '123 Test St',
+ city: 'Test City',
+ stateCode: 'CA',
+ postalCode: '94105'
+ }
+ }
+ ]
+
+ useSearchStores.mockReturnValue({
+ data: mockStoreData,
+ isLoading: false
+ })
+
+ const {result} = renderHook(() => useStoreLocator(), {wrapper})
+
+ expect(result.current.data).toEqual(mockStoreData)
+ })
+})
diff --git a/packages/template-retail-react-app/app/pages/store-locator/index.jsx b/packages/template-retail-react-app/app/pages/store-locator/index.jsx
index d568a5f807..16fbff6a77 100644
--- a/packages/template-retail-react-app/app/pages/store-locator/index.jsx
+++ b/packages/template-retail-react-app/app/pages/store-locator/index.jsx
@@ -1,49 +1,35 @@
/*
- * Copyright (c) 2021, salesforce.com, inc.
+ * 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 {Box, Container} from '@chakra-ui/react'
+import {StoreLocator} from '@salesforce/retail-react-app/app/components/store-locator'
-// Components
-import {Box, Container} from '@salesforce/retail-react-app/app/components/shared/ui'
-import Seo from '@salesforce/retail-react-app/app/components/seo'
-import StoreLocatorContent from '@salesforce/retail-react-app/app/components/store-locator-modal/store-locator-content'
-
-// Others
-import {
- StoreLocatorContext,
- useStoreLocator
-} from '@salesforce/retail-react-app/app/components/store-locator-modal/index'
-
-const StoreLocator = () => {
- const storeLocator = useStoreLocator()
-
+const StoreLocatorPage = () => {
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
)
}
-StoreLocator.getTemplateName = () => 'store-locator'
+StoreLocatorPage.getTemplateName = () => 'store-locator'
-StoreLocator.propTypes = {}
+StoreLocatorPage.propTypes = {}
-export default StoreLocator
+export default StoreLocatorPage
diff --git a/packages/template-retail-react-app/app/pages/store-locator/index.test.jsx b/packages/template-retail-react-app/app/pages/store-locator/index.test.jsx
index 959ad891cc..60f037b201 100644
--- a/packages/template-retail-react-app/app/pages/store-locator/index.test.jsx
+++ b/packages/template-retail-react-app/app/pages/store-locator/index.test.jsx
@@ -1,256 +1,36 @@
/*
- * Copyright (c) 2021, salesforce.com, inc.
+ * 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
+ */
+/*
+ * 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 {screen, waitFor, within} from '@testing-library/react'
-import {rest} from 'msw'
-import {
- createPathWithDefaults,
- renderWithProviders
-} from '@salesforce/retail-react-app/app/utils/test-utils'
-import StoreLocator from '.'
-import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
-
-const mockStores = {
- limit: 4,
- data: [
- {
- address1: 'Kirchgasse 12',
- city: 'Wiesbaden',
- countryCode: 'DE',
- distance: 0.74,
- distanceUnit: 'km',
- id: '00019',
- inventoryId: 'inventory_m_store_store11',
- latitude: 50.0826,
- longitude: 8.24,
- name: 'Wiesbaden Tech Depot',
- phone: '+49 611 876543',
- posEnabled: false,
- postalCode: '65185',
- storeHours:
- 'Monday 9 AM–7 PM\nTuesday 9 AM–7 PM\nWednesday 9 AM–7 PM\nThursday 9 AM–8 PM\nFriday 9 AM–7 PM\nSaturday 9 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Schaumainkai 63',
- city: 'Frankfurt am Main',
- countryCode: 'DE',
- distance: 30.78,
- distanceUnit: 'km',
- id: '00002',
- inventoryId: 'inventory_m_store_store4',
- latitude: 50.097416,
- longitude: 8.669059,
- name: 'Frankfurt Electronics Store',
- phone: '+49 69 111111111',
- posEnabled: false,
- postalCode: '60596',
- storeHours:
- 'Monday 10 AM–6 PM\nTuesday 10 AM–6 PM\nWednesday 10 AM–6 PM\nThursday 10 AM–9 PM\nFriday 10 AM–6 PM\nSaturday 10 AM–6 PM\nSunday 10 AM–6 PM',
- storeLocatorEnabled: true
- },
- {
- address1: 'Löhrstraße 87',
- city: 'Koblenz',
- countryCode: 'DE',
- distance: 55.25,
- distanceUnit: 'km',
- id: '00035',
- inventoryId: 'inventory_m_store_store27',
- latitude: 50.3533,
- longitude: 7.5946,
- name: 'Koblenz Electronics Store',
- phone: '+49 261 123456',
- posEnabled: false,
- postalCode: '56068',
- storeHours:
- 'Monday 9 AM–7 PM\nTuesday 9 AM–7 PM\nWednesday 9 AM–7 PM\nThursday 9 AM–8 PM\nFriday 9 AM–7 PM\nSaturday 9 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- },
- {
- address1: 'Hauptstraße 47',
- city: 'Heidelberg',
- countryCode: 'DE',
- distance: 81.1,
- distanceUnit: 'km',
- id: '00021',
- inventoryId: 'inventory_m_store_store13',
- latitude: 49.4077,
- longitude: 8.6908,
- name: 'Heidelberg Tech Mart',
- phone: '+49 6221 123456',
- posEnabled: false,
- postalCode: '69117',
- storeHours:
- 'Monday 10 AM–7 PM\nTuesday 10 AM–7 PM\nWednesday 10 AM–7 PM\nThursday 10 AM–8 PM\nFriday 10 AM–7 PM\nSaturday 10 AM–6 PM\nSunday Closed',
- storeLocatorEnabled: true
- }
- ],
- offset: 0,
- total: 4
-}
-
-const mockNoStores = {
- limit: 4,
- total: 0
-}
-
-const MockedComponent = () => {
- return (
-
-
-
- )
-}
-
-// Set up and clean up
-beforeEach(() => {
- jest.resetModules()
- window.history.pushState({}, 'Store locator', createPathWithDefaults('/store-locator'))
-})
-afterEach(() => {
- jest.resetModules()
- localStorage.clear()
- jest.clearAllMocks()
-})
-
-test('Allows customer to go to store locator page', async () => {
- global.server.use(
- rest.get(
- '*/shopper-stores/v1/organizations/v1/organizations/f_ecom_zzrf_001/store-search',
- (req, res, ctx) => {
- return res(ctx.delay(0), ctx.status(200), ctx.json(mockStores))
- }
- )
- )
-
- // render our test component
- const {user} = renderWithProviders(, {
- wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
- })
-
- await user.click(await screen.findByText('Find a Store'))
-
- await waitFor(() => {
- expect(window.location.pathname).toBe('/uk/en-GB/store-locator')
- })
-})
-
-test('Allows customer to go to store locator page and then select a new store', async () => {
- global.server.use(
- rest.get(
- 'https://www.domain.com/mobify/proxy/api/store/shopper-stores/v1/organizations/:organizationId/store-search',
- (req, res, ctx) => {
- return res(ctx.delay(0), ctx.status(200), ctx.json(mockStores))
- }
- )
- )
-
- const {user} = renderWithProviders(, {
- wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
- })
-
- await user.click(await screen.findByText('Find a Store'))
-
- const countrySelect = await screen.findByDisplayValue('Select a country')
- await user.selectOptions(countrySelect, 'DE')
-
- await user.type(screen.getByPlaceholderText(/Enter postal code/i), '69117')
-
- const findButtonInForm = screen.getByRole('button', {name: 'Find'})
- await user.click(findButtonInForm)
-
- const storeToSelect = 'Heidelberg Tech Mart'
- await waitFor(() => {
- expect(screen.getByText(storeToSelect)).toBeInTheDocument()
- })
-
- const storeNameElement = await screen.findByText(storeToSelect)
-
- const storeAccordionItem = storeNameElement.closest('.chakra-accordion__item')
- if (!storeAccordionItem) {
- throw new Error(`Could not find parent .chakra-accordion__item for store: ${storeToSelect}`)
- }
-
- const storeRadio = within(storeAccordionItem).getByRole('radio')
-
- await user.click(storeRadio)
- expect(storeRadio).toBeChecked()
-})
-
-test('Show no stores are found if there are no stores', async () => {
- global.server.use(
- rest.get(
- '*/shopper-stores/v1/organizations/v1/organizations/f_ecom_zzrf_001/store-search',
- (req, res, ctx) => {
- return res(ctx.delay(0), ctx.status(200), ctx.json(mockNoStores))
- }
- )
- )
-
- // render our test component
- renderWithProviders(, {
- wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
- })
-
- await waitFor(() => {
- const descriptionFindAStore = screen.getByText(/Find a Store/i)
- const noLocationsInThisArea = screen.getByText(
- /Sorry, there are no locations in this area/i
- )
- expect(descriptionFindAStore).toBeInTheDocument()
- expect(noLocationsInThisArea).toBeInTheDocument()
-
- expect(window.location.pathname).toBe('/uk/en-GB/store-locator')
- })
-})
-
-test('Allows customer to search for stores and expand to view store details', async () => {
- global.server.use(
- rest.get('*/shopper-stores/v1/organizations/*/store-search', (req, res, ctx) => {
- return res(ctx.delay(0), ctx.status(200), ctx.json(mockStores))
- })
- )
- const {user} = renderWithProviders(, {
- wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
- })
-
- await user.click(await screen.findByText('Find a Store'))
- const countrySelect = await screen.findByDisplayValue('Select a country')
- await user.selectOptions(countrySelect, 'DE')
+import React from 'react'
+import {render, screen} from '@testing-library/react'
+import StoreLocatorPage from '@salesforce/retail-react-app/app/pages/store-locator/index'
- const postalCodeInput = screen.getByPlaceholderText(/Enter postal code/i)
- await user.type(postalCodeInput, '65185')
+jest.mock('@salesforce/retail-react-app/app/components/store-locator', () => ({
+ StoreLocator: () => Mock Content
+}))
- const findButton = screen.getByRole('button', {name: 'Find'})
- await user.click(findButton)
+describe('StoreLocatorPage', () => {
+ it('renders the store locator page with content', () => {
+ render()
- await waitFor(() => {
- expect(screen.getByText('Wiesbaden Tech Depot')).toBeInTheDocument()
- expect(screen.getByText('Frankfurt Electronics Store')).toBeInTheDocument()
- })
+ // Verify the page wrapper is rendered
+ expect(screen.getByTestId('store-locator-page')).toBeTruthy()
- await waitFor(() => {
- expect(screen.getByText('Kirchgasse 12')).toBeInTheDocument()
- expect(screen.getByText(/Phone:\s*\+49 611 876543/)).toBeInTheDocument()
- expect(screen.getByText('0.74 km away')).toBeInTheDocument()
+ // Verify the mocked content is rendered
+ expect(screen.getByTestId('mock-store-locator-content')).toBeTruthy()
})
- const wiesbadenStoreElement = await screen.findByText('Wiesbaden Tech Depot')
- const wiesbadenAccordionItem = wiesbadenStoreElement.closest('.chakra-accordion__item')
-
- expect(wiesbadenAccordionItem).toBeTruthy()
-
- const viewMoreButton = within(wiesbadenAccordionItem).getByText('View More')
- await user.click(viewMoreButton)
-
- await waitFor(() => {
- expect(within(wiesbadenAccordionItem).getByText(/Monday 9 AM–7 PM/)).toBeInTheDocument()
- expect(within(wiesbadenAccordionItem).getByText(/Sunday Closed/)).toBeInTheDocument()
+ it('returns correct template name', () => {
+ expect(StoreLocatorPage.getTemplateName()).toBe('store-locator')
})
})
diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json
index 352d183530..5856856e31 100644
--- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json
+++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json
@@ -3289,140 +3289,6 @@
"value": " to proceed."
}
],
- "store_locator.action.find": [
- {
- "type": 0,
- "value": "Find"
- }
- ],
- "store_locator.action.select_a_country": [
- {
- "type": 0,
- "value": "Select a country"
- }
- ],
- "store_locator.action.use_my_location": [
- {
- "type": 0,
- "value": "Use My Location"
- }
- ],
- "store_locator.action.viewMore": [
- {
- "type": 0,
- "value": "View More"
- }
- ],
- "store_locator.description.away": [
- {
- "type": 0,
- "value": "away"
- }
- ],
- "store_locator.description.loading_locations": [
- {
- "type": 0,
- "value": "Loading locations..."
- }
- ],
- "store_locator.description.no_locations": [
- {
- "type": 0,
- "value": "Sorry, there are no locations in this area"
- }
- ],
- "store_locator.description.or": [
- {
- "type": 0,
- "value": "Or"
- }
- ],
- "store_locator.description.phone": [
- {
- "type": 0,
- "value": "Phone:"
- }
- ],
- "store_locator.description.viewing_near_postal_code": [
- {
- "type": 0,
- "value": "Viewing stores within "
- },
- {
- "type": 1,
- "value": "distance"
- },
- {
- "type": 1,
- "value": "distanceUnit"
- },
- {
- "type": 0,
- "value": " of "
- },
- {
- "type": 1,
- "value": "postalCode"
- },
- {
- "type": 0,
- "value": " in"
- }
- ],
- "store_locator.description.viewing_near_your_location": [
- {
- "type": 0,
- "value": "Viewing stores near your location"
- }
- ],
- "store_locator.dropdown.germany": [
- {
- "type": 0,
- "value": "Germany"
- }
- ],
- "store_locator.dropdown.united_states": [
- {
- "type": 0,
- "value": "United States"
- }
- ],
- "store_locator.error.agree_to_share_your_location": [
- {
- "type": 0,
- "value": "Please agree to share your location"
- }
- ],
- "store_locator.error.please_enter_a_postal_code": [
- {
- "type": 0,
- "value": "Please enter a postal code."
- }
- ],
- "store_locator.error.please_select_a_country": [
- {
- "type": 0,
- "value": "Please select a country."
- }
- ],
- "store_locator.field.placeholder.enter_postal_code": [
- {
- "type": 0,
- "value": "Enter postal code"
- }
- ],
- "store_locator.pagination.load_more": [
- {
- "type": 0,
- "value": "Load More"
- }
- ],
- "store_locator.title": [
- {
- "type": 0,
- "value": "Find a Store"
- }
- ],
"swatch_group.selected.label": [
{
"type": 1,
diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json
index 352d183530..5856856e31 100644
--- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json
+++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json
@@ -3289,140 +3289,6 @@
"value": " to proceed."
}
],
- "store_locator.action.find": [
- {
- "type": 0,
- "value": "Find"
- }
- ],
- "store_locator.action.select_a_country": [
- {
- "type": 0,
- "value": "Select a country"
- }
- ],
- "store_locator.action.use_my_location": [
- {
- "type": 0,
- "value": "Use My Location"
- }
- ],
- "store_locator.action.viewMore": [
- {
- "type": 0,
- "value": "View More"
- }
- ],
- "store_locator.description.away": [
- {
- "type": 0,
- "value": "away"
- }
- ],
- "store_locator.description.loading_locations": [
- {
- "type": 0,
- "value": "Loading locations..."
- }
- ],
- "store_locator.description.no_locations": [
- {
- "type": 0,
- "value": "Sorry, there are no locations in this area"
- }
- ],
- "store_locator.description.or": [
- {
- "type": 0,
- "value": "Or"
- }
- ],
- "store_locator.description.phone": [
- {
- "type": 0,
- "value": "Phone:"
- }
- ],
- "store_locator.description.viewing_near_postal_code": [
- {
- "type": 0,
- "value": "Viewing stores within "
- },
- {
- "type": 1,
- "value": "distance"
- },
- {
- "type": 1,
- "value": "distanceUnit"
- },
- {
- "type": 0,
- "value": " of "
- },
- {
- "type": 1,
- "value": "postalCode"
- },
- {
- "type": 0,
- "value": " in"
- }
- ],
- "store_locator.description.viewing_near_your_location": [
- {
- "type": 0,
- "value": "Viewing stores near your location"
- }
- ],
- "store_locator.dropdown.germany": [
- {
- "type": 0,
- "value": "Germany"
- }
- ],
- "store_locator.dropdown.united_states": [
- {
- "type": 0,
- "value": "United States"
- }
- ],
- "store_locator.error.agree_to_share_your_location": [
- {
- "type": 0,
- "value": "Please agree to share your location"
- }
- ],
- "store_locator.error.please_enter_a_postal_code": [
- {
- "type": 0,
- "value": "Please enter a postal code."
- }
- ],
- "store_locator.error.please_select_a_country": [
- {
- "type": 0,
- "value": "Please select a country."
- }
- ],
- "store_locator.field.placeholder.enter_postal_code": [
- {
- "type": 0,
- "value": "Enter postal code"
- }
- ],
- "store_locator.pagination.load_more": [
- {
- "type": 0,
- "value": "Load More"
- }
- ],
- "store_locator.title": [
- {
- "type": 0,
- "value": "Find a Store"
- }
- ],
"swatch_group.selected.label": [
{
"type": 1,
diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json
index 75acacecc2..e2ed079b0b 100644
--- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json
+++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json
@@ -7001,292 +7001,6 @@
"value": "]"
}
],
- "store_locator.action.find": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ƒīƞḓ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.action.select_a_country": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Şḗḗŀḗḗƈŧ ȧȧ ƈǿǿŭŭƞŧřẏ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.action.use_my_location": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ŭşḗḗ Ḿẏ Ŀǿǿƈȧȧŧīǿǿƞ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.action.viewMore": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ṽīḗḗẇ Ḿǿǿřḗḗ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.description.away": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "ȧȧẇȧȧẏ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.description.loading_locations": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ŀǿǿȧȧḓīƞɠ ŀǿǿƈȧȧŧīǿǿƞş..."
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.description.no_locations": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Şǿǿřřẏ, ŧħḗḗřḗḗ ȧȧřḗḗ ƞǿǿ ŀǿǿƈȧȧŧīǿǿƞş īƞ ŧħīş ȧȧřḗḗȧȧ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.description.or": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ǿř"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.description.phone": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ƥħǿǿƞḗḗ:"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.description.viewing_near_postal_code": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ṽīḗḗẇīƞɠ şŧǿǿřḗḗş ẇīŧħīƞ "
- },
- {
- "type": 1,
- "value": "distance"
- },
- {
- "type": 1,
- "value": "distanceUnit"
- },
- {
- "type": 0,
- "value": " ǿǿƒ "
- },
- {
- "type": 1,
- "value": "postalCode"
- },
- {
- "type": 0,
- "value": " īƞ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.description.viewing_near_your_location": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ṽīḗḗẇīƞɠ şŧǿǿřḗḗş ƞḗḗȧȧř ẏǿǿŭŭř ŀǿǿƈȧȧŧīǿǿƞ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.dropdown.germany": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ɠḗḗřḿȧȧƞẏ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.dropdown.united_states": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ŭƞīŧḗḗḓ Şŧȧȧŧḗḗş"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.error.agree_to_share_your_location": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ƥŀḗḗȧȧşḗḗ ȧȧɠřḗḗḗḗ ŧǿǿ şħȧȧřḗḗ ẏǿǿŭŭř ŀǿǿƈȧȧŧīǿǿƞ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.error.please_enter_a_postal_code": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ƥŀḗḗȧȧşḗḗ ḗḗƞŧḗḗř ȧȧ ƥǿǿşŧȧȧŀ ƈǿǿḓḗḗ."
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.error.please_select_a_country": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ƥŀḗḗȧȧşḗḗ şḗḗŀḗḗƈŧ ȧȧ ƈǿǿŭŭƞŧřẏ."
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.field.placeholder.enter_postal_code": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ḗƞŧḗḗř ƥǿǿşŧȧȧŀ ƈǿǿḓḗḗ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.pagination.load_more": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ŀǿǿȧȧḓ Ḿǿǿřḗḗ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
- "store_locator.title": [
- {
- "type": 0,
- "value": "["
- },
- {
- "type": 0,
- "value": "Ƒīƞḓ ȧȧ Şŧǿǿřḗḗ"
- },
- {
- "type": 0,
- "value": "]"
- }
- ],
"swatch_group.selected.label": [
{
"type": 0,
diff --git a/packages/template-retail-react-app/app/utils/test-utils.js b/packages/template-retail-react-app/app/utils/test-utils.js
index c6105b6f37..f9b496e11e 100644
--- a/packages/template-retail-react-app/app/utils/test-utils.js
+++ b/packages/template-retail-react-app/app/utils/test-utils.js
@@ -20,10 +20,23 @@ import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/compon
import fallbackMessages from '@salesforce/retail-react-app/app/static/translations/compiled/en-GB.json'
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
// Contexts
-import {CurrencyProvider, MultiSiteProvider} from '@salesforce/retail-react-app/app/contexts'
+import {
+ CurrencyProvider,
+ MultiSiteProvider,
+ StoreLocatorProvider
+} from '@salesforce/retail-react-app/app/contexts'
import {createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url'
import {getSiteByReference} from '@salesforce/retail-react-app/app/utils/site-utils'
+import {
+ STORE_LOCATOR_RADIUS,
+ STORE_LOCATOR_RADIUS_UNIT,
+ STORE_LOCATOR_DEFAULT_COUNTRY,
+ STORE_LOCATOR_DEFAULT_COUNTRY_CODE,
+ STORE_LOCATOR_DEFAULT_POSTAL_CODE,
+ STORE_LOCATOR_DEFAULT_PAGE_SIZE,
+ STORE_LOCATOR_SUPPORTED_COUNTRIES
+} from '@salesforce/retail-react-app/app/constants'
import jwt from 'jsonwebtoken'
import userEvent from '@testing-library/user-event'
// This JWT's payload is special
@@ -120,6 +133,16 @@ export const TestProviders = ({
locale.alias || locale.id
)
+ const storeLocatorConfig = {
+ radius: STORE_LOCATOR_RADIUS,
+ radiusUnit: STORE_LOCATOR_RADIUS_UNIT,
+ defaultCountry: STORE_LOCATOR_DEFAULT_COUNTRY,
+ defaultCountryCode: STORE_LOCATOR_DEFAULT_COUNTRY_CODE,
+ defaultPostalCode: STORE_LOCATOR_DEFAULT_POSTAL_CODE,
+ defaultPageSize: STORE_LOCATOR_DEFAULT_PAGE_SIZE,
+ supportedCountries: STORE_LOCATOR_SUPPORTED_COUNTRIES
+ }
+
return (
@@ -135,11 +158,13 @@ export const TestProviders = ({
fetchedToken={bypassAuth ? (isGuest ? guestToken : registerUserToken) : ''}
>
-
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json
index c6e5c939e7..ba6307226d 100644
--- a/packages/template-retail-react-app/translations/en-GB.json
+++ b/packages/template-retail-react-app/translations/en-GB.json
@@ -1406,63 +1406,6 @@
"social_login_redirect.message.redirect_link": {
"defaultMessage": "If you are not automatically redirected, click this link to proceed."
},
- "store_locator.action.find": {
- "defaultMessage": "Find"
- },
- "store_locator.action.select_a_country": {
- "defaultMessage": "Select a country"
- },
- "store_locator.action.use_my_location": {
- "defaultMessage": "Use My Location"
- },
- "store_locator.action.viewMore": {
- "defaultMessage": "View More"
- },
- "store_locator.description.away": {
- "defaultMessage": "away"
- },
- "store_locator.description.loading_locations": {
- "defaultMessage": "Loading locations..."
- },
- "store_locator.description.no_locations": {
- "defaultMessage": "Sorry, there are no locations in this area"
- },
- "store_locator.description.or": {
- "defaultMessage": "Or"
- },
- "store_locator.description.phone": {
- "defaultMessage": "Phone:"
- },
- "store_locator.description.viewing_near_postal_code": {
- "defaultMessage": "Viewing stores within {distance}{distanceUnit} of {postalCode} in"
- },
- "store_locator.description.viewing_near_your_location": {
- "defaultMessage": "Viewing stores near your location"
- },
- "store_locator.dropdown.germany": {
- "defaultMessage": "Germany"
- },
- "store_locator.dropdown.united_states": {
- "defaultMessage": "United States"
- },
- "store_locator.error.agree_to_share_your_location": {
- "defaultMessage": "Please agree to share your location"
- },
- "store_locator.error.please_enter_a_postal_code": {
- "defaultMessage": "Please enter a postal code."
- },
- "store_locator.error.please_select_a_country": {
- "defaultMessage": "Please select a country."
- },
- "store_locator.field.placeholder.enter_postal_code": {
- "defaultMessage": "Enter postal code"
- },
- "store_locator.pagination.load_more": {
- "defaultMessage": "Load More"
- },
- "store_locator.title": {
- "defaultMessage": "Find a Store"
- },
"swatch_group.selected.label": {
"defaultMessage": "{label}:"
},
diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json
index c6e5c939e7..ba6307226d 100644
--- a/packages/template-retail-react-app/translations/en-US.json
+++ b/packages/template-retail-react-app/translations/en-US.json
@@ -1406,63 +1406,6 @@
"social_login_redirect.message.redirect_link": {
"defaultMessage": "If you are not automatically redirected, click this link to proceed."
},
- "store_locator.action.find": {
- "defaultMessage": "Find"
- },
- "store_locator.action.select_a_country": {
- "defaultMessage": "Select a country"
- },
- "store_locator.action.use_my_location": {
- "defaultMessage": "Use My Location"
- },
- "store_locator.action.viewMore": {
- "defaultMessage": "View More"
- },
- "store_locator.description.away": {
- "defaultMessage": "away"
- },
- "store_locator.description.loading_locations": {
- "defaultMessage": "Loading locations..."
- },
- "store_locator.description.no_locations": {
- "defaultMessage": "Sorry, there are no locations in this area"
- },
- "store_locator.description.or": {
- "defaultMessage": "Or"
- },
- "store_locator.description.phone": {
- "defaultMessage": "Phone:"
- },
- "store_locator.description.viewing_near_postal_code": {
- "defaultMessage": "Viewing stores within {distance}{distanceUnit} of {postalCode} in"
- },
- "store_locator.description.viewing_near_your_location": {
- "defaultMessage": "Viewing stores near your location"
- },
- "store_locator.dropdown.germany": {
- "defaultMessage": "Germany"
- },
- "store_locator.dropdown.united_states": {
- "defaultMessage": "United States"
- },
- "store_locator.error.agree_to_share_your_location": {
- "defaultMessage": "Please agree to share your location"
- },
- "store_locator.error.please_enter_a_postal_code": {
- "defaultMessage": "Please enter a postal code."
- },
- "store_locator.error.please_select_a_country": {
- "defaultMessage": "Please select a country."
- },
- "store_locator.field.placeholder.enter_postal_code": {
- "defaultMessage": "Enter postal code"
- },
- "store_locator.pagination.load_more": {
- "defaultMessage": "Load More"
- },
- "store_locator.title": {
- "defaultMessage": "Find a Store"
- },
"swatch_group.selected.label": {
"defaultMessage": "{label}:"
},