diff --git a/app/javascript/packages/address-search/hooks/use-usps-locations.ts b/app/javascript/packages/address-search/hooks/use-usps-locations.ts index 1ec1ff7f206..c5365101680 100644 --- a/app/javascript/packages/address-search/hooks/use-usps-locations.ts +++ b/app/javascript/packages/address-search/hooks/use-usps-locations.ts @@ -5,20 +5,20 @@ import useSWR from 'swr/immutable'; import type { Location, FormattedLocation, LocationQuery, PostOffice } from '../types'; import { formatLocations, snakeCase, transformKeys } from '../utils'; -const requestUspsLocations = async ({ +export async function requestUspsLocations({ locationsURL, address, }: { locationsURL: string; address: LocationQuery; -}): Promise => { +}): Promise { const response = await request(locationsURL, { method: 'post', json: { address: transformKeys(address, snakeCase) }, }); return formatLocations(response); -}; +} function requestAddressCandidates({ unvalidatedAddressInput, diff --git a/app/javascript/packages/address-search/index.tsx b/app/javascript/packages/address-search/index.tsx index 2de5e8a0b9a..0ce14767c33 100644 --- a/app/javascript/packages/address-search/index.tsx +++ b/app/javascript/packages/address-search/index.tsx @@ -3,6 +3,7 @@ import InPersonLocations from './components/in-person-locations'; import AddressInput from './components/address-input'; import AddressSearch from './components/address-search'; import NoInPersonLocationsDisplay from './components/no-in-person-locations-display'; +import { requestUspsLocations } from './hooks/use-usps-locations'; export { snakeCase, @@ -11,6 +12,7 @@ export { InPersonLocations, AddressInput, NoInPersonLocationsDisplay, + requestUspsLocations, }; export default AddressSearch; diff --git a/app/javascript/packages/document-capture/components/in-person-full-address-search.tsx b/app/javascript/packages/document-capture/components/in-person-full-address-search.tsx index 83c91d7c93b..0f3d3639feb 100644 --- a/app/javascript/packages/document-capture/components/in-person-full-address-search.tsx +++ b/app/javascript/packages/document-capture/components/in-person-full-address-search.tsx @@ -1,139 +1,13 @@ import { TextInput, SelectInput } from '@18f/identity-components'; import { useState, useRef, useEffect, useCallback, useContext } from 'react'; import { t } from '@18f/identity-i18n'; -import { request } from '@18f/identity-request'; import ValidatedField from '@18f/identity-validated-field/validated-field'; import SpinnerButton, { SpinnerButtonRefHandle } from '@18f/identity-spinner-button/spinner-button'; import type { RegisterFieldCallback } from '@18f/identity-form-steps'; -import useSWR from 'swr/immutable'; import { useDidUpdateEffect } from '@18f/identity-react-hooks'; -import { transformKeys, snakeCase } from '@18f/identity-address-search'; -import type { - LocationQuery, - PostOffice, - FormattedLocation, -} from '@18f/identity-address-search/types'; +import type { LocationQuery, FormattedLocation } from '@18f/identity-address-search/types'; import { InPersonContext } from '../context'; - -const formatLocations = (postOffices: PostOffice[]): FormattedLocation[] => - postOffices.map((po: PostOffice, index) => ({ - formattedCityStateZip: `${po.city}, ${po.state}, ${po.zip_code_5}-${po.zip_code_4}`, - id: index, - distance: po.distance, - name: po.name, - saturdayHours: po.saturday_hours, - streetAddress: po.address, - sundayHours: po.sunday_hours, - weekdayHours: po.weekday_hours, - isPilot: !!po.is_pilot, - })); - -const requestUspsLocations = async ({ - address, - locationsURL, -}: { - locationsURL: string; - address: LocationQuery; -}): Promise => { - const response = await request(locationsURL, { - method: 'post', - json: { address: transformKeys(address, snakeCase) }, - }); - - return formatLocations(response); -}; - -function useUspsLocations(locationsURL: string) { - const [locationQuery, setLocationQuery] = useState(null); - const validatedAddressFieldRef = useRef(null); - const validatedCityFieldRef = useRef(null); - const validatedStateFieldRef = useRef(null); - const validatedZipCodeFieldRef = useRef(null); - - const checkValidityAndDisplayErrors = (address, city, state, zipCode) => { - let formIsValid = true; - const zipCodeIsValid = zipCode.length === 5 && !!zipCode.match(/\d{5}/); - - if (address.length === 0) { - validatedAddressFieldRef.current?.setCustomValidity(t('simple_form.required.text')); - formIsValid = false; - } else { - validatedAddressFieldRef.current?.setCustomValidity(''); - } - - if (city.length === 0) { - formIsValid = false; - validatedCityFieldRef.current?.setCustomValidity(t('simple_form.required.text')); - } else { - validatedCityFieldRef.current?.setCustomValidity(''); - } - - if (state.length === 0) { - formIsValid = false; - validatedStateFieldRef.current?.setCustomValidity(t('simple_form.required.text')); - } else { - validatedStateFieldRef.current?.setCustomValidity(''); - } - - if (zipCode.length === 0) { - formIsValid = false; - validatedZipCodeFieldRef.current?.setCustomValidity(t('simple_form.required.text')); - } else { - validatedZipCodeFieldRef.current?.setCustomValidity(''); - } - - validatedAddressFieldRef.current?.reportValidity(); - validatedCityFieldRef.current?.reportValidity(); - validatedStateFieldRef.current?.reportValidity(); - validatedZipCodeFieldRef.current?.reportValidity(); - - return formIsValid && zipCodeIsValid; - }; - - const handleLocationSearch = useCallback( - (event, addressValue, cityValue, stateValue, zipCodeValue) => { - event.preventDefault(); - const address = addressValue.trim(); - const city = cityValue.trim(); - const zipCode = zipCodeValue.trim(); - - const formIsValid = checkValidityAndDisplayErrors(address, city, stateValue, zipCode); - - if (!formIsValid) { - return; - } - - setLocationQuery({ - address: `${address}, ${city}, ${stateValue} ${zipCode}`, - streetAddress: address, - city, - state: stateValue, - zipCode, - }); - }, - [], - ); - - const { - data: locationResults, - isLoading: isLoadingLocations, - error: uspsError, - } = useSWR([locationQuery], ([address]) => - address ? requestUspsLocations({ address, locationsURL }) : null, - ); - - return { - locationQuery, - locationResults, - uspsError, - isLoading: isLoadingLocations, - handleLocationSearch, - validatedAddressFieldRef, - validatedCityFieldRef, - validatedStateFieldRef, - validatedZipCodeFieldRef, - }; -} +import useValidatedUspsLocations from '../hooks/use-validated-usps-locations'; interface FullAddressSearchProps { registerField?: RegisterFieldCallback; @@ -170,7 +44,7 @@ function FullAddressSearch({ validatedCityFieldRef, validatedStateFieldRef, validatedZipCodeFieldRef, - } = useUspsLocations(locationsURL); + } = useValidatedUspsLocations(locationsURL); const inputChangeHandler = (input) => diff --git a/app/javascript/packages/document-capture/hooks/use-validated-usps-locations.spec.ts b/app/javascript/packages/document-capture/hooks/use-validated-usps-locations.spec.ts new file mode 100644 index 00000000000..a711f27b229 --- /dev/null +++ b/app/javascript/packages/document-capture/hooks/use-validated-usps-locations.spec.ts @@ -0,0 +1,62 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import type { SetupServer } from 'msw/node'; +import useValidatedUspsLocations from './use-validated-usps-locations'; +import { LOCATIONS_URL } from '../components/in-person-location-post-office-search-step'; + +const USPS_RESPONSE = [ + { + address: '100 Main St E, Bronwood, Georgia, 39826', + location: { + latitude: 31.831686000000005, + longitude: -84.363768, + }, + street_address: '100 Main St E', + city: 'Bronwood', + state: 'GA', + zip_code: '39826', + }, + { + address: '200 Main St E, Bronwood, Georgia, 39826', + location: { + latitude: 32.831686000000005, + longitude: -83.363768, + }, + street_address: '200 Main St E', + city: 'Bronwood', + state: 'GA', + zip_code: '39826', + }, +]; + +describe('useValidatedUspsLocations', () => { + let server: SetupServer; + + before(() => { + server = setupServer(); + server.listen(); + }); + + after(() => { + server.close(); + }); + + beforeEach(() => { + server.resetHandlers(); + server.use(rest.post(LOCATIONS_URL, (_req, res, ctx) => res(ctx.json(USPS_RESPONSE)))); + }); + + it('returns location results', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useValidatedUspsLocations(LOCATIONS_URL), + ); + + const { handleLocationSearch } = result.current; + handleLocationSearch(new Event('submit'), '200 main', 'Endeavor', 'DE', '12345'); + + await waitForNextUpdate(); + + expect(result.current.locationResults?.length).to.equal(USPS_RESPONSE.length); + }); +}); diff --git a/app/javascript/packages/document-capture/hooks/use-validated-usps-locations.ts b/app/javascript/packages/document-capture/hooks/use-validated-usps-locations.ts new file mode 100644 index 00000000000..829e4a77041 --- /dev/null +++ b/app/javascript/packages/document-capture/hooks/use-validated-usps-locations.ts @@ -0,0 +1,97 @@ +import { useState, useRef, useCallback } from 'react'; +import { requestUspsLocations } from '@18f/identity-address-search'; +import useSWR from 'swr/immutable'; +import type { LocationQuery } from '@18f/identity-address-search/types'; +import { t } from '@18f/identity-i18n'; + +export default function useValidatedUspsLocations(locationsURL: string) { + const [locationQuery, setLocationQuery] = useState(null); + const validatedAddressFieldRef = useRef(null); + const validatedCityFieldRef = useRef(null); + const validatedStateFieldRef = useRef(null); + const validatedZipCodeFieldRef = useRef(null); + + const checkValidityAndDisplayErrors = (address, city, state, zipCode) => { + let formIsValid = true; + const zipCodeIsValid = zipCode.length === 5 && !!zipCode.match(/\d{5}/); + + if (address.length === 0) { + validatedAddressFieldRef.current?.setCustomValidity(t('simple_form.required.text')); + formIsValid = false; + } else { + validatedAddressFieldRef.current?.setCustomValidity(''); + } + + if (city.length === 0) { + formIsValid = false; + validatedCityFieldRef.current?.setCustomValidity(t('simple_form.required.text')); + } else { + validatedCityFieldRef.current?.setCustomValidity(''); + } + + if (state.length === 0) { + formIsValid = false; + validatedStateFieldRef.current?.setCustomValidity(t('simple_form.required.text')); + } else { + validatedStateFieldRef.current?.setCustomValidity(''); + } + + if (zipCode.length === 0) { + formIsValid = false; + validatedZipCodeFieldRef.current?.setCustomValidity(t('simple_form.required.text')); + } else { + validatedZipCodeFieldRef.current?.setCustomValidity(''); + } + + validatedAddressFieldRef.current?.reportValidity(); + validatedCityFieldRef.current?.reportValidity(); + validatedStateFieldRef.current?.reportValidity(); + validatedZipCodeFieldRef.current?.reportValidity(); + + return formIsValid && zipCodeIsValid; + }; + + const handleLocationSearch = useCallback( + (event, addressValue, cityValue, stateValue, zipCodeValue) => { + event.preventDefault(); + const address = addressValue.trim(); + const city = cityValue.trim(); + const zipCode = zipCodeValue.trim(); + + const formIsValid = checkValidityAndDisplayErrors(address, city, stateValue, zipCode); + + if (!formIsValid) { + return; + } + + setLocationQuery({ + address: `${address}, ${city}, ${stateValue} ${zipCode}`, + streetAddress: address, + city, + state: stateValue, + zipCode, + }); + }, + [], + ); + + const { + data: locationResults, + isLoading: isLoadingLocations, + error: uspsError, + } = useSWR([locationQuery], ([address]) => + address ? requestUspsLocations({ address, locationsURL }) : null, + ); + + return { + locationQuery, + locationResults, + uspsError, + isLoading: isLoadingLocations, + handleLocationSearch, + validatedAddressFieldRef, + validatedCityFieldRef, + validatedStateFieldRef, + validatedZipCodeFieldRef, + }; +}