-
Notifications
You must be signed in to change notification settings - Fork 166
LG-7720: IPP: PO Search SWR refactor and enhancements #7468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c2f51c4
956b041
dde0cb5
395d797
e27c30a
af80882
0565404
9f7a41c
745ee80
c0485ed
c751036
45e5dbb
caeb20a
5b462f7
01c10cb
38e27e0
05be876
1f87724
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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( | ||
|
|
@@ -32,14 +31,13 @@ function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSear | |
| if (unvalidatedAddressInput === '') { | ||
| return; | ||
| } | ||
| const addressCandidates = await request(ADDRESS_SEARCH_URL, { | ||
| const addressCandidates = await request<Location>(ADDRESS_SEARCH_URL, { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed the |
||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| json: { address: unvalidatedAddressInput }, | ||
| }); | ||
|
|
||
| const bestMatchedAddress = addressCandidates[0]; | ||
| setAddressQuery(bestMatchedAddress); | ||
| onAddressFound(bestMatchedAddress); | ||
| }, | ||
| [unvalidatedAddressInput], | ||
|
|
@@ -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}</> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| 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; | ||
|
|
@@ -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; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) => { | ||
|
|
@@ -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); | ||
|
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); | ||
|
|
@@ -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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }); | ||
|
|
@@ -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, | ||
|
|
@@ -140,7 +133,7 @@ function InPersonLocationPostOfficeSearchStep({ onChange, toPreviousStep, regist | |
| setInProgress(false); | ||
| }); | ||
| }, | ||
| [locationData, inProgress], | ||
| [locationResults, inProgress], | ||
| ); | ||
|
|
||
| const handleFoundAddress = useCallback((address) => { | ||
|
allthesignals marked this conversation as resolved.
|
||
|
|
@@ -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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} /> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:' | ||
|
|
||
There was a problem hiding this comment.
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