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 205ff2d4e9b..208a7b70bc9 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,5 +1,5 @@ -import { TextInput } from '@18f/identity-components'; -import { useState, useRef, useEffect, useCallback } from 'react'; +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'; @@ -15,6 +15,7 @@ import { LOCATIONS_URL, PostOffice, } from '@18f/identity-address-search'; +import { InPersonContext } from '../context'; const formatLocations = (postOffices: PostOffice[]): FormattedLocation[] => postOffices.map((po: PostOffice, index) => ({ @@ -46,7 +47,7 @@ function useUspsLocations() { const validatedZipCodeFieldRef = useRef(null); const handleLocationSearch = useCallback( - (event, addressInput, cityInput, stateInput, zipCodeInput) => { + (event, addressValue, cityValue, stateValue, zipCodeValue) => { event.preventDefault(); validatedAddressFieldRef.current?.setCustomValidity(''); validatedAddressFieldRef.current?.reportValidity(); @@ -58,20 +59,20 @@ function useUspsLocations() { validatedZipCodeFieldRef.current?.reportValidity(); if ( - addressInput.trim().length === 0 || - cityInput.trim().length === 0 || - stateInput.trim().length === 0 || - zipCodeInput.trim().length === 0 + addressValue.trim().length === 0 || + cityValue.trim().length === 0 || + stateValue.trim().length === 0 || + zipCodeValue.trim().length === 0 ) { return; } setLocationQuery({ - address: `${addressInput}, ${cityInput}, ${stateInput} ${zipCodeInput}`, - streetAddress: addressInput, - city: cityInput, - state: stateInput, - zipCode: zipCodeInput, + address: `${addressValue}, ${cityValue}, ${stateValue} ${zipCodeValue}`, + streetAddress: addressValue, + city: cityValue, + state: stateValue, + zipCode: zipCodeValue, }); }, [], @@ -114,12 +115,11 @@ function FullAddressSearch({ onError = () => undefined, disabled = false, }: FullAddressSearchProps) { - // todo: should we get rid of verbose 'input' word? const spinnerButtonRef = useRef(null); - const [addressInput, setAddressInput] = useState(''); - const [cityInput, setCityInput] = useState(''); - const [stateInput, setStateInput] = useState(''); - const [zipCodeInput, setZipCodeInput] = useState(''); + const [addressValue, setAddressValue] = useState(''); + const [cityValue, setCityValue] = useState(''); + const [stateValue, setStateValue] = useState(''); + const [zipCodeValue, setZipCodeValue] = useState(''); const { locationQuery, locationResults, @@ -132,15 +132,17 @@ function FullAddressSearch({ validatedZipCodeFieldRef, } = useUspsLocations(); - const textInputChangeHandler = (input) => (event: React.ChangeEvent) => { - const { target } = event; - input(target.value); - }; + const inputChangeHandler = + (input) => + (event: React.ChangeEvent) => { + const { target } = event; + input(target.value); + }; - const onAddressChange = textInputChangeHandler(setAddressInput); - const onCityChange = textInputChangeHandler(setCityInput); - const onStateChange = textInputChangeHandler(setStateInput); - const onZipCodeChange = textInputChangeHandler(setZipCodeInput); + const onAddressChange = inputChangeHandler(setAddressValue); + const onCityChange = inputChangeHandler(setCityValue); + const onStateChange = inputChangeHandler(setStateValue); + const onZipCodeChange = inputChangeHandler(setZipCodeValue); useEffect(() => { spinnerButtonRef.current?.toggleSpinner(isLoading); @@ -158,18 +160,20 @@ function FullAddressSearch({ const handleSearch = useCallback( (event) => { onError(null); - onSearch(event, addressInput, cityInput, stateInput, zipCodeInput); + onSearch(event, addressValue, cityValue, stateValue, zipCodeValue); }, - [addressInput, cityInput, stateInput, zipCodeInput], + [addressValue, cityValue, stateValue, zipCodeValue], ); + const { usStatesTerritories } = useContext(InPersonContext); + return ( <> - + > + + {usStatesTerritories.map(([name, abbr]) => ( + + ))} + { it('renders the step', () => { const { getByRole } = render( - , - { - wrapper, - }, + + , + , + { wrapper }, ); expect(getByRole('heading', { name: 'in_person_proofing.headings.po_search.location' })); @@ -83,7 +92,16 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { it('displays a try again error message', async () => { const { findByText, findByLabelText } = render( - , + + , + , { wrapper }, ); @@ -95,7 +113,7 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { await findByLabelText('in_person_proofing.body.location.po_search.city_label'), 'Endeavor', ); - await userEvent.type( + await userEvent.selectOptions( await findByLabelText('in_person_proofing.body.location.po_search.state_label'), 'DE', ); @@ -115,10 +133,17 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { it('displays validation error messages to the user if fields are empty', async () => { const { findAllByText, findByText } = render( - , - { - wrapper, - }, + + , + , + { wrapper }, ); await userEvent.click( @@ -131,7 +156,16 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { it('displays no post office results if a successful search is followed by an unsuccessful search', async () => { const { findByText, findByLabelText, queryByRole } = render( - , + + , + , { wrapper }, ); @@ -143,7 +177,7 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { await findByLabelText('in_person_proofing.body.location.po_search.city_label'), 'Endeavor', ); - await userEvent.type( + await userEvent.selectOptions( await findByLabelText('in_person_proofing.body.location.po_search.state_label'), 'DE', ); @@ -171,7 +205,16 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { it('clicking search again after first results do not clear results', async () => { const { findAllByText, findByText, findByLabelText } = render( - , + + , + , { wrapper }, ); @@ -183,7 +226,7 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { await findByLabelText('in_person_proofing.body.location.po_search.city_label'), 'Endeavor', ); - await userEvent.type( + await userEvent.selectOptions( await findByLabelText('in_person_proofing.body.location.po_search.state_label'), 'DE', ); @@ -215,7 +258,17 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { }) } > - + + , + + , , { wrapper }, ); @@ -227,7 +280,7 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { await findByLabelText('in_person_proofing.body.location.po_search.city_label'), 'Endeavor', ); - await userEvent.type( + await userEvent.selectOptions( await findByLabelText('in_person_proofing.body.location.po_search.state_label'), 'DE', ); @@ -266,7 +319,17 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { }) } > - + + , + + , , { wrapper }, ); @@ -279,7 +342,7 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { await findByLabelText('in_person_proofing.body.location.po_search.city_label'), 'Endeavor', ); - await userEvent.type( + await userEvent.selectOptions( await findByLabelText('in_person_proofing.body.location.po_search.state_label'), 'DE', ); @@ -303,7 +366,16 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { it('allows user to select a location', async () => { const { findAllByText, findByLabelText, findByText, queryByText } = render( - , + + , + , { wrapper }, ); await userEvent.type( @@ -314,7 +386,7 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { await findByLabelText('in_person_proofing.body.location.po_search.city_label'), 'Endeavor', ); - await userEvent.type( + await userEvent.selectOptions( await findByLabelText('in_person_proofing.body.location.po_search.state_label'), 'DE', ); @@ -333,9 +405,6 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { await userEvent.clear( await findByLabelText('in_person_proofing.body.location.po_search.city_label'), ); - await userEvent.clear( - await findByLabelText('in_person_proofing.body.location.po_search.state_label'), - ); await userEvent.clear( await findByLabelText('in_person_proofing.body.location.po_search.zipcode_label'), ); diff --git a/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.tsx b/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.tsx index e0506317236..c57dbd34c93 100644 --- a/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.tsx +++ b/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.tsx @@ -128,7 +128,7 @@ function InPersonLocationFullAddressEntryPostOfficeSearchStep({ )} diff --git a/app/javascript/packages/document-capture/components/in-person-outage-alert.spec.tsx b/app/javascript/packages/document-capture/components/in-person-outage-alert.spec.tsx index f69d556b7f0..f8440070c7a 100644 --- a/app/javascript/packages/document-capture/components/in-person-outage-alert.spec.tsx +++ b/app/javascript/packages/document-capture/components/in-person-outage-alert.spec.tsx @@ -11,6 +11,7 @@ describe('InPersonOutageAlert', () => { inPersonOutageExpectedUpdateDate: 'January 1, 2024', inPersonOutageMessageEnabled: true, inPersonFullAddressEntryEnabled: false, + usStatesTerritories: [], }} > diff --git a/app/javascript/packages/document-capture/components/in-person-prepare-step.spec.tsx b/app/javascript/packages/document-capture/components/in-person-prepare-step.spec.tsx index 4c6543b8056..933c97aa072 100644 --- a/app/javascript/packages/document-capture/components/in-person-prepare-step.spec.tsx +++ b/app/javascript/packages/document-capture/components/in-person-prepare-step.spec.tsx @@ -26,6 +26,7 @@ describe('InPersonPrepareStep', () => { inPersonOutageMessageEnabled: true, inPersonOutageExpectedUpdateDate: 'January 1, 2024', inPersonFullAddressEntryEnabled: false, + usStatesTerritories: [], }} > @@ -38,7 +39,11 @@ describe('InPersonPrepareStep', () => { it('does not render a warning when the flag is disabled', () => { const { queryByText } = render( , diff --git a/app/javascript/packages/document-capture/context/in-person.ts b/app/javascript/packages/document-capture/context/in-person.ts index d554fae3c55..818ada23e4c 100644 --- a/app/javascript/packages/document-capture/context/in-person.ts +++ b/app/javascript/packages/document-capture/context/in-person.ts @@ -20,11 +20,18 @@ export interface InPersonContextProps { * When true users must enter a full address when searching for a Post Office location */ inPersonFullAddressEntryEnabled: boolean; + + /** + * Collection of US states and territories + * Each item is [Long name, abbreviation], e.g. ['Ohio', 'OH'] + */ + usStatesTerritories: Array<[string, string]>; } const InPersonContext = createContext({ inPersonOutageMessageEnabled: false, inPersonFullAddressEntryEnabled: false, + usStatesTerritories: [], }); InPersonContext.displayName = 'InPersonContext'; diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index 19f936a3685..d70821ccd6e 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -83,8 +83,14 @@ const { inPersonFullAddressEntryEnabled, inPersonOutageMessageEnabled, inPersonOutageExpectedUpdateDate, + usStatesTerritories = '', } = appRoot.dataset as DOMStringMap & AppRootData; +let parsedUsStatesTerritories = []; +try { + parsedUsStatesTerritories = JSON.parse(usStatesTerritories); +} catch (e) {} + const App = composeComponents( [MarketingSiteContextProvider, { helpCenterRedirectURL, securityAndPrivacyHowItWorksURL }], [DeviceContext.Provider, { value: device }], @@ -96,6 +102,7 @@ const App = composeComponents( inPersonOutageMessageEnabled: inPersonOutageMessageEnabled === 'true', inPersonOutageExpectedUpdateDate, inPersonFullAddressEntryEnabled: inPersonFullAddressEntryEnabled === 'true', + usStatesTerritories: parsedUsStatesTerritories, }, }, ], diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index 204e8406a81..54a48fe0836 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -33,6 +33,7 @@ in_person_full_address_entry_enabled: IdentityConfig.store.in_person_full_address_entry_enabled, in_person_outage_message_enabled: IdentityConfig.store.in_person_outage_message_enabled, in_person_outage_expected_update_date: IdentityConfig.store.in_person_outage_expected_update_date, + us_states_territories: us_states_territories, } %> <%= simple_form_for( :doc_auth, diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb index cb7b884f0d8..01ed1149994 100644 --- a/spec/support/features/in_person_helper.rb +++ b/spec/support/features/in_person_helper.rb @@ -118,8 +118,7 @@ def search_for_post_office_with_full_address with: GOOD_ADDRESS1 fill_in t('in_person_proofing.body.location.po_search.city_label'), with: GOOD_CITY - fill_in t('in_person_proofing.body.location.po_search.state_label'), - with: GOOD_STATE + select GOOD_STATE, from: t('in_person_proofing.form.state_id.identity_doc_address_state') fill_in t('in_person_proofing.body.location.po_search.zipcode_label'), with: GOOD_ZIPCODE click_spinner_button_and_wait(t('in_person_proofing.body.location.po_search.search_button'))