Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<FormattedLocation[]> => {
}): Promise<FormattedLocation[]> {
const response = await request<PostOffice[]>(locationsURL, {
method: 'post',
json: { address: transformKeys(address, snakeCase) },
});

return formatLocations(response);
};
}

function requestAddressCandidates({
unvalidatedAddressInput,
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/packages/address-search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -11,6 +12,7 @@ export {
InPersonLocations,
AddressInput,
NoInPersonLocationsDisplay,
requestUspsLocations,
};

export default AddressSearch;
Original file line number Diff line number Diff line change
@@ -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<FormattedLocation[]> => {
const response = await request<PostOffice[]>(locationsURL, {
method: 'post',
json: { address: transformKeys(address, snakeCase) },
});

return formatLocations(response);
};

function useUspsLocations(locationsURL: string) {
const [locationQuery, setLocationQuery] = useState<LocationQuery | null>(null);
const validatedAddressFieldRef = useRef<HTMLFormElement>(null);
const validatedCityFieldRef = useRef<HTMLFormElement>(null);
const validatedStateFieldRef = useRef<HTMLFormElement>(null);
const validatedZipCodeFieldRef = useRef<HTMLFormElement>(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;
Expand Down Expand Up @@ -170,7 +44,7 @@ function FullAddressSearch({
validatedCityFieldRef,
validatedStateFieldRef,
validatedZipCodeFieldRef,
} = useUspsLocations(locationsURL);
} = useValidatedUspsLocations(locationsURL);

const inputChangeHandler =
<T extends HTMLElement & { value: string }>(input) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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<LocationQuery | null>(null);
const validatedAddressFieldRef = useRef<HTMLFormElement>(null);
const validatedCityFieldRef = useRef<HTMLFormElement>(null);
const validatedStateFieldRef = useRef<HTMLFormElement>(null);
const validatedZipCodeFieldRef = useRef<HTMLFormElement>(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,
};
}