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 @@ -22,7 +22,6 @@ export const ADDRESS_SEARCH_URL = '/api/addresses';
function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSearchProps) {
const validatedFieldRef = useRef<HTMLFormElement | null>(null);
const [unvalidatedAddressInput, setUnvalidatedAddressInput] = useState('');
const [addressQuery, setAddressQuery] = useState({} as Location);
const { t } = useI18n();

const handleAddressSearch = useCallback(
Expand All @@ -32,14 +31,13 @@ function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSear
if (unvalidatedAddressInput === '') {
return;
}
const addressCandidates = await request(ADDRESS_SEARCH_URL, {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only existed in order to display on screen

const addressCandidates = await request<Location>(ADDRESS_SEARCH_URL, {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the request library so that responses need to be typed

method: 'POST',
headers: { 'Content-Type': 'application/json' },
json: { address: unvalidatedAddressInput },
});

const bestMatchedAddress = addressCandidates[0];
setAddressQuery(bestMatchedAddress);
onAddressFound(bestMatchedAddress);
},
[unvalidatedAddressInput],
Expand Down Expand Up @@ -68,7 +66,6 @@ function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSear
<Button type="submit" className="margin-y-5" onClick={handleAddressSearch}>
{t('in_person_proofing.body.location.po_search.search_button')}
</Button>
<>{addressQuery.address}</>
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { useState, useEffect, useCallback, useRef, useContext } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { PageHeading, SpinnerDots } from '@18f/identity-components';
import { PageHeading } from '@18f/identity-components';
import { request } from '@18f/identity-request';
import useSWR from 'swr';
import BackButton from './back-button';
import LocationCollection from './location-collection';
import LocationCollectionItem from './location-collection-item';
import AnalyticsContext from '../context/analytics';
import AddressSearch from './address-search';
import InPersonContext from '../context/in-person';
import InPersonLocations, { FormattedLocation } from './in-person-locations';

interface PostOffice {
address: string;
Expand All @@ -22,31 +21,16 @@ interface PostOffice {
zip_code_5: string;
}

interface FormattedLocation {
formattedCityStateZip: string;
id: number;
name: string;
phone: string;
saturdayHours: string;
streetAddress: string;
sundayHours: string;
weekdayHours: string;
}
interface LocationQuery {
streetAddress: string;
city: string;
state: string;
zipCode: string;
address: string;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added so that we can use the full address in messages

}

export const LOCATIONS_URL = '/verify/in_person/usps_locations';

const getUspsLocations = (address) =>
request(LOCATIONS_URL, {
method: 'post',
json: { address },
});

const formatLocation = (postOffices: PostOffice[]) => {
const formattedLocations = [] as FormattedLocation[];
postOffices.forEach((po: PostOffice, index) => {
Expand All @@ -72,23 +56,32 @@ const snakeCase = (value: string) =>
.toLowerCase();

// snake case the keys of the location
const prepToSend = (location: object) => {
const transformKeys = (location: object, predicate: (key: string) => string) => {
const sendObject = {};
Object.keys(location).forEach((key) => {
sendObject[snakeCase(key)] = location[key];
sendObject[predicate(key)] = location[key];
});
return sendObject;
};

const requestUspsLocations = async (address: LocationQuery): Promise<FormattedLocation[]> => {
const response = await request<PostOffice[]>(LOCATIONS_URL, {
method: 'post',
json: { address: transformKeys(address, snakeCase) },
});

return formatLocation(response);
};

function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, registerField }) {
const { t } = useI18n();
const [locationData, setLocationData] = useState([] as FormattedLocation[]);
const [foundAddress, setFoundAddress] = useState({} as LocationQuery);
const [foundAddress, setFoundAddress] = useState<LocationQuery | null>(null);
const [inProgress, setInProgress] = useState(false);
const [autoSubmit, setAutoSubmit] = useState(false);
const [isLoadingComplete, setIsLoadingComplete] = useState(false);
const { setSubmitEventMetadata } = useContext(AnalyticsContext);
const { arcgisSearchEnabled } = useContext(InPersonContext);
Comment thread
allthesignals marked this conversation as resolved.
Outdated
const { data: locationResults } = useSWR([LOCATIONS_URL, foundAddress], ([, address]) =>
address ? requestUspsLocations(address) : null,
);

// ref allows us to avoid a memory leak
const mountedRef = useRef(false);
Expand All @@ -103,7 +96,7 @@ function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, regist
// useCallBack here prevents unnecessary rerenders due to changing function identity
const handleLocationSelect = useCallback(
async (e: any, id: number) => {
const selectedLocation = locationData[id];
const selectedLocation = locationResults![id]!;
Comment on lines -106 to +99
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not great because locationResults can potentially be null!

const { name: selectedLocationName } = selectedLocation;
setSubmitEventMetadata({ selected_location: selectedLocationName });
onChange({ selectedLocationName });
Expand All @@ -115,7 +108,7 @@ function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, regist
if (inProgress) {
return;
}
const selected = prepToSend(selectedLocation);
const selected = transformKeys(selectedLocation, snakeCase);
setInProgress(true);
await request(LOCATIONS_URL, {
json: selected,
Expand All @@ -140,7 +133,7 @@ function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, regist
setInProgress(false);
});
},
[locationData, inProgress],
[locationResults, inProgress],
);

const handleFoundAddress = useCallback((address) => {
Comment thread
allthesignals marked this conversation as resolved.
Expand All @@ -149,65 +142,23 @@ function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, regist
city: address.city,
state: address.state,
zipCode: address.zip_code,
address: address.address,
});
}, []);

useEffect(() => {
let didCancel = false;
(async () => {
try {
const fetchedLocations = await getUspsLocations(prepToSend(foundAddress));

if (!didCancel) {
const formattedLocations = formatLocation(fetchedLocations);
setLocationData(formattedLocations);
}
} finally {
if (!didCancel) {
setIsLoadingComplete(true);
}
}
})();
return () => {
didCancel = true;
};
}, [foundAddress]);

let locationsContent: React.ReactNode;
if (!isLoadingComplete) {
locationsContent = <SpinnerDots />;
} else if (locationData.length < 1) {
locationsContent = <h4>{t('in_person_proofing.body.location.none_found')}</h4>;
} else {
locationsContent = (
<LocationCollection>
{locationData.map((item, index) => (
<LocationCollectionItem
key={`${index}-${item.name}`}
handleSelect={handleLocationSelect}
name={`${item.name} — ${t('in_person_proofing.body.location.post_office')}`}
streetAddress={item.streetAddress}
selectId={item.id}
formattedCityStateZip={item.formattedCityStateZip}
weekdayHours={item.weekdayHours}
saturdayHours={item.saturdayHours}
sundayHours={item.sundayHours}
/>
))}
</LocationCollection>
);
}

return (
<>
<PageHeading>{t('in_person_proofing.headings.po_search.location')}</PageHeading>
<p>{t('in_person_proofing.body.location.po_search.po_search_about')}</p>
{arcgisSearchEnabled && (
<AddressSearch onAddressFound={handleFoundAddress} registerField={registerField} />
Comment on lines -205 to -206
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant due to this PR, so removed it.

<AddressSearch onAddressFound={handleFoundAddress} registerField={registerField} />
{locationResults && (
<InPersonLocations
locations={locationResults}
onSelect={handleLocationSelect}
address={foundAddress?.address || ''}
/>
)}
<p>{t('in_person_proofing.body.location.location_step_about')}</p>
{locationsContent}
<BackButton onClick={toPreviousStep} />
<BackButton includeBorder onClick={toPreviousStep} />
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ describe('InPersonLocationStep', () => {
await userEvent.click(
await findByText('in_person_proofing.body.location.po_search.search_button'),
);
await findByText('100 Main St, South Fulton, Tennessee, 38257');
await findByText('in_person_proofing.body.location.po_search.results_description');
expect(window.fetch).to.have.been.calledWith(
LOCATIONS_URL,
sandbox.match({
body: '{"address":{"street_address":"100 Main St","city":"South Fulton","state":"TN","zip_code":"38257"}}',
body: '{"address":{"street_address":"100 Main St","city":"South Fulton","state":"TN","zip_code":"38257","address":"100 Main St, South Fulton, Tennessee, 38257"}}',
method: 'post',
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface LocationQuery {
export const LOCATIONS_URL = '/verify/in_person/usps_locations';

const getUspsLocations = (address) =>
request(LOCATIONS_URL, {
request<PostOffice[]>(LOCATIONS_URL, {
method: 'post',
json: { address },
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useI18n } from '@18f/identity-react-i18n';
import LocationCollection from './location-collection';
import LocationCollectionItem from './location-collection-item';

export interface FormattedLocation {
formattedCityStateZip: string;
id: number;
name: string;
phone: string;
saturdayHours: string;
streetAddress: string;
sundayHours: string;
weekdayHours: string;
}
Comment thread
allthesignals marked this conversation as resolved.
Outdated

interface InPersonLocationsProps {
locations: FormattedLocation[] | null | undefined;
onSelect;
address: string;
}

function InPersonLocations({ locations, onSelect, address }: InPersonLocationsProps) {
const { t } = useI18n();

if (locations?.length === 0) {
return (
<>
<h3>{t('in_person_proofing.body.location.po_search.none_found', { address })}</h3>
<p>{t('in_person_proofing.body.location.po_search.none_found_tip')}</p>
</>
);
}

return (
<>
<h3>
{t('in_person_proofing.body.location.po_search.results_description', {
address,
count: locations?.length,
})}
</h3>
<p>{t('in_person_proofing.body.location.po_search.results_instructions')}</p>
<LocationCollection>
{(locations || []).map((item, index) => (
<LocationCollectionItem
key={`${index}-${item.name}`}
handleSelect={onSelect}
name={`${item.name} — ${t('in_person_proofing.body.location.post_office')}`}
streetAddress={item.streetAddress}
selectId={item.id}
formattedCityStateZip={item.formattedCityStateZip}
weekdayHours={item.weekdayHours}
saturdayHours={item.saturdayHours}
sundayHours={item.sundayHours}
/>
))}
</LocationCollection>
</>
);
}

export default InPersonLocations;
3 changes: 2 additions & 1 deletion app/javascript/packages/document-capture/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "1.0.0",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"swr": "^2.0.0"
}
}
7 changes: 5 additions & 2 deletions app/javascript/packages/request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ interface RequestOptions extends RequestInit {
const getCSRFToken = () =>
document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;

export const request = async (url: string, options: Partial<RequestOptions> = {}) => {
export async function request<Response>(
Comment thread
allthesignals marked this conversation as resolved.
Outdated
url: string,
options: Partial<RequestOptions> = {},
): Promise<Response> {
const { csrf = true, json = true, ...fetchOptions } = options;
let { body, headers } = fetchOptions;
headers = new Headers(headers);
Expand All @@ -39,4 +42,4 @@ export const request = async (url: string, options: Partial<RequestOptions> = {}

const response = await window.fetch(url, { ...fetchOptions, headers, body });
return json ? response.json() : response.text();
};
}
9 changes: 9 additions & 0 deletions config/locales/in_person_proofing/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,17 @@ en:
po_search:
address_search_hint: 'Example: 1960 W Chelsea Ave Allentown PA 18104'
address_search_label: Enter an address to find a Post Office near you
none_found: Sorry, there are no participating Post Offices within 50 miles of
%{address}.
none_found_tip: You can search using a different address, or add photos of your
ID to try and verify your identity online again.
po_search_about: If you are having trouble adding your ID, you may be able to
verify your identity in person at a local United States Post Office.
results_description: There are %{count} participating Post Offices within 50
miles of %{address}.
results_instructions: Select a Post Office location below, or search again using
a different address. For facility accessibility, use the contact
information listed for the Post Office location.
search_button: Search
post_office: Post Office ™
retail_hours_heading: Retail Hours
Expand Down
11 changes: 11 additions & 0 deletions config/locales/in_person_proofing/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,20 @@ es:
address_search_hint: 'Ejemplo: 1960 W Chelsea Ave Allentown PA 18104'
address_search_label: Introduzca una dirección para encontrar una Oficina de
Correos cercana a usted
none_found: Lo sentimos, no hay Oficinas de Correos participantes en un radio de
50 millas de la %{address}.
none_found_tip: Puede buscar utilizando una dirección diferente, o añadir fotos
de su documento de identidad para intentar verificar su identidad en
línea de nuevo.
po_search_about: Si tiene problemas para añadir su documento de identidad, es
posible que pueda verificar su identidad en persona en una oficina
de correos local de los Estados Unidos.
results_description: Hay %{count} de oficinas de correos participantes en un
radio de 50 millas de la %{address}.
results_instructions: Seleccione una ubicación de la Oficina de Correos a
continuación, o busque de nuevo utilizando una dirección diferente.
Para la accesibilidad de las instalaciones, utilice la información
de contacto que aparece para la ubicación de la Oficina de Correos.
search_button: Búsqueda
post_office: Oficina de Correos
retail_hours_heading: Horario de atención al público
Expand Down
12 changes: 11 additions & 1 deletion config/locales/in_person_proofing/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,20 @@ fr:
po_search:
address_search_hint: 'Exemple : 1960 W Chelsea Ave Allentown PA 18104'
address_search_label: Entrez une adresse pour trouver un bureau de poste près de chez vous.
none_found: Placeholder %{address}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to actually translate this before merging (or mark as draft for now) since this is what a user would see in production.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if there's an outstanding story it may be fine since this is behind a FF.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me do some cleanup on the screenshots and styling. Thanks for checking that. One of those screenshots shows intermediate progress on this and needs updated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated screenshots.

none_found_tip: Vous pouvez effectuer une recherche en utilisant une autre
adresse, ou ajouter des photos de votre pièce d’identité pour
essayer de vérifier à nouveau votre identité en ligne.
po_search_about: Si vous avez des difficultés à ajouter votre pièce d’identité,
vous pouvez vérifier votre identité en personne dans un bureau de
poste américain proche.
search_button: Chercher
results_description: Il y a %{count} de bureaux de poste participants dans un
rayon de 80 km autour de %{address}.
results_instructions: Sélectionnez un emplacement de bureau de poste ci-dessous,
ou effectuez une nouvelle recherche en utilisant une autre adresse.
Pour l’accessibilité des installations, utilisez les informations de
contact indiquées pour l’emplacement du bureau de poste.
search_button: Rechercher
post_office: Bureau de Poste
retail_hours_heading: Heures de vente au détail
retail_hours_sat: 'Sam:'
Expand Down
Loading